fix(translator): translate without modifying the caller's source document - #135
Merged
Merged
Conversation
…ment `FieldChunkCollector` seated the caller's own leaf value into the pipeline's working tree, and write-back then mutated it in place. For a string that is harmless — strings are immutable, and the write lands on the rebuilt container's key. For a richText field the value is a whole tree of objects, so translating a document rewrote the text nodes of the source document the caller had passed in. The visible consequence was provenance. `captureFingerprint` had to be called before the pipeline ran, or it hashed the translation instead of the source and reported every fresh translation as instantly stale. Nothing enforced that: the ordering rested on two statements being in the right order in `translate-document/handler.ts`, and the test guarding it emulated the mutation on a top-level string, which the pipeline never touches — so it pinned the order without being able to catch the regression it named. Object-valued leaves are now detached when they are seated. Only the leaves the collector selects are copied — the ones a write can reach — so scalars and untranslated values pass through untouched, and `structuredClone` is never handed an arbitrary document value. `ProvenanceService.captureFingerprint` no longer demands pristine input; its docblock records why the ordering used to matter. The handler test keeps its hostile stub and now says plainly that it guards against a regression rather than mirroring what the pipeline does. Verification: 1363 unit tests (4 new), including a before/after fingerprint comparison across a real translateContent run; check-types clean; lint 58 warnings and 0 errors, identical to main for the same files; the declaration build passes. Removing the copy turns the three new richText assertions red.
SearheiParkhamchuk
requested review from
ChiefCreator and
dogfrogfog
as code owners
September 10, 2026 21:41
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
🎉 This PR is included in version 0.11.5 🎉 The release is available on npm package (@latest dist-tag) Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect
FieldChunkCollectorseats the caller's own leaf value into the pipeline's working tree, and write-back then mutates it in place.For a
textfield that is harmless: strings are immutable, and the write lands on the rebuilt container's key. For arichTextfield the value is a whole tree of objects, and the pipeline holds references straight into it — so translating a document rewrote the text nodes of the source document the caller passed in.The outer tree was never the problem:
DataReconcileralready rebuilds every container level. The corruption is entirely inside a rich-text leaf, which the field walker treats as one value and hands over by reference.How it showed up
Through provenance.
captureFingerprinthad to run before the pipeline, or it hashed the translation instead of the source and reported every fresh translation as instantly stale.Nothing enforced that. The ordering rested on two statements being in the right order in
translate-document/handler.ts, andPipelineContext.sourceDatabeingreadonlyforbids reassigning the reference, not writingsourceData.body.root.children[0].text.The test that guarded it could not have caught a regression either: it emulated the mutation by overwriting a top-level string key, which the pipeline never does.
Reproduced on
main, in the new test — two different hashes for the same source document, before and after one translation:The fix
Only the leaves the collector selects are copied — those are exactly the ones a write can reach, since the branch runs under
isTranslatableLeaf(field) && strategy.shouldTranslate(...). Scalars pass through untouched, andstructuredCloneis never handed an arbitrary document value.Cloning at
DataReconciler's leaf instead was considered and rejected: it returns every leaf, translatable or not, so it would put values throughstructuredClonethat nothing writes and that it may not be able to copy.The prohibition on
structuredClonerecorded inprojectFieldLike.tsdoes not apply here — it is about field definitions, which embed Lexical editor configs containing async functions. Document data arrives frompayload.findByIDatdepth: 0.Consequences
ProvenanceService.captureFingerprintno longer demands pristine input. Its docblock now states what is true and records why the ordering used to matter.Verification
computeSourceFingerprintbefore and after a realtranslateContentrun — the check that proves the ordering rule is retired, run rather than inferred.check-typesclean. Lint 58 warnings, 0 errors — measured on a clean checkout ofmainand again with the change: identical.turbo run buildpasses, including thetscdeclaration build.Scope
Deliberately narrow. Write-back still mutates the nodes it now owns; replacing that with a rebuild is a larger change with a competing design already on record, and is not attempted here.