Skip to content

chore(verification): scaffold OpenSpec change for Tier 2 OoxmlDoc model + inv_field_001 closure#202

Merged
stevenobiajulu merged 2 commits into
mainfrom
201-tier2-ooxml-doc-subset-20260512
May 14, 2026
Merged

chore(verification): scaffold OpenSpec change for Tier 2 OoxmlDoc model + inv_field_001 closure#202
stevenobiajulu merged 2 commits into
mainfrom
201-tier2-ooxml-doc-subset-20260512

Conversation

@stevenobiajulu
Copy link
Copy Markdown
Member

Summary

Authors the OpenSpec change add-ooxml-doc-subset-and-inv-field-001-proof to kick off Tier 2 of the Lean 4 verification roadmap defined in verification/ROADMAP.md. This PR is the proposal only — it does NOT implement Tier 2. Implementation lands in successor PRs (one per tasks.md item, each via /codex-implement against its own GitHub issue).

Tracking issue: #201.
Prerequisite: #164 (Tier 1 spike, merged 2026-05-11).

What's in this PR

  • openspec/changes/add-ooxml-doc-subset-and-inv-field-001-proof/proposal.md — the change overview.
  • openspec/changes/add-ooxml-doc-subset-and-inv-field-001-proof/design.md — model boundary, predicate strengthening, axiom rewiring, proof sketches, stop conditions.
  • openspec/changes/add-ooxml-doc-subset-and-inv-field-001-proof/tasks.md — eight tasks (T1..T6), each sized for one /codex-implement run.
  • openspec/changes/add-ooxml-doc-subset-and-inv-field-001-proof/specs/docx-comparison/spec.md## ADDED Requirements delta with five scenarios.
  • verification/ROADMAP.md:3 — flip stale "draft PR chore(verification): atom-level Lean 4 spike with proofs + bridge + CI #164" → "merged PR chore(verification): atom-level Lean 4 spike with proofs + bridge + CI #164" and note this proposal in progress.

openspec validate add-ooxml-doc-subset-and-inv-field-001-proof --strict passes.

Key design decisions

  • Model boundary: small tree-structured syntactic OOXML subset (paragraphs → blocks with Ins/Del/MoveFrom/MoveTo wrappers → runs → atoms including DelInstrText). Mirrors trackChangesAcceptorAst.ts:368-659 structurally; narrower than full ComparisonUnitAtom (intentional and documented).
  • Closure strategy: introduce a recursive well-formedness predicate recursivelyWellformed (whole-doc validateFieldStructure plus every wrapper subtree fieldSelfContained) and prove a preservation lemma over the Lean model. Compose it with a single new named axiom compareDocumentXml_output_recursivelyWellformed to close the Spec.lean:71 sorry. The new axiom is the load-bearing residual obligation Tier 3 will discharge.
  • fieldSelfContained strengthening (round-2 peer review fix): the counterexample Begin, Separate, Ins(Del(End, Begin), InstrText), End showed that per-subtree validateFieldStructure is too weak, so wrapper subtrees must enter and exit the depth/seenSeparator walk at (0, false).

Out of scope (separate future changes)

  • inv_rt_001 closure (Spec.lean:95 sorry remains untouched)
  • Discharge of compareDocumentXml_output_recursivelyWellformed (Tier 3, by modeling compareDocumentXml definitionally)
  • Lean↔TS extensional equivalence for accept/reject (Tier 2.5)
  • Full field-bearing fast-check arbitrary (only one fixture-based bridge case lands here, per codex round-1 review)

Peer review

Two rounds of gemini + codex CLI peer review have been incorporated:

  • Round 1: codex flagged that the original "close the sorry directly via a preservation lemma" framing was incorrect because Spec.lean:66 has no validateFieldStructure combined precondition. Fix: introduce the named axiom compareDocumentXml_output_recursivelyWellformed and compose. Also: original tape-shaped model retracted in favor of a tree.
  • Round 2: codex caught the specific counterexample noted above. Fix: strengthen the precondition from per-subtree validateFieldStructure to fieldSelfContained.

Further iteration on the model boundary, predicate shape, and named-axiom phrasing is welcomed in PR comments before any implementation work begins.

Test plan

  • openspec validate add-ooxml-doc-subset-and-inv-field-001-proof --strict passes locally
  • Codex/Gemini reviewers verify the fieldSelfContained strengthening actually closes the counterexample family
  • Once approved, file individual issues for T1..T6 and run /codex-implement per task

…el + inv_field_001 closure

Authors the OpenSpec change `add-ooxml-doc-subset-and-inv-field-001-proof`
to kick off Tier 2 of the Lean 4 verification roadmap. Two rounds of
gemini + codex CLI peer review have been incorporated. The proposal does
NOT implement Tier 2; it scopes the work and decomposes it into eight
single-`/codex-implement`-run-sized tasks (T1, T2, T3a, T3b, T4a, T4b,
T5a, T5b/T6) that successor PRs against this change will land.

Key decisions captured:

- Model boundary: small tree-structured syntactic OOXML subset (paragraphs
  → blocks with `Ins`/`Del`/`MoveFrom`/`MoveTo` wrappers → runs → atoms
  including `DelInstrText`). Mirrors `trackChangesAcceptorAst.ts:368-659`
  structurally; deliberately narrower than full `ComparisonUnitAtom`.
- Closure strategy: introduce a recursive well-formedness predicate
  `recursivelyWellformed` (whole-doc `validateFieldStructure` plus every
  wrapper subtree `fieldSelfContained`) and prove a preservation lemma
  over the Lean model. The `Spec.lean:71` sorry closes by composing the
  lemma with a single new named axiom
  `compareDocumentXml_output_recursivelyWellformed`, which becomes the
  load-bearing residual obligation Tier 3 will discharge.
- `fieldSelfContained` strengthening (round-2 peer review fix): the
  counterexample `Begin, Separate, Ins(Del(End, Begin), InstrText), End`
  showed that per-subtree `validateFieldStructure` is too weak, so
  wrapper subtrees must enter and exit the depth/seenSeparator walk at
  `(0, false)`.
- `inv_rt_001` (Spec.lean:95 sorry), Tier 2.5 (Lean↔TS equivalence,
  broader projection), and Tier 3 (definitional `compareDocumentXml`)
  are explicitly out of scope for this change.

Also updates `verification/ROADMAP.md` line 3 to reflect PR #164 merged
and this change in progress (the line was stale, calling PR #164 a draft
when it merged 2026-05-11).

`openspec validate add-ooxml-doc-subset-and-inv-field-001-proof --strict`
passes. The proposal is opened as a draft PR so the model-boundary and
predicate-strengthening decisions can take further peer-review iteration
in PR comments before any implementation work begins.

Ref: #201 (tracking issue), #164 (Tier 1 spike).
@vercel
Copy link
Copy Markdown

vercel Bot commented May 12, 2026

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

Project Deployment Actions Updated (UTC)
site Ready Ready Preview, Comment May 14, 2026 9:49pm

Request Review

@github-actions github-actions Bot added the chore label May 12, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 12, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…eld context, T0 generic-lemma split)

Round 3 peer review (codex + gemini CLI, 2026-05-13) caught three further
counterexample families that defeat any "standalone walk from (0, false)
ends at (0, false)" predicate:

- DelInstrText is unchecked: Del(DelInstrText) breaks reject after rewrite
- subtree consumes outer field state via leading End/Separate:
  Begin, Ins(End) breaks reject; Begin, Del(End) breaks accept
- single-boolean seenSeparator reset:
  [Begin, Separate, Del(Begin, End), InstrText, End] slips through

The fix tracks seenSeparator as a depth-indexed stack (FieldCtx := List Bool),
exactly mirroring the TS engine's pastSeparatorAtDepth at pipeline.ts:374-389,
and strengthens the per-subtree predicate to fieldContextNeutral: the subtree
is invariant under any outer field-stack context.

Other revisions in this commit:

- Pull shared walk-over-append + context-extension + DelInstrText rewrite
  lemmas out into a new T0 task (Tier2/WalkLemmas.lean) so accept and
  reject halves consume identical generic results.
- Fix DelInstrText locality claim: rewrite is global at the operation
  level (after both Del and MoveFrom unwraps complete, per TS lines
  602-616); locality is enforced by recursivelyWellformed on the input.
- Tone down empirical wording in the new axiom's docstring; scope it to
  this repo's inplace atomizer rather than to OOXML comparison engines
  in general.
- Add scenario [LEAN-T2-04](f) covering the DelInstrText locality and
  reject ordering documentation.
@stevenobiajulu
Copy link
Copy Markdown
Member Author

Round 3 peer review incorporated (commit 9beb692)

Ran /peer-review with codex + gemini CLI on 2026-05-13. Both reviewers independently caught three further counterexample families that defeated the round-2 fieldSelfContained predicate:

  • Family A (DelInstrText unchecked): Del(DelInstrText) validates because the standalone walk sees no InstrText, but reject rewrites and unwraps, leaving a bare InstrText outside any field.
  • Family B (subtree consumes outer state via leading End/Separate): Begin, Ins(End) makes reject invalid; Begin, Del(End) makes accept invalid.
  • Family C (single-boolean seenSeparator reset): [Begin, Separate, Del(Begin, End), InstrText, End] slips through whole-doc validation if seenSeparator is a single mutable boolean rather than a depth-indexed stack.

Both reviewers landed independently on the same root cause: the per-subtree predicate must operate over a stack-valued field context, exactly mirroring what pipeline.ts:374-389 already does (pastSeparatorAtDepth: number[]), and must be context-neutral — invariant under any outer starting context — not just self-balanced from (0, false).

Changes in this commit

  1. Renamed predicate: fieldSelfContainedfieldContextNeutral, defined as ∀ ctx, walkBlocks (.ok ctx) bs = .ok ctx. Stack model spelled out at the type level (FieldCtx := List Bool).
  2. Round-3 counterexamples enumerated in design.md > Context so a future reader understands what the predicate has to rule out.
  3. New T0 task (Tier2/WalkLemmas.lean): pulls the three load-bearing generic lemmas (walkBlocks_append, context-extension corollary, DelInstrText → InstrText rewrite-safety) out of T4a/T5a so both halves consume identical results rather than rediscovering them.
  4. DelInstrText locality fix: reject rewrites globally after BOTH Del AND MoveFrom unwraps complete (per trackChangesAcceptorAst.ts:602-616); locality of DelInstrText to deleted-content wrappers is enforced by recursivelyWellformed on the input, not by the bare datatype. Spec scenario [LEAN-T2-04](f) and [LEAN-T2-02] updated.
  5. Empirical wording tightened in the new axiom's docstring: scoped to this repo's inplace atomizer, not to OOXML comparison engines in general; explicit that the existing fast-check bridge cases at lean-spec-bridge.test.ts:42-44 exclude field-bearing inputs and only check the consequence (not the recursive precondition).
  6. Status note added: after this strengthening, no obvious round-3 counterexample remains; suggestive, not a proof.

Validation

  • openspec validate add-ooxml-doc-subset-and-inv-field-001-proof --strict passes.
  • Reviewer prompt + raw transcripts retained at /tmp/peer-review-1778788080-77229.md, /tmp/.../tasks/beqmnxl2c.output, /tmp/.../tasks/bqrkzi0s3.output.
  • Files changed: proposal.md +21/-9, design.md +66/-34, tasks.md +18/-8, specs/docx-comparison/spec.md +6/-3.

Round 4 review is welcomed if a third-party Lean-fluent reviewer wants to attack the fieldContextNeutral formulation directly. The predicate has now been strengthened twice in successive rounds; if a fourth strengthening lands, the model boundary itself (not the predicate) probably needs to change.

@stevenobiajulu stevenobiajulu marked this pull request as ready for review May 14, 2026 21:57
@stevenobiajulu stevenobiajulu enabled auto-merge (squash) May 14, 2026 21:58
@stevenobiajulu stevenobiajulu merged commit 0597db0 into main May 14, 2026
36 of 38 checks passed
@stevenobiajulu
Copy link
Copy Markdown
Member Author

✅ Post-merge smoke passed

Merged: 0597db0d00c7b7880909af8dec3b98de621002ef
Built from: main @ 0597db0 (fast-forwarded from e37a4ea)
Smoke: openspec proposal validation (this PR is documentation-only — no production-engine code change)

Steps

  • openspec validate add-ooxml-doc-subset-and-inv-field-001-proof --strict
  • ✅ change appears in openspec list (0/10 tasks tracked)
  • ✅ all 4 proposal files in place (proposal.md 63L, design.md 228L, tasks.md 46L, specs/docx-comparison/spec.md 39L)
  • ✅ spec delta parseable

Why no npm test step: the workspace test suite hits a known-flaky fast-check property in lean-spec-bridge.test.ts (the same INV-FIELD-001 counterexample search that flagged on this PR's CI run with seed -663637051). That test is unrelated to this docs-only change; rerunning it locally would only reproduce the same flake. workspace-test (20) was also already broken on main before this merge — separate issue.

CI rerun outcome: failed jobs were rerun and presumably passed (PR merged successfully via automerge).

Log: /tmp/automerge-smoke-202-1778796582.log (local).

Successor work: file individual issues for T0..T6 from tasks.md and run /codex-implement per task.

stevenobiajulu added a commit that referenced this pull request May 22, 2026
…208)

* feat(verification): close inv_field_001 with Tier 2 OoxmlDoc subset

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

* feat(verification): extend Tier2 Block with transparent .other container; 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

* docs(verification): reference issue #217 in inv_field_001 residual axiom docstring

Replaces the placeholder "see follow-up issue" with the concrete cross-reference
to the engine fragmentation conformance work tracked in #217. No behavior
change.

Ref: #208, #217

---------

Co-authored-by: Steven Obiajulu <stevenobiajulu@Stevens-MacBook-Pro.local>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant