Skip to content

feat(diff): Consolidate paragraph-format merge (B1) — pPrChange across reviewers with consensus/conflict#254

Merged
JSv4 merged 4 commits into
feat/diff-block-format-followups-afrom
feat/diff-block-format-consolidate-b
Jul 4, 2026
Merged

feat(diff): Consolidate paragraph-format merge (B1) — pPrChange across reviewers with consensus/conflict#254
JSv4 merged 4 commits into
feat/diff-block-format-followups-afrom
feat/diff-block-format-consolidate-b

Conversation

@JSv4

@JSv4 JSv4 commented Jul 4, 2026

Copy link
Copy Markdown
Owner

What & why

Sub-project B1 of the block-format-change family: DocxDiff.Consolidate now 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

  • A paragraph slice of the composite flag. TrackBlockFormatChanges is split into TrackParagraphFormatChanges (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 as IrCell.ShellDigest is for a cell shell.
  • ComposeParagraphFormat in IrCompositeMerger.MergeOneBaseBlock (mirroring ComposeCellShell): a multi-reviewer paragraph FormatOnly edit is composed by its full boundary-normalized BlockSignature (run formats + pPr) — 0 changers → base, all agree → consensus (first reviewer), ≥2 distinct → a recorded DocxDiffConflict resolved 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.
  • Native markup. The consolidated document carries w:pPrChange (and w:rPrChange for 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

  • Full .NET suite green (2602+ passed / 0 failed / 3 skipped) — the flag split causes zero engine-wide regression. Release + WASM-mode core builds clean.
  • The invariant-heavy composite battery all green with the merge: CompositeFuzzTests (3/4/5-way × 50 seeds, reject ≡ base + apply-verifier), ConsolidateParityScoreboardTests (84), IrCompositeMergerTests, IrCompositeJsonTests, the Ops/API consolidate suites.
  • New tests: single-reviewer merge, multi-reviewer consensus, multi-reviewer conflict per policy, a multi-reviewer byte-level round-trip stress test (reject ≡ base pPr for every paragraph, accept ≡ policy-winner, under BaseWins and FirstReviewerWins), and the adversarial-review regression (two reviewers agree on pPr but disagree on run format → a recorded conflict, not a silent drop).
  • An adversarial code-reviewer pass 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 full BlockSignature) 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.


Depends on #253 (sub-project A). This branch is stacked on feat/diff-block-format-followups-a; retarget to main once #253 merges.

JSv4 added 4 commits July 3, 2026 21:42
…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 JSv4 merged commit 548cb1c into feat/diff-block-format-followups-a Jul 4, 2026
1 check passed
@JSv4 JSv4 deleted the feat/diff-block-format-consolidate-b branch July 4, 2026 03:41
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant