This was written agentically; verify its assertions and edit accordingly:
What's wrong
fingerprintBlob (src/merge/object-fingerprint.ts) is used asymmetrically for textBox/pict-kind body objects, so every unmodified round-trip of a captured text-box object falsely reports as an objectConflict.
- Base side (
src/merge/diff.ts, detectObjectConflicts): fingerprintBlob(snapshot.meta.blob) fingerprints the DB-stored ObjectMeta.blob directly. Per src/parser/docx/body-objects.ts's own documented capture convention ("a text box's blob root is the HOST body paragraph (w:p) that carries the drawing run" — see the module comment near line 12-18), a textBox/pict object's blob root is the host w:p wrapping the w:r > w:drawing/w:pict run — never the bare drawing/pict node.
- Theirs side (
src/merge/extract.ts, walkObjectBlocks): when it matches an OBJECT_BLOCK_TAGS tag (w:tbl/w:drawing/w:pict) in the freshly re-parsed round-tripped document, it fingerprints [node] where node is the matched tag itself — for w:drawing/w:pict this is the bare drawing/pict node, with no host paragraph wrapper.
For table-kind objects this is symmetric (a w:tbl IS the whole block-level object, no host paragraph involved), so the existing #520/#648 table-based tests never exercise this — they all pass. For textBox/pict-kind objects the two sides fingerprint structurally different node trees (base includes an extra w:p(w:r(...)) wrapper layer that theirs lacks), so fingerprintsDiverge is always true, even for a byte-identical, unmodified round trip.
Repro
Reproduced directly against generateDocx → extractContentControls with a hand-built ObjectMeta of kind: 'textBox' (host-w:p-wrapped blob, matching body-objects.ts's own capture shape) and no edits at all:
original fingerprint: { kind: 'textBox', hash: '20a226d7...' }
extracted fingerprint: { kind: 'textBox', hash: 'e5e2d27f...' }
Hashes never match for a textBox/pict object, regardless of content.
Where this was found
Discovered while adding #648 regression coverage (draft PR for #648) for collectDrawingAnchors's object-interior vanish handling: adding a real DB → generateDocx → extractContentControls → computeDiff integration test for a textBox-kind object (mirroring the existing table-kind #648 wiring test in src/api/diff.integration.test.ts) immediately produced a spurious objectConflicts entry on an otherwise-untouched round trip. That addition was reverted out of the #648 PR since this bug is unrelated to vanish handling — this issue tracks fixing it separately.
Suggested fix direction (unverified — needs its own investigation)
Either:
walkObjectBlocks fingerprints the enclosing host w:p for w:drawing/w:pict matches (mirroring capture's own convention), or
- Capture (
body-objects.ts) is changed to store just the bare drawing/pict node as blob root for textBox/pict kind (bigger blast radius — buildObjectBlocks's re-emit and the existing #520/generator tests already depend on the host-w:p-wrapped shape).
Option 1 looks lower-risk but needs verification against the interior-uuid capture path (findInteriorUuids) and the generator's re-emit wiring before committing to it.
Impact
Any real Word document containing a text box or VML shape with anchored SpecR content controls will show a false structural conflict on every diff, even with zero edits — this is a live correctness bug on the round-trip/merge path for a supported object kind (ADR-072).
🤖 Co-authored by Claude Sonnet 5.
This was written agentically; verify its assertions and edit accordingly:
What's wrong
fingerprintBlob(src/merge/object-fingerprint.ts) is used asymmetrically fortextBox/pict-kind body objects, so every unmodified round-trip of a captured text-box object falsely reports as anobjectConflict.src/merge/diff.ts,detectObjectConflicts):fingerprintBlob(snapshot.meta.blob)fingerprints the DB-storedObjectMeta.blobdirectly. Persrc/parser/docx/body-objects.ts's own documented capture convention ("a text box's blob root is the HOST body paragraph (w:p) that carries the drawing run" — see the module comment near line 12-18), a textBox/pict object'sblobroot is the hostw:pwrapping thew:r > w:drawing/w:pictrun — never the bare drawing/pict node.src/merge/extract.ts,walkObjectBlocks): when it matches anOBJECT_BLOCK_TAGStag (w:tbl/w:drawing/w:pict) in the freshly re-parsed round-tripped document, it fingerprints[node]wherenodeis the matched tag itself — forw:drawing/w:pictthis is the bare drawing/pict node, with no host paragraph wrapper.For
table-kind objects this is symmetric (aw:tblIS the whole block-level object, no host paragraph involved), so the existing #520/#648 table-based tests never exercise this — they all pass. FortextBox/pict-kind objects the two sides fingerprint structurally different node trees (base includes an extraw:p(w:r(...))wrapper layer that theirs lacks), sofingerprintsDivergeis always true, even for a byte-identical, unmodified round trip.Repro
Reproduced directly against
generateDocx→extractContentControlswith a hand-builtObjectMetaofkind: 'textBox'(host-w:p-wrapped blob, matchingbody-objects.ts's own capture shape) and no edits at all:Hashes never match for a textBox/pict object, regardless of content.
Where this was found
Discovered while adding #648 regression coverage (draft PR for #648) for
collectDrawingAnchors's object-interior vanish handling: adding a real DB →generateDocx→extractContentControls→computeDiffintegration test for a textBox-kind object (mirroring the existing table-kind #648 wiring test insrc/api/diff.integration.test.ts) immediately produced a spuriousobjectConflictsentry on an otherwise-untouched round trip. That addition was reverted out of the #648 PR since this bug is unrelated to vanish handling — this issue tracks fixing it separately.Suggested fix direction (unverified — needs its own investigation)
Either:
walkObjectBlocksfingerprints the enclosing hostw:pforw:drawing/w:pictmatches (mirroring capture's own convention), orbody-objects.ts) is changed to store just the bare drawing/pict node as blob root for textBox/pict kind (bigger blast radius —buildObjectBlocks's re-emit and the existing#520/generator tests already depend on the host-w:p-wrapped shape).Option 1 looks lower-risk but needs verification against the interior-uuid capture path (
findInteriorUuids) and the generator's re-emit wiring before committing to it.Impact
Any real Word document containing a text box or VML shape with anchored SpecR content controls will show a false structural conflict on every diff, even with zero edits — this is a live correctness bug on the round-trip/merge path for a supported object kind (ADR-072).
🤖 Co-authored by Claude Sonnet 5.