Skip to content

fix(docx-core): corrected reject/accept round-trip oracle — projection-to-projection (#347)#361

Merged
stevenobiajulu merged 1 commit into
mainfrom
347-corrected-roundtrip-oracle-relax-arbitrary-20260608
Jun 10, 2026
Merged

fix(docx-core): corrected reject/accept round-trip oracle — projection-to-projection (#347)#361
stevenobiajulu merged 1 commit into
mainfrom
347-corrected-roundtrip-oracle-relax-arbitrary-20260608

Conversation

@stevenobiajulu

Copy link
Copy Markdown
Member

Fixes #347. Fixes #339 (the INV-RT-001 CI flake — root cause, not a rerun band-aid).

What

The inplace reconstruction safety check compared the candidate's accept/reject projections against the raw extracted text of the inputs. extractTextWithParagraphs counts both w:t and w:delText, so on a pre-tracked input the raw baseline is neither projection — the check was falsified by construction on legitimate inputs and forced spurious inplace→rebuild fallbacks. The corrected law is apples-to-apples projection equality:

  • accept-all(combined) == accept-all(revised input)
  • reject-all(combined) == reject-all(original input)

For clean inputs the projections equal the raw extraction, so the common case is a no-op. Three layers encode the corrected law in lockstep:

  1. Enginepipeline.ts round-trip baselines project each input through the same accept/reject operation the candidate is checked under (evaluateRoundTripSafety only; the soft bookmark baselines never trigger fallback and deliberately keep their raw shape).
  2. Lean (Tier 2)compareDocumentXml_output_text_roundtrip restated over the inputs' revisedText/originalText projections; inv_rt_001 restated as projection equality and closed by applying the existing machine-checked lemmas on both combined and the inputs. Zero sorry; #print axioms inv_rt_001 lists propext, Classical.choice, Quot.sound, compareDocumentXml, compareDocumentXml_output_text_roundtrip — the only text residual axiom is unchanged in name and count; no new residual was introduced.
  3. Bridge testassertRoundTripInvariant asserts the projection surface (the old theorem-domain triage category collapsed into the law), and trackedPairArb was relaxed to the corrected oracle's scope, paragraph-insert originals included.

Scope of the guarantee (deliberate)

Projection-to-projection guarantees global accept/reject text round-trip safety. It does not validate the raw mixed-revision markup surface or author provenance — a candidate whose projections are correct but whose intermediate markup is ugly or whose attribution is wrong can pass. That is the intended contract of this text oracle; the Lean axiom doc, theorem doc, and bridge prose now say so explicitly.

Discovery payoff — two engine bug classes pinned and filed

Relaxing the arbitrary did its job (aggressive discovery, bounded remediation). Every surfaced failure was adjudicated by XML before labeling:

Both pins carry a characterization test asserting today's behavior verbatim (mode=rebuild, failedChecks=["rejectText"], the exact reject divergences), so the eventual fixes flip it deliberately — the [LEAN-HELP-08]/G5 cadence. By contrast, paragraph-MARK provenance already threads correctly (stacked PPR-DEL(Comparison) + PPR-INS(input-author)), which the included two-author stacked-insertion test locks in: it stays inplace, carries both authors' revisions, and round-trips projection-to-projection.

Plan deviation worth knowing: the plan predicted the exact #339 shape would stop falling back. It doesn't — that shape is a genuine #359 provenance conflict, and under the corrected oracle its fallback is a true positive (the rebuild output satisfies the law). What the oracle fix does eliminate is the spurious-fallback families: the PR #344 seed-1586860402 family (comment-anchor original + paragraph-insert revised) and the peer-review paragraph-insert-original families now stay inplace with empty fallbackDiagnostics.

Verification

  • lean-spec-bridge.test.ts: 14/14, six consecutive fresh-seed runs
  • Fixture suites (round-trip, round-trip-inplace, nvca-coi-regression, roundtrip-structural-invariants): 34/34
  • lean-differential-helpers (accept/reject helpers untouched): 11/11
  • Full docx-core: 1357/1357
  • lake build green; zero-sorry grep over LeanSpike* + Tier2*
  • Pre-submit chain green: build, lint:workspaces, test:run, check:spec-coverage, check:conformance-citations, check:conformance-doc
  • Housekeeping: removed the verification/lean/print_axioms.lean review scratch (untracked)

Ref #344, #226.

…to-projection (#347)

The inplace reconstruction safety check compared the candidate's
accept/reject PROJECTIONS against the RAW extracted text of the inputs
(pipeline.ts originalTextForRoundTrip / revisedTextForRoundTrip).
extractTextWithParagraphs counts both w:t and w:delText, so on an input
that already carries its own tracked changes the raw baseline is neither
the accept- nor the reject-projection — the check was falsified by
construction on legitimate pre-tracked inputs and forced spurious
inplace→rebuild fallbacks (the INV-RT-001 CI flake, #339, and the PR #344
seed-1586860402 family).

The corrected law is apples-to-apples projection equality:
  accept-all(combined) == accept-all(revised input)
  reject-all(combined) == reject-all(original input)
For clean inputs the projections equal the raw extraction, so the common
case is unchanged. Accept/reject are global across all authors; the law
constrains text projections only, not the raw mixed-revision markup or
author provenance (a deliberate scope choice, documented in the Lean and
bridge prose). The soft bookmark baselines (never fallback-triggering)
keep the raw shape and are out of scope.

Three layers encode the corrected law in lockstep:
- engine: project each input through the same accept/reject operation the
  candidate is checked under (pipeline.ts baselines feeding
  evaluateRoundTripSafety);
- Lean: compareDocumentXml_output_text_roundtrip restated over the
  inputs' revisedText/originalText projections; inv_rt_001 restated as
  projection equality, closed by applying the EXISTING machine-checked
  lemmas (extractText_accept_normalized, extractText_reject) on both
  combined and the inputs — zero sorry, no new residual axiom
  (#print axioms inv_rt_001: propext, Classical.choice, Quot.sound,
  compareDocumentXml, compareDocumentXml_output_text_roundtrip);
- bridge test: assertRoundTripInvariant now asserts the projection
  surface (the former theorem-domain triage category collapsed into the
  law) and trackedPairArb spans paragraph-insert originals.

Discovery payoff (aggressive discovery, bounded remediation): relaxing
the arbitrary surfaced two genuine engine bug classes, adjudicated by
XML, pinned with narrow issue-linked exclusions plus a characterization
test asserting today's divergences verbatim, and filed:
- #358 inline run-level pre-tracked w:ins ORIGINALS: insertion
  provenance flattened unconditionally; reject(combined) keeps text
  reject(original) drops, on both reconstruction paths.
- #359 pre-tracked insertion text colliding with the other side's plain
  text: one lineage's provenance wins; the inline shape (the exact #339
  counterexample) is rescued by the rebuild fallback, the
  paragraph-insert shapes ship law-violating output.
A dedicated two-author stacked-insertion test locks in the committed
global accept/reject semantics (stays inplace, both authors' revisions
present, projections round-trip).

Verification: lean-spec-bridge 14/14 across 6 fresh-seed runs; fixture
round-trip suites 34/34; helper differential 11/11 (accept/reject
helpers untouched); full docx-core 1357/1357; lake build green,
zero-sorry; PR#344 comment-anchor+paragraph-insert family and the
paragraph-insert-original peer-review family now stay inplace with empty
fallbackDiagnostics.

Fixes: #347
Fixes: #339
Ref: #358, #359, #344, #226
@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
site Ready Ready Preview, Comment Jun 10, 2026 4:00am

Request Review

@github-actions github-actions Bot added the fix label Jun 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

LLM-Based Quality Gate

Overall: ✅ PASS (5 pass · 0 warn · 9 skipped · 14 total)

Check Verdict
⏭️ SKIPPED read_file response metadata parity paths not touched by this PR
⏭️ SKIPPED Live DOM namespace-safe OOXML writes paths not touched by this PR
Deleted field markup keeps w:fldChar outside w:del The PR does not touch field atomization, validateFieldStructure, hasFldCharInsideDel, w:fldChar, w:instrText, w:delInstrText, or collapsed field comparison logic; it instead updates the round-trip text projections to accept/reject changes under #347.
Field validation per story, not global Field validation is run independently per ECMA story in validateFieldStructure (pipeline.ts:490-492) by checking each FieldStory extracted by splitStories. Sidecars from both original and revised archives are considered via auxiliarySidecars (pipeline.ts:763-766), and global counter balance is not treated as sufficient (pipeline.ts:504-511).
⏭️ SKIPPED Revision IDs seeded from all revision-bearing side parts paths not touched by this PR
Accept/reject sweep side parts and caches The PR does not touch DocxDocument.acceptChanges, DocxDocument.rejectChanges, REVISION_STORY_PART_PATHS, accept_changes, reject_changes, or side-part revision markup; it only updates the projection-based round-trip safety checks in pipeline.ts and related spec/verification files.
⏭️ SKIPPED DocumentViewNode.heading stays canonical paths not touched by this PR
⏭️ SKIPPED AI-author parity across entry points paths not touched by this PR
⏭️ SKIPPED Property-change wrapper discipline paths not touched by this PR
⏭️ SKIPPED SUPPORT.md Table A drift vs. implementation paths not touched by this PR
⏭️ SKIPPED Table A / Table B boundary on side-part revisions paths not touched by this PR
⏭️ SKIPPED Canonical-emission surface completeness paths not touched by this PR
Lean predicate drift against engine semantics (asymmetric) The PR correctly updates both the TS engine's round-trip check to use projection-to-projection comparison (pipeline.ts:782-783) and the Lean residual predicate (Spec.lean:191-194) and bridge tests (lean-spec-bridge.test.ts:1152-1153) in unison under Issue #347.
Unit-test quality (avoid tautological / change-detector tests) The test added at packages/docx-core/src/integration/lean-spec-bridge.test.ts:1375-1444 uses first-principles scenario construction to verify multi-author stacked insertions, while the characterization tests at packages/docx-core/src/integration/lean-spec-bridge.test.ts:1446-1670 verify precise engine bug behaviors with hardcoded expected values rather than SUT mocks.
Full checklist questions
  1. read_file response metadata parity: If this PR touches packages/docx-mcp/src/tools/read_file.ts, budgeted pagination returns, or additive response metadata like warnings / comment_load_error, do every successful return path (default budgeted early return, non-budget fallthrough, explicit limit/node_ids) preserve the same additive diagnostic fields? read_file has multiple success exits; diagnostics have already disappeared on one path before. Reference: fix(docx-core): declare xmlns:w14/w15 on comments root before writing prefixed attributes (#154) #180 surfaced comment_load_error, fix(docx-mcp): warn when read_file budget is exceeded by a single node (closes #184) #186 added an early budget return + warnings, fix(docx-mcp): surface comment_load_error on the default budgeted read path (closes #189) #191 fixed the missing comment_load_error on the default budgeted path.

  2. Live DOM namespace-safe OOXML writes: If this PR touches packages/docx-core/src/primitives/comments.ts or writes prefixed OOXML attributes/elements (w14:*, w15:*, xmlns:*, comments.xml, commentsExtended.xml, people.xml), are prefixed OOXML names written with namespace-aware APIs — root aliases bound with setAttributeNS(XMLNS_NS, ...), prefixed attributes with setAttributeNS(W14_NS/W15_NS, ...), and is there a test that proves the live DOM works before serialization/reparse? String-prefixed attributes can serialize plausibly while the live DOM still throws namespace errors. Reference: fix(docx-core): declare xmlns:w14/w15 on comments root before writing prefixed attributes (#154) #180 (xmlns:w14/w15 declared on comments root before writing prefixed attrs).

  3. Deleted field markup keeps w:fldChar outside w:del: If this PR touches field atomization, validateFieldStructure, hasFldCharInsideDel, w:fldChar, w:instrText, w:delInstrText, or collapsed field comparison logic, does deleted field output stay ECMA-376-conformant — w:fldChar sibling-level (never inside w:del), deleted instructions use w:delInstrText only inside valid delete wrappers, accept/reject safety checks still reject malformed combined output? Word treats deleted field-state markup in the wrong container as document-corrupting. References: fix(docx-core): validate w:delInstrText placement and reject w:fldChar inside <w:del> #211, fix(docx-core): partition field-closure validation by ECMA-376 story (#212) #225, fix(docx-core): fragment w:fldChar outside w:del per ECMA-376 Part 4 #228.

  4. Field validation per story, not global: If this PR touches packages/docx-core/src/baselines/atomizer/pipeline.ts, splitStories, validateFieldStructure, side-part merge logic, or footnote/endnote field handling, is field validation run independently per ECMA story (document.xml, each footnote, each endnote), with sidecars from both original and revised archives considered, and global counter balance not treated as sufficient? A document can be globally balanced but have an invalid field sequence inside one story. References: fix(docx-core): partition field-closure validation by ECMA-376 story (#212) #225, fix(docx-core): fragment w:fldChar outside w:del per ECMA-376 Part 4 #228, feat(docx-core): sweep side-part revisions on accept/reject #218.

  5. Revision IDs seeded from all revision-bearing side parts: If this PR touches packages/docx-mcp/src/session/manager.ts (especially getRevisionContextForSession or FIXED_REVISION_ID_SEED_PARTS), createRevisionContext, revision-ID allocation, or MCP tools that create tracked changes/comments/footnotes, does revision-ID allocation scan all relevant package parts before issuing new IDs — comments, footnotes, endnotes, glossary, headers, footers — ignore non-revision w:id values (comment IDs, bookmarks), and handle malformed optional parts gracefully? Revision IDs are package-wide; document-only seeding collides with existing side-part revisions. Reference: fix(docx-mcp): seed revision ids from side parts #216 (seed revision ids from side parts).

  6. Accept/reject sweep side parts and caches: If this PR touches DocxDocument.acceptChanges, DocxDocument.rejectChanges, REVISION_STORY_PART_PATHS, accept_changes, reject_changes, or side-part revision markup, does accept/reject process every revision-bearing story — updating document.xml + footnotes.xml + endnotes.xml + comments.xml, writing back only changed side parts while refreshing cached XML, and pruning orphan footnotes without deleting reserved separator entries? Accepting only in the main document leaves stale revisions and dangling references in the package. References: feat(docx-core): sweep side-part revisions on accept/reject #218, fix(docx-mcp): seed revision ids from side parts #216, fix(docx-core): partition field-closure validation by ECMA-376 story (#212) #225.

  7. DocumentViewNode.heading stays canonical: If this PR touches packages/docx-core/src/primitives/document_view.ts, HeadingValue, heading heuristics, ListMetadata.header_style, or Google Docs document-view heading normalization, does node.heading remain a structural heading signal — exact Word styles Heading1Heading6 win, heuristic sources suppressed inside table cells while real Word heading styles still pass, ordinary body paragraphs omit the heading key? Consumers use node.heading != null as a structural test; heuristic false positives break downstream navigation. References: fix(docx-core): harden heading detection (#157 Phase 1) #178, fix(docx-core): suppress non-sectional false-positive headings (closes #187) #188, feat(docx-core): add derived heading object to DocumentViewNode (closes #179) #190.

  8. AI-author parity across entry points: If this PR touches packages/docx-mcp/src/server.ts, packages/docx-mcp/src/cli/tool_runner.ts, packages/docx-mcp/src/cli/commands/**, or adds any new new SessionManager(...) call site in docx-mcp, does every entry point that constructs a SessionManager resolve SAFE_DOCX_AI_AUTHOR with the same three-way semantics (set → use it; empty string → opt out to untracked; unset → defaultAiAuthor), or has a new entry path silently bypassed tracked emission? Each entry path looks locally correct while diverging from another; tracked emission has gone dark in one path before anyone noticed. References: feat(docx-mcp): wire configurable AI author through MCP layer (#142) #172 (production MCP wiring would have kept tracked emission dark), fix(docx-mcp): honor SAFE_DOCX_AI_AUTHOR in CLI entry points (#181) #182 (CLI runners constructing bare SessionManager() silently produced untracked edits).

  9. Property-change wrapper discipline: If this PR touches packages/docx-core/src/primitives/layout.ts, packages/docx-core/src/primitives/text.ts, packages/docx-mcp/src/tools/clear_formatting.ts, or packages/docx-core/src/primitives/track-changes-emitter.ts, do tracked formatting/property edits emit exactly one correct *PrChange wrapper (pPrChange / rPrChange / trPrChange / tcPrChange) carrying a snapshot of the prior live properties — not stacking stale wrappers, not stripping valid historical children (cellIns/cellDel/cellMerge), and not omitting the snapshot when the operation is formatting-aware? Emitted OOXML is visually plausible but subtle snapshot mistakes only surface during later accept/reject or in Word's tracked-changes UI. References: feat(docx-core): emit pPrChange/trPrChange/tcPrChange from layout setters (#140) #167 (duplicate pPrChange/trPrChange/tcPrChange stacking + over-broad tcPr exclusion), feat(docx-mcp): emit rPrChange from clear_formatting MCP tool (#141) #170 (clear_formatting failing to strip stale rPrChange), feat(docx-core): emit rPrChange for formatted paragraph replacements #215 (rPrChange for formatted paragraph replacements + filtering nested stale records).

  10. SUPPORT.md Table A drift vs. implementation: If this PR modifies OOXML revision emission behavior (w:ins, w:del, w:rPrChange, etc.) in packages/docx-core/src/primitives/**, or touches packages/docx-core/SUPPORT.md, does the PR symmetrically update Table A in SUPPORT.md when the supported revision-emission surface in primitives changed — added, removed, or weakened — or is the documented contract now lying about what's supported? Reviewers focus on TS AST correctness and golden tests; Markdown contract tables get treated as an afterthought, so the documented surface drifts from the actual surface. Reference: [120.8] Regression suite for canonical revision emission across the surface #143 review caught replaceParagraphTextRange should emit w:rPrChange when run formatting changes #173 (formatting mismatch in Table A) and addCommentReply should emit body revision markup OR SUPPORT.md should be softened #174 (comment body revision omission forcing a Table A softening) late in peer review.

  11. Table A / Table B boundary on side-part revisions: If this PR touches packages/docx-core/src/primitives/comments.ts, packages/docx-core/src/primitives/footnotes.ts, or other side-part primitives, and adds/changes revision markup (w:ins, w:del), does tracked-change revision logic stay scoped to Table A (document-body content inside the side part) without leaking revision markup into Table B (the side-part package bootstrap — comments.xml/footnotes.xml element registration itself)? Body runs and side-part package elements share nearly identical XML namespace schemas; revisions emitted in the wrong table corrupt the package contract while looking plausible. References: [120.3] Emit w:ins/w:del for comment body anchors #138 (comment-body straddle constraints), [120.4] Emit w:ins/w:del for footnote reference and text #139 (footnote-reference straddle constraints).

  12. Canonical-emission surface completeness: If this PR adds or changes a tracked-edit surface in packages/docx-core/src/primitives/** or packages/docx-mcp/src/tools/**, are the paired artifacts updated together — packages/docx-core/src/integration/canonical-emission-regression.test.ts, packages/docx-mcp/src/integration/canonical-emission-mcp.test.ts, and the documented emitter surface (Table A) — or is the rollout only partially wired? The primitive change looks done before the MCP path, regression matrix, and documented contract are wired through; partial rollouts ship undocumented surface that drifts. References: feat(docx-mcp): wire configurable AI author through MCP layer (#142) #172 (RevisionContext threaded through every Table A MCP tool), test(docx-core,docx-mcp): final regression suite for canonical emission (#143) #175 (24-test regression suite + verified write-time emitter rows), feat(docx-core): emit rPrChange for formatted paragraph replacements #215 (re-enabled rPrChange regression + updated support surface for replaceParagraphTextRange).

  13. Lean predicate drift against engine semantics (asymmetric): If this PR changes field-wrapper semantics, the proof boundary, or atomizer behavior — packages/docx-core/src/baselines/atomizer/**, verification/lean/LeanSpike/Spec.lean, verification/lean/Tier2/**, or packages/docx-core/src/integration/lean-spec-bridge.test.ts — and if the TS engine semantics shifted, did the PR also update the Lean residual predicate and bridge tests, or is the proof now pinned to a stale stronger/weaker assumption? Asymmetric: a TS change without a corresponding Lean update is WARN; a Lean-only change without a TS update should not fire. The Lean side can still compile while the abstraction boundary is subtly wrong for the next engine refactor. References: feat(verification): close inv_field_001 with Tier 2 OoxmlDoc subset #208 (closed inv_field_001 using stronger recursivelyWellformed), refactor(verification): weaken inv_field_001 axiom to document-level preservationFriendly (rebased follow-up to #208) #220 (weakened the axiom to document-level preservationFriendly to avoid breakage when field fragmentation lands).

  14. Unit-test quality (avoid tautological / change-detector tests): If this PR adds or modifies any **/*.test.ts (or other test files), are the test assertions independent of the system under test — expected values constructed from first principles rather than re-derived from the function under test, mocks limited to external boundaries (filesystem, network, clocks) rather than mocking the SUT itself, assertions making concrete semantic claims rather than just snapshotting current behavior or asserting non-null, and any test added alongside a bug fix actually exercising the bug? Tests that re-implement the production code as the "expected" value, or mock out the system under test, pass green while providing no regression protection.

Estimated cost (this run): $0.0136 — 41,803 input + 377 output tokens (≈4 chars/token) on gemini-3.5-flash. Char-count estimate, not provider telemetry.

@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@stevenobiajulu
stevenobiajulu merged commit 77c4c04 into main Jun 10, 2026
39 of 40 checks passed
@stevenobiajulu
stevenobiajulu deleted the 347-corrected-roundtrip-oracle-relax-arbitrary-20260608 branch June 10, 2026 04:15
@stevenobiajulu

Copy link
Copy Markdown
Member Author

✅ Post-merge smoke passed

Merged: 77c4c04cb0bc5999e295de98b60b1fad18ad232f
Built from: main @ 77c4c04
Smoke: npm run build && npm run test:run + real-document pre-tracked comparison e2e (.tmp/smoke-361.mts)

Steps

  • ✅ clean build from main HEAD (all workspaces)
  • ✅ full test suite — 2416 tests passed, 0 failed (docx-core 833+ incl. the 14 lean-spec-bridge tests, odf-core, docx-mcp, google-docs-core)
  • ✅ real-document end-to-end through the merged oracle path (below)
  • ✅ produced DOCX re-opens cleanly

Real-world fixtures

  • common-paper-mutual-nda (local template at open-agreements/content/templates/common-paper-mutual-nda/template.docx, 19,548 bytes)
    • Original = real NDA + tracked paragraph insertion by "Jane Doe" ("Receiving Party shall return or destroy all Confidential Information within thirty (30) days of termination.") — a pre-tracked paragraph-insert original, exactly the family the old raw-text oracle spuriously bounced to rebuild.
    • Revised = same pre-tracked doc + one more Jane clause ("Northeast Logistics LLC may retain one archival copy solely for compliance purposes.").
    • compareDocuments(original, revised, {engine:'atomizer', reconstructionMode:'inplace', author:'Comparison'}):
      • reconstructionModeUsed === 'inplace', fallbackDiagnostics empty — the merged fix's payoff, live.
      • Corrected projection law holds on the combined output: accept-all(combined) == accept-all(revised) ✓ and reject-all(combined) == reject-all(original) ✓.
      • Combined document.xml carries both w:author="Jane Doe" and w:author="Comparison" revisions (5× <w:ins>), confirming the global-accept/reject multi-author semantics on a real document.

Visual evidence: rendered + opened locally — /tmp/automerge-smoke-361-20260610-001828/combined-1.png shows the real NDA cover page with both underlined redline insertions and margin change bars in the Purpose section (Jane's pre-tracked clause + the comparison-stacked clause).
Log: /tmp/automerge-smoke-361-20260610-001828.log (local).

Cleanup

  • ⚠️ remote branch 347-corrected-roundtrip-oracle-relax-arbitrary-20260608 not deleted — branch deletion blocked by the session's permission policy; delete manually if desired
  • ✅ no worktree was using the PR branch
  • ⚠️ local branch left in place (same permission policy)

stevenobiajulu added a commit that referenced this pull request Jun 10, 2026
First minor since v0.9.1 — headline: OpenDocument (.odt) support is now
installable. Covers everything accumulated on main:

- ODF lane end-to-end: odf-core library (#328), provider wiring (#335),
  grep + insert_paragraph (#336), comments (#341), compare_documents
  two-file (#348), session mode (#366), intra-paragraph (run-level)
  tracked changes (#370)
- odf-core folded into the suite release train + published as a real
  dependency of docx-mcp (#373); first publish bootstrapped manually at
  0.9.1 with the trusted publisher configured, so this tag publishes
  odf-core@0.10.0 via OIDC like the rest
- docx-core: formatting-fidelity comparison check (#365), reject/accept
  round-trip oracle stated projection-to-projection (#361), LibreOffice
  oracle trust-boundary characterization (#364)

Also in this commit: add odf-core to scripts/bump_version.mjs
(PACKAGE_JSONS + WORKSPACE_DEPS — it predated the package), and refresh
the README granularity wording (#370 upgraded ODF compare from
whole-paragraph to inline run-level tracked changes).

Generated via scripts/bump_version.mjs.

Ref: #372
stevenobiajulu added a commit that referenced this pull request Jun 10, 2026
…#379)

* chore(release): bump workspace versions to 0.10.0

First minor since v0.9.1 — headline: OpenDocument (.odt) support is now
installable. Covers everything accumulated on main:

- ODF lane end-to-end: odf-core library (#328), provider wiring (#335),
  grep + insert_paragraph (#336), comments (#341), compare_documents
  two-file (#348), session mode (#366), intra-paragraph (run-level)
  tracked changes (#370)
- odf-core folded into the suite release train + published as a real
  dependency of docx-mcp (#373); first publish bootstrapped manually at
  0.9.1 with the trusted publisher configured, so this tag publishes
  odf-core@0.10.0 via OIDC like the rest
- docx-core: formatting-fidelity comparison check (#365), reject/accept
  round-trip oracle stated projection-to-projection (#361), LibreOffice
  oracle trust-boundary characterization (#364)

Also in this commit: add odf-core to scripts/bump_version.mjs
(PACKAGE_JSONS + WORKSPACE_DEPS — it predated the package), and refresh
the README granularity wording (#370 upgraded ODF compare from
whole-paragraph to inline run-level tracked changes).

Generated via scripts/bump_version.mjs.

Ref: #372

* fix(odf-core): group adjacent word replacements in ODF inline compare (#378)

Comparing "… by and among Zephyr BioSystems, Inc., …" against
"… by and among Acme Manufacturing, Inc., …" produced an interleaved
per-word redline (Acme ~~Zephyr~~ Manufacturing, ~~BioSystems,~~) instead
of one grouped deletion + one grouped insertion. The token-level LCS in
diffInline matches the space between the two replaced words as `equal`,
which splits a single logical replacement into two delete/insert pairs;
unlike the DOCX pipeline (reorderChangeBlocks /
coalesceDelInsPairChains), the ODF inline diff had no whitespace-bridging
pass to put them back together. Grouped redlines are what Word, Word
compare, and LibreOffice all produce for multi-word replacements, so the
interleaved shape reads as broken to reviewers even though it
round-trips correctly.

The fix is a post-pass in diffInline: a maximal window of change spans
may absorb an equal span only when it is whitespace-only AND has change
spans on both sides, and a window containing at least one delete and one
insert is rewritten as a single delete+insert pair (the delete covers
every original char in the window, bridge whitespace included; the
insert every revised char). Delete-only and insert-only windows keep
their bridges as `equal` — striking a kept space just to reinsert it
would be noise, not grouping. The pass preserves the
delete-before-insert / shared-revStart convention the emitter relies on
and the lockstep-offset reconstruction invariant the property sweep
pins.

Verified the DOCX path does NOT have this bug on main (both rebuild and
inplace group correctly over the same NVCA preamble), so the fix is
scoped to odf-core. Spec scenario [OCMPI-14] added to the active
add-odf-intra-paragraph-compare change.

Fixes: #378
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

1 participant