Skip to content

feat(diff): block-format family follow-ups A — tblPrExChange, mid-doc sectPrChange, public opt-out, note/hdr-ftr pPrChange#253

Merged
JSv4 merged 9 commits into
mainfrom
feat/diff-block-format-followups-a
Jul 4, 2026
Merged

feat(diff): block-format family follow-ups A — tblPrExChange, mid-doc sectPrChange, public opt-out, note/hdr-ftr pPrChange#253
JSv4 merged 9 commits into
mainfrom
feat/diff-block-format-followups-a

Conversation

@JSv4

@JSv4 JSv4 commented Jul 4, 2026

Copy link
Copy Markdown
Owner

What & why

Follow-up A to the block-format-change family (#252): finishes the two-way surface. Four of the five deferred items land here; the fifth (Consolidate block-format merge) is a separate, larger PR (sub-project B).

Grounded reading reshaped the scope: two items turned out cheaper or better-declined than expected.

Item Outcome
w:tblPrExChange (row-level table property exceptions) Now tracked (was visible-but-untracked)
Mid-document w:sectPrChange (inline in-pPr sectPr) Now tracked (was invisible — properties weren't modeled)
Note / header-footer w:pPrChange Already worked — proven by tests + doc correction (the "ceiling" was over-conservative)
Split/merge w:pPrChange Deliberately declined — semantically ill-defined
TrackBlockFormatChanges public opt-out Surfaced on DocxDiffSettings (+ npm/python wire)

How

  • A1 — public opt-out. Mirrors the CompareHeadersFooters ripple exactly: DocxDiffSettings.TrackBlockFormatChanges (default true) → ToIrDiffSettingsDocxDiffOps JSON key trackBlockFormatChanges → npm/python types. WASM/index/dispatcher/session are passthrough. Consolidate still forces it off internally regardless of the value.
  • A2 — w:tblPrExChange. New IrRow.TrPrExDigest (a flattened tblPrEx-only projection, parallel to TrPrShellDigest), a TblPrExInnerExclude, one ApplyShellChange call, and a TableRow-scope revision with the distinct changed-name "tblPrEx". RevisionProcessor already accepted/rejected it. Round-trips (accept ≡ right, reject ≡ left).
  • A3 — mid-document w:sectPrChange. The reader models the inline sectPr as IrParagraph.InlineSectionFormat (via a MapSectionFormat refactor shared with BuildSectionBreak), folded into the paragraph FormatFingerprint and IrModeledFormat.BlockSignature (a new SectionKey) so a sectPr-only change classifies FormatOnly under ModeledOnly instead of Unchanged. The emit stamps w:sectPrChange inside the paragraph's own w:pPr/w:sectPr (not the pPrChange inner — CT_PPrBase excludes sectPr) via a refactored explicit-old-source ApplySectPrChange that snapshots both old and right props before mutating (the output element aliases one or the other). A per-paragraph Section revision. One-sided add/remove is structural (untracked, documented).
  • A4 — note/header-footer pPrChange + split/merge decline. No new code: ApplyBlockFormatChanges is gated only by the shared state.Settings.TrackBlockFormatChanges (no per-scope gate), and note/story ops route through the same RenderBlockOp dispatch — so a changed footnote/header/footer paragraph already emits w:pPrChange. Pinned by tests. Split/merge is declined (D1): a split's members are brand-new paragraphs already tracked by the inserted pilcrow mark, a merge's non-final members are deleted — a pPr "change" isn't well-defined and would fight the reject-fuse.

Verification

  • Full .NET suite green: 2596 passed / 0 failed / 3 skipped. Release build clean (warnings-as-errors); WASM-mode core clean; npm tsc clean; python to_wire verified.
  • Ratchets hold: IrParityScoreboardTests (179), IrMarkupParityScoreboardTests (39), ConsolidateParityScoreboardTests (84), IrVsWmlComparerTests. New revisions are Fine-only non-Run scopes (excluded from compatible mode by construction → parity counts unaffected).
  • New: BlockFormatChangeTests (tblPrEx, mid-doc sectPr, note/header pPrChange, split decline, both-fire schema order, fresh-tblPrEx order), BlockFormatChangeRealDocTests (a real corpus doc mutated across the full table + section family — all markers present, schema-valid, round-tripping), the strengthened IrMarkupRendererTests round-trip battery (tprex:/psec: signatures), and additive wire tests (.NET/npm/python).
  • An adversarial code-reviewer pass returned SOUND — no correctness bugs; its three LOW-severity notes (a stale comment, two coverage gaps, one blind-spot doc line) are all addressed in the final commit.

Follow-up (out of scope)

Sub-project B — Consolidate block-format merge: lift the composite ceiling and merge reviewers' pPr/table-shell/section edits with per-reviewer attribution + a competing-property conflict class, strengthening the byte-level round-trip verifiers. Its own spec/plan/PR.

JSv4 added 8 commits July 3, 2026 20:54
…onFormat, folded into the paragraph fingerprint
… 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).
…d dispatch) + the split/merge pPrChange decline (D1)
…p the closed v1 ceilings in ir_diff_engine/CLAUDE/CHANGELOG
…ema-order test, fresh-tblPrEx-order test, corrected harness comment, ModeledOnly inline-sectPr blind-spot doc
…s 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.
@JSv4 JSv4 merged commit 6796769 into main Jul 4, 2026
12 checks passed
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