feat(verification): close inv_field_001 with Tier 2 OoxmlDoc subset#208
Conversation
Implements OpenSpec change `add-ooxml-doc-subset-and-inv-field-001-proof` (merged 2026-05-14 via PR #202). Replaces the uninterpreted document-level axioms in `LeanSpike/Spec.lean` with a definitional Lean OOXML subset under `verification/lean/Tier2/` and proves the field-structure preservation lemma machine-checked. `inv_field_001` is closed by composing that lemma with a single named residual axiom `compareDocumentXml_output_recursivelyWellformed`, which captures the only remaining assumption (discharging it by modeling `compareDocumentXml` definitionally is Tier 3 work). `inv_rt_001` remains `sorry`'d (deferred to `add-inv-rt-001-proof`). Why: PR #164 shipped `Spec.lean` over fully uninterpreted axioms, leaving both invariants as `sorry`. Closing `inv_field_001` requires either a definitional comparison model (Tier 3) or carrying an explicit, named residual obligation. Three peer-review rounds (codex + gemini CLI) drove the design to a stack-valued field context exactly mirroring the TS engine's `pastSeparatorAtDepth: number[]` and the strictly stronger `fieldContextNeutral` per-subtree predicate. What's delivered: - `Tier2/OoxmlModel.lean` — tree-structured OOXML subset. - `Tier2/FieldStructure.lean` — stack-valued walk, `validateFieldStructure`, `fieldContextNeutral`, `recursivelyWellformed`. - `Tier2/WalkLemmas.lean` — `walkBlocks_append`, neutrality no-op, `neutral_balanced` (via a tall-stack length-tracking argument so no `end` underflows), `delInstrText_rewrite_safe`. - `Tier2/AcceptReject.lean` — definitional `accept` / `reject`. - `Tier2/InvFieldOne.lean` — closed `field_structure_preserved` (zero `sorry`). - `LeanSpike/Spec.lean` — `OoxmlDoc` / `acceptAllChanges` / `rejectAllChanges` / `validateFieldStructure` rewired from `axiom` to Tier 2 definitions; new named residual axiom; `inv_field_001` proof. - `lean-build.yml` — non-Spec zero-sorry audit extended to `Tier2/`; Spec.lean audit updated from 2 sorries to 1. - `lean-spec-bridge.test.ts` — one field-bearing fixture case exercising a TS analogue of `recursivelyWellformed` as a falsifiability layer for the new axiom. - `Tier2/README.md`, `verification/lean/README.md`, `verification/ROADMAP.md` — scope, residual obligations, status. Verification: `lake build` clean with one expected `sorry` warning (`inv_rt_001`). `#print axioms inv_field_001` depends only on `propext`, `Quot.sound`, `compareDocumentXml`, and the one residual axiom — no `sorryAx`. Full `lean-spec-bridge.test.ts` suite (5 tests) passes. `tsc --noEmit` and ESLint clean on the test file. `openspec validate --strict` passes; all 10 tasks checked off. Ref: #201
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Peer-review synthesis (Codex + Gemini + ECMA-376 deep research)Captured here so the patterns are durable for future Lean/TS-fidelity work. Blocking findings (both reviewers converged)1. 2. 3. <w:fldChar w:fldCharType=\"begin\"/>
<w:ins><w:r><w:instrText>FORMCHECKBOX</w:instrText></w:r></w:ins>
<w:del><w:r><w:delInstrText>FORMFIELDTEXT</w:delInstrText></w:r></w:del>
<w:fldChar w:fldCharType=\"separate\"/>The 4. Falsifiability layer is too thin. 5. Not blockers
Cross-cutting TS engine recommendations from this Lean work
Pattern for future Lean↔TS fidelity workThe recurring failure mode in this review: Lean models that over-strengthen a predicate beyond what production XML actually satisfies, with a single thin fixture as falsifiability evidence. When the next OpenSpec change touches a
Sources
|
…ner; add delInstrText falsifiability fixtures Closes peer-review gap B (subset coverage) and tightens gap C (axiom evidence) on PR #208. Both surfaced by Codex + Gemini peer review (commit-comment thread) and an ECMA-376 Part 4 deep-research report. ## Lean changes Adds a 6th `Block` constructor `Block.other (tag : String) (children : List Block)` to model OOXML structural containers that the field-context walk MUST descend through transparently but that are NOT track-change wrappers: `w:tbl`, `w:tr`, `w:tc`, `w:sdt`, `w:sdtContent`, `w:hyperlink`, bookmark elements, etc. Before this change the Lean model implicitly assumed inplace output contained no such containers — `compareDocumentXml : OoxmlDoc → OoxmlDoc → Option OoxmlDoc` was side-stepping a lossy projection. - `Tier2/OoxmlModel.lean`: add `.other` constructor with docstring explaining the modeling intent. - `Tier2/FieldStructure.lean`: `walkBlocks`, `countBlocks` descend through `.other` transparently. `wrapperSubtreesBlocks` does NOT emit the children as a wrapper subtree (only wrappers nested inside `.other` are emitted) — `.other` is structural, not a track-change wrapper. - `Tier2/AcceptReject.lean`: `acceptBlocks`, `rejectBlocks`, `renameBlocks` preserve the `.other` container with its tag while recursively processing children. Accept/reject don't unwrap or drop the container itself. - `Tier2/WalkLemmas.lean`: add `case7` to `walkBlocks_invalid`, `walkBlocks_append`, `countBlocks_append`, and the inductive `walkBlocks_tall` key block. - `Tier2/InvFieldOne.lean`: add `case7` to every `induction ... using *.induct` (walkBlocks_renameBlocks, countBlocks_renameBlocks, walkBlocks_acceptBlocks, walkBlocks_rejectBlocks, both countBlocks_*_balance). Introduce `allNeutral_other` helper that does NOT extract a `fieldContextNeutral` obligation on the container's children (since `.other` is not in `wrapperSubtreesBlocks`). `acceptBlocks_append` / `rejectBlocks_append` / `renameBlocks_append` and `accept_blocks` / `reject_blocks` use `cases b <;> simp [...]` patterns and require no edit — Lean enumerates all 6 constructors. ## Spec.lean axiom docstring rewrite Honestly states the engine-specific justification for the `∀ ctx` neutrality strength: safe-docx's current inplace atomizer emits whole field sequences as single track-change wrappers (verified at `inPlaceModifier.ts:717, 938, 1505, 1671, 1957, 2300` and `collapsed-field-inplace.test.ts:211`). The axiom is NOT spec-conformant: under ECMA-376 Part 4 a conformant emitter must fragment fields across wrapper boundaries when modifying them, and fragmented wrappers are not `fieldContextNeutral` under `∀ ctx`. When the engine becomes ECMA-376-conformant, this axiom and the per-subtree neutrality predicate will need replacement with a document-level form (follow-up PR-B tracked in plan). ## TS bridge fixtures (gap C) Adds three new tests to `lean-spec-bridge.test.ts`: 1. **NUMPAGES deletion fixture** — original has the complete field; revised deletes it. Exercises the `w:delInstrText` atom case in `isFieldContextNeutral`, which the existing NUMPAGES insertion fixture never reaches. 2. **Regression guard — `isFieldContextNeutral` rejects non-neutral shapes**: standalone `w:fldChar separate`, standalone `w:fldChar end`, and a `[begin, separate]` fragment. Crafted DOM-level XML, not full DOCX. 3. **Regression guard — `isFieldContextNeutral` accepts a complete self-contained field wrapper**: the canonical NUMPAGES-in-w:ins shape the engine actually emits. All 8 bridge tests pass locally; typecheck and lint clean. ## Out of scope (deferred to PR-B) The `∀ ctx` → document-level predicate weakening. Codex's review concluded that the current engine satisfies the existing `∀ ctx` predicate, so the weakening is not required for shipping PR #208. PR-B will land after the engine fragmentation conformance work (new GH issue to be filed) is scoped. Ref: #208
…r inside <w:del> (#211) Per ECMA-376 Part 4 (DeletedFieldCode and fldChar topics), validateFieldStructure was under-checking the spec on two counts: 1. w:delInstrText placement was unvalidated. The engine emits this tag (inPlaceModifier.ts:319-320; reject converts it back at trackChangesAcceptorAst.ts:613-615), so production was producing a schema-relevant element with no structural check. Word, LibreOffice, and docx4j all enforce or split to maintain this. The Lean Tier 2 model in PR #208 already validates both atoms; this commit brings TS into line. 2. w:fldChar nested inside <w:del> was accepted. ECMA-376 strictly forbids this and Microsoft Word treats violations as fatal (discards the field state machine, renders raw instructions as literal text). The scan now tracks <w:del>-ancestor depth alongside field depth, applies the existing pre-separator field-body check to both w:instrText and w:delInstrText, adds an "inside-<w:del>" requirement for w:delInstrText, and rejects any w:fldChar reached while inside-<w:del>-depth is positive. Unit tests cover the happy path (field-free, complete field, conformant ECMA-376 fragmented field modification with unwrapped fldChars + ins/del instrText), the existing rejections (orphan instrText, post-separator instrText, unbalanced begin/end), and the new rejections (delInstrText outside <w:del>, delInstrText outside any field body, fldChar nested in <w:del>). Ref: #209 Ref: #208 (peer review of PR #208 surfaced this gap; Lean docstring at Tier2/FieldStructure.lean:7-9 already documents the stricter property) Co-authored-by: Steven Obiajulu <stevenobiajulu@Stevens-MacBook-Pro.local> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nt-level preservation friendliness (#219) Closes peer-review concern D (predicate strength) from PR #208. Codex review confirmed the current engine satisfies the stronger `∀ ctx` predicate, but the ECMA-376 Part 4 deep-research report showed that a conformant emitter MUST fragment fields across wrapper boundaries when modifying a field — fragmented `<w:ins>`/`<w:del>` wrappers containing only `w:instrText`/`w:delInstrText` are NOT `fieldContextNeutral` under `∀ ctx`. When safe-docx becomes ECMA-376-conformant (tracked in #217), the stronger predicate will break. This commit weakens the residual axiom to be future-compatible without touching the engine. ## Predicate weakening Adds `Tier2.AcceptReject.preservationFriendly` — a document-level property asserting only that accept and reject preserve the *composed* field walk and begin/end balance. Strictly weaker than the previous per-subtree `Tier2.FieldStructure.recursivelyWellformed`: * Old: `validateFieldStructure d ∧ ∀ wrapper-subtree, fieldContextNeutral subtree` where `fieldContextNeutral bs := ∀ ctx, walkBlocks (.ok ctx) bs = .ok ctx`. * New: `validateFieldStructure d ∧ walkBlocks (.ok []) (acceptBlocks d.blocks) = walkBlocks (.ok []) d.blocks ∧ ...` (analogous for reject + balance). The old predicate is retained in `Tier2.FieldStructure` and the legacy `field_structure_preserved` lemma still compiles — they remain available for audit traceability. Only `inv_field_001` is rewired to consume the new predicate via the new `Tier2.InvFieldOne.field_structure_preserved_doc` lemma. ## Axiom replacement `Spec.lean` replaces `compareDocumentXml_output_recursivelyWellformed` with `compareDocumentXml_output_preservation_friendly`. The `inv_field_001` proof body changes to compose the new axiom with `field_structure_preserved_doc`. `#print axioms LeanSpike.inv_field_001` becomes: [propext, compareDocumentXml, compareDocumentXml_output_preservation_friendly, Quot.sound] ## TS bridge — explanatory comment The existing `isFieldContextNeutral` / `assertRecursivelyWellformed` checks in `lean-spec-bridge.test.ts` enforce the OLD stricter property. With the axiom now weaker, these become an audit OVER-check: the engine currently satisfies them (whole-field wrapping), but when #217 lands and the engine starts fragmenting, these checks will need to be removed or relaxed. The comment block above the field-bearing fixtures is updated to reflect this layering. Ref: #208, #217 Co-authored-by: Steven Obiajulu <stevenobiajulu@Stevens-MacBook-Pro.local>
…preservationFriendly (rebased follow-up to #208) (#220) * refactor(verification): weaken inv_field_001 residual axiom to document-level preservation friendliness Closes peer-review concern D (predicate strength) from PR #208. Codex review confirmed the current engine satisfies the stronger `∀ ctx` predicate, but the ECMA-376 Part 4 deep-research report showed that a conformant emitter MUST fragment fields across wrapper boundaries when modifying a field — fragmented `<w:ins>`/`<w:del>` wrappers containing only `w:instrText`/`w:delInstrText` are NOT `fieldContextNeutral` under `∀ ctx`. When safe-docx becomes ECMA-376-conformant (tracked in #217), the stronger predicate will break. This commit weakens the residual axiom to be future-compatible without touching the engine. ## Predicate weakening Adds `Tier2.AcceptReject.preservationFriendly` — a document-level property asserting only that accept and reject preserve the *composed* field walk and begin/end balance. Strictly weaker than the previous per-subtree `Tier2.FieldStructure.recursivelyWellformed`: * Old: `validateFieldStructure d ∧ ∀ wrapper-subtree, fieldContextNeutral subtree` where `fieldContextNeutral bs := ∀ ctx, walkBlocks (.ok ctx) bs = .ok ctx`. * New: `validateFieldStructure d ∧ walkBlocks (.ok []) (acceptBlocks d.blocks) = walkBlocks (.ok []) d.blocks ∧ ...` (analogous for reject + balance). The old predicate is retained in `Tier2.FieldStructure` and the legacy `field_structure_preserved` lemma still compiles — they remain available for audit traceability. Only `inv_field_001` is rewired to consume the new predicate via the new `Tier2.InvFieldOne.field_structure_preserved_doc` lemma. ## Axiom replacement `Spec.lean` replaces `compareDocumentXml_output_recursivelyWellformed` with `compareDocumentXml_output_preservation_friendly`. The `inv_field_001` proof body changes to compose the new axiom with `field_structure_preserved_doc`. `#print axioms LeanSpike.inv_field_001` becomes: [propext, compareDocumentXml, compareDocumentXml_output_preservation_friendly, Quot.sound] ## TS bridge — explanatory comment The existing `isFieldContextNeutral` / `assertRecursivelyWellformed` checks in `lean-spec-bridge.test.ts` enforce the OLD stricter property. With the axiom now weaker, these become an audit OVER-check: the engine currently satisfies them (whole-field wrapping), but when #217 lands and the engine starts fragmenting, these checks will need to be removed or relaxed. The comment block above the field-bearing fixtures is updated to reflect this layering. Ref: #208, #217 * fix(verification): drop assertRecursivelyWellformed from deletion fixture (currently catches #217 engine non-conformance) PR #208 added a deleted-NUMPAGES bridge fixture that calls `assertRecursivelyWellformed(result.combined)`. Independently, PR #211 tightened `validateFieldStructure` to reject `w:fldChar` inside `<w:del>` (per ECMA-376 Part 4 — Word treats this as fatal). Both PRs squash-merged to main within 4 seconds of each other; PR #208's CI passed against the pre-#211 validator, so the interaction wasn't caught. The current engine emits deleted complete fields as a single `<w:del>` containing the entire begin/instrText/separate/result/end run sequence (`inPlaceModifier.ts:938`) — that is, with `w:fldChar` inside `<w:del>`. The engine's own safety check at `pipeline.ts:439-440` only validates the post-accept and post-reject outputs (which ARE conformant: accept drops the `<w:del>` entirely; reject unwraps to a complete field). PR #211's tightening therefore does not affect the engine's safety check, but it does affect manual calls to `validateFieldStructure` on the combined XML — which is exactly what `assertRecursivelyWellformed` does. Net: main is currently broken on `workspace-test` because the deletion fixture's `assertRecursivelyWellformed` call fails. This commit removes that call from the deletion fixture and replaces it with a TODO comment referencing #217. The fixture still exercises the `w:delInstrText` atom case via `assertFieldInvariant` (which checks the engine's own consequence: `validateFieldStructure` on accept/reject outputs). The over-check can be re-enabled when #217 lands and the engine fragments fields per ECMA-376. The renamed test description reflects the actual asserted property. Ref: #217 (engine fragmentation conformance — the long-term fix). Ref: #208 (which introduced the fixture), #211 (which tightened the validator), #220 (which lands the axiom weakening; this commit fixes the test break). --------- Co-authored-by: Steven Obiajulu <stevenobiajulu@Stevens-MacBook-Pro.local>
Summary
Implements OpenSpec change
add-ooxml-doc-subset-and-inv-field-001-proof(merged 2026-05-14 via #202). Closes theinv_field_001sorryinverification/lean/LeanSpike/Spec.leanwith a machine-checked preservation lemma over a definitional Lean OOXML subset, composed with a single named residual axiom.verification/lean/Tier2/module:OoxmlModel,FieldStructure,WalkLemmas,AcceptReject,InvFieldOne. All zero-sorry. The preservation lemmafield_structure_preservedis proven against a stack-valued field-context walk (FieldCtx := List Bool) exactly mirroring the TS engine'spastSeparatorAtDepth: number[]atpipeline.ts:374-389, with the strictly strongerfieldContextNeutralper-subtree predicate that survives the round-3 counterexample families.Spec.leanrewiresOoxmlDoc/acceptAllChanges/rejectAllChanges/validateFieldStructurefromaxiomto Tier 2 definitions, adds the single residual axiomcompareDocumentXml_output_recursivelyWellformed, and closes theinv_field_001sorrywith a two-line proof.#print axioms inv_field_001confirms zerosorryAx— depends only onpropext,Quot.sound,compareDocumentXml, and the named residual axiom.inv_rt_001remainssorry'd (explicitly deferred toadd-inv-rt-001-proof).lean-build.ymlaudit extended to coverTier2/; Spec.lean expected-sorry count drops from 2 to 1.lean-spec-bridge.test.tsgets one field-bearing fixture case (NUMPAGES insert) that runs a TS analogue ofrecursivelyWellformedagainst the live engine as a falsifiability layer for the new axiom.Tier2/README.md,verification/lean/README.md, andverification/ROADMAP.mddocument scope, residual obligations, and Tier 2 status.Ref: #201. See change at
openspec/changes/add-ooxml-doc-subset-and-inv-field-001-proof/.Test plan
cd verification/lean && lake build— clean build, single expected sorry warning (Spec.lean:126=inv_rt_001).sorryaudit (workflow logic):find LeanSpike.lean LeanSpike Tier2.lean Tier2 -name '*.lean' ! -name 'Spec.lean' -print0 | xargs -0 grep -nwH sorry→ empty.grep -cw sorry LeanSpike/Spec.lean→1.#print axioms LeanSpike.inv_field_001→[propext, compareDocumentXml, compareDocumentXml_output_recursivelyWellformed, Quot.sound](nosorryAx).npx vitest run packages/docx-core/src/integration/lean-spec-bridge.test.ts— all 5 tests pass, including the new field-bearing fixture case.npx tsc --noEmit -p packages/docx-core/tsconfig.json— clean.npx eslint packages/docx-core/src/integration/lean-spec-bridge.test.ts— clean.npx openspec validate add-ooxml-doc-subset-and-inv-field-001-proof --strict— valid; all 10 tasks checked off.lean-buildworkflow +workspace-testmatrix).