feat(diff): Consolidate paragraph-format merge (B1) — pPrChange across reviewers with consensus/conflict#254
Merged
JSv4 merged 4 commits intoJul 4, 2026
Conversation
…l digest for composite attribution
…oss reviewers with consensus/conflict Split the internal TrackBlockFormatChanges into a paragraph slice (TrackParagraphFormatChanges, defaults equal so two-way is byte-identical) and the table/section slice. The composite turns the paragraph slice ON while keeping table/section OFF (B2 ceiling). A reviewer's pPr-only edit now surfaces as a FormatOnly op; MergeOneBaseBlock routes multi-reviewer pPr edits through a new ComposePPr (mirroring ComposeCellShell: 0→base, all-agree→consensus, >=2 distinct→recorded conflict resolved by policy) BEFORE the text-based consensus — so a competing pPr is never silently dropped. The composite renderer stamps w:pPrChange authored to the winning reviewer via the existing single-source path; reject ≡ base, accept ≡ policy-winner hold at the pPr-byte level (proven by a multi-reviewer byte-level round-trip stress test). Composite fuzzer (3/4/5-way) + 84-case parity scoreboard unaffected. Flips the former Consolidate ceiling pin.
…engine/CLAUDE/CHANGELOG (table-shell+section = B2)
…t pPr digest (no silent run-format drop) Adversarial review found a silent-drop regression: IsParagraphPPrOnlyEdit checked that pPr CHANGED, not that the edit was pPr-ONLY, so a FormatOnly op carrying BOTH a pPr change and a run/mark-rPr change passed. When two reviewers agreed on pPr but disagreed on run format, ComposePPr saw matching pPr digests, declared consensus, and dropped the second reviewer's run edit with no conflict. Now IsParagraphFormatEdit + ComposeParagraphFormat compose by the FULL boundary-normalized BlockSignature (run formats + pPr), so any format disagreement records a conflict — a competitor is never silently dropped. Also links the conflict id onto the winning op.
JSv4
added a commit
that referenced
this pull request
Jul 4, 2026
… sectPrChange, public opt-out, note/hdr-ftr pPrChange (#253) * feat(diff): surface TrackBlockFormatChanges as a public opt-out (additive wire; composite still forces off) * feat(ir): IrRow.TrPrExDigest — a tblPrEx-only flattened digest for tblPrExChange attribution * feat(diff): native w:tblPrExChange (row-level table property exceptions) — tracked + round-trips * feat(ir): model the inline (in-pPr) sectPr as IrParagraph.InlineSectionFormat, folded into the paragraph fingerprint * feat(diff): native mid-document w:sectPrChange (inline in-pPr section property changes) Fold the inline section format into IrModeledFormat.BlockSignature (so a mid-doc sectPr-only change classifies FormatOnly, not Unchanged, under ModeledOnly), stamp w:sectPrChange inside the emitted pPr/sectPr via an explicit-old-source ApplySectPrChange overload (snapshots old+right props before mutating, since output may alias either), and emit a per-paragraph Section-scope revision. Reject preserves refs (already generic). * test(diff): pin note/header-footer pPrChange (already works via shared dispatch) + the split/merge pPrChange decline (D1) * docs+test(diff): follow-up A — extend real-doc battery (tblPrEx), flip the closed v1 ceilings in ir_diff_engine/CLAUDE/CHANGELOG * test+docs(diff): address code-review notes — both-fire pPr+sectPr schema-order test, fresh-tblPrEx-order test, corrected harness comment, ModeledOnly inline-sectPr blind-spot doc * feat(diff): Consolidate paragraph-format merge (B1) — pPrChange across reviewers with consensus/conflict (#254) * feat(ir): IrParagraph.PPrDigest — pPrChange-comparable paragraph-shell digest for composite attribution * feat(diff): Consolidate paragraph-property merge (B1) — pPrChange across reviewers with consensus/conflict Split the internal TrackBlockFormatChanges into a paragraph slice (TrackParagraphFormatChanges, defaults equal so two-way is byte-identical) and the table/section slice. The composite turns the paragraph slice ON while keeping table/section OFF (B2 ceiling). A reviewer's pPr-only edit now surfaces as a FormatOnly op; MergeOneBaseBlock routes multi-reviewer pPr edits through a new ComposePPr (mirroring ComposeCellShell: 0→base, all-agree→consensus, >=2 distinct→recorded conflict resolved by policy) BEFORE the text-based consensus — so a competing pPr is never silently dropped. The composite renderer stamps w:pPrChange authored to the winning reviewer via the existing single-source path; reject ≡ base, accept ≡ policy-winner hold at the pPr-byte level (proven by a multi-reviewer byte-level round-trip stress test). Composite fuzzer (3/4/5-way) + 84-case parity scoreboard unaffected. Flips the former Consolidate ceiling pin. * docs(diff): Consolidate pPr merge (B1) — flip the ceiling in ir_diff_engine/CLAUDE/CHANGELOG (table-shell+section = B2) * fix(diff): Consolidate pPr merge — compose by full BlockSignature, not pPr digest (no silent run-format drop) Adversarial review found a silent-drop regression: IsParagraphPPrOnlyEdit checked that pPr CHANGED, not that the edit was pPr-ONLY, so a FormatOnly op carrying BOTH a pPr change and a run/mark-rPr change passed. When two reviewers agreed on pPr but disagreed on run format, ComposePPr saw matching pPr digests, declared consensus, and dropped the second reviewer's run edit with no conflict. Now IsParagraphFormatEdit + ComposeParagraphFormat compose by the FULL boundary-normalized BlockSignature (run formats + pPr), so any format disagreement records a conflict — a competitor is never silently dropped. Also links the conflict id onto the winning op.
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.
What & why
Sub-project B1 of the block-format-change family:
DocxDiff.Consolidatenow merges reviewers' paragraph-format (w:pPr+ run/mark) changes. Previously the N-way merge forced block-format tracking off, so a reviewer's formatting-only edit was silently ignored. This is the last of the five deferred follow-ups (four shipped in #253).Branches off #253 (the two-way follow-up A).
How
TrackBlockFormatChangesis split intoTrackParagraphFormatChanges(default equal, so every two-way path is byte-identical — the full 2600-test suite confirms zero regression). The composite turns the paragraph slice ON while keeping the table/section slices OFF (the B2 ceiling), so only pPr merges here.IrParagraph.PPrDigest(a flattened, pPrChange-comparable paragraph-shell digest) is the attribution substrate, exactly asIrCell.ShellDigestis for a cell shell.ComposeParagraphFormatinIrCompositeMerger.MergeOneBaseBlock(mirroringComposeCellShell): a multi-reviewer paragraph FormatOnly edit is composed by its full boundary-normalizedBlockSignature(run formats + pPr) — 0 changers → base, all agree → consensus (first reviewer), ≥2 distinct → a recordedDocxDiffConflictresolved by policy. A paragraph format cannot stack (one paragraph has one pPr/run set), but a competitor is never silently dropped — every disagreeing reviewer is listed in the conflict.w:pPrChange(andw:rPrChangefor run edits) authored to the winning reviewer via the existing single-source render path; reject ≡ base and accept ≡ the policy-winner hold at the property-byte level.Verification
CompositeFuzzTests(3/4/5-way × 50 seeds, reject ≡ base + apply-verifier),ConsolidateParityScoreboardTests(84),IrCompositeMergerTests,IrCompositeJsonTests, the Ops/API consolidate suites.code-reviewerpass over the merger commits found one critical silent-drop bug (composing on the pPr digest alone dropped a competitor's run-format edit when reviewers agreed on pPr) — fixed in this branch (compose by the fullBlockSignature) with the regression test above, plus its minor notes (conflict-id now linked on the winning op).Follow-up (out of scope — sub-project B2)
Table-shell (
w:tcPr/w:trPr/w:tblPr) and section (w:sectPr) composite merge; text+pPr combined edits still route to the conflict path (only pPr-/format-only edits compose in B1). All pinned + documented.