From 4a4a766d4b52c370cfa43995fbf052b643f56153 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Tue, 28 Jul 2026 14:35:53 -0400 Subject: [PATCH] Contract G2a review fixes: the normative surfaces, and a G1 debt All four findings verified independently against the tree before fixing; all four hold, and the first is worse than filed. P1 (normative wire docs) is not a G2a omission but a G1 regression. 3b09595 touched operation_catalog.tex and text_projection.tex and no other .tex, so it shipped four falsehoods: binary_format's payload-layout and tag tables stop at kind 30 while 31 is assigned in code, and req:binfmt:kind-discriminants says each row pins the payload's byte layout; binary_format:2432 still asserts there is no CreateInstrument; core_spec:12186 still says Canvas.layout_defaults and Instrument.range reach only the non-canonical snapshot; and the catalog gained a CreateInstrument section with no version bump against its own convention. G2a now repairs all four and adds its own two surfaces, with explicit version and changelog dispositions for binary_format (0.11.0 -> 0.12.0), the catalog (0.9.0 -> 0.10.0, covering G1 retroactively), and two narrow core_spec edits. The core_spec:5114 amendment is pinned rather than left to a subagent. The K8 paragraph has a load-bearing half the ruling does not touch -- root and canvas remain structural givens no operation mints -- and only the outside-the-operation-set clause is superseded. P1 (source) adds ops/src/lib.rs re-exports and the textproj parse.rs literal HEADER fixture, whose tracking test is a tripwire that fires by design. P2: s8 and s9 gain the mutations they lacked, and s2's was fabricated -- set_metadata contains no comparison to reverse, it records and overwrites unconditionally, so the mutation is now setter-local first-write-wins. s7's framing was backwards: WorkingSnapshot is transaction rollback, so omitting a chain retains a failed transaction's write rather than losing history, and the wrong framing would have sent the test hunting the wrong symptom. Records the G2b holdout: ScoreTuningContext's Codec deliberately drops accidental_extensions while OperationSet::accept stores envelopes as objects, so a naive full-value SetTuningContext diverges between a live session and the same document reloaded -- and canonical_value! cannot see it, because its check compares bytes and never the originating value. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV --- spec/CONTRACT_GENESIS_G2A_SETTINGS.md | 121 ++++++++++++++++++++++---- spec/PLAN_GENESIS_OPS.md | 21 +++++ 2 files changed, 125 insertions(+), 17 deletions(-) diff --git a/spec/CONTRACT_GENESIS_G2A_SETTINGS.md b/spec/CONTRACT_GENESIS_G2A_SETTINGS.md index 6c1d8db..3d946cd 100644 --- a/spec/CONTRACT_GENESIS_G2A_SETTINGS.md +++ b/spec/CONTRACT_GENESIS_G2A_SETTINGS.md @@ -120,6 +120,68 @@ containable to core + ops: hardcoded kind *count*, currently `32`, becomes `34`. Its own comment explains why it stays a literal; do not "fix" it into a derivation. +## Normative documentation — and the G1 debt this packet repairs + +**The G1 contract said "no `binary_format.tex`". That was wrong, and G1 landed +leaving four falsehoods in normative documents.** Verified against 3b09595, +which touched `operation_catalog.tex` and `text_projection.tex` and no other +`.tex`: + +* `binary_format.tex:1443` — the payload-layout table stops at kind **30**. + `req:binfmt:kind-discriminants` (`:1310`) says the table is golden-locked and + that *"each row also pins the payload's byte layout"* (`:1315`). Kind 31 is + assigned in code and pinned nowhere. +* `binary_format.tex:1516` — the `OperationKindTag` table likewise stops at 30. +* `binary_format.tex:2432` — still asserts *"there is no + `CreateCanvas`/`CreateInstrument` … instruments live in score genesis"*, and + that `Canvas.layout_defaults` and `Instrument.range` are *"confined to that + one **non-canonical** chunk"*. G1 falsified the `Instrument` half; **G2a + falsifies the `Canvas.layout_defaults` half.** +* `core_spec.tex:12186` — the same claim in the schema-major-1 narrative: + *"`Canvas.layout_defaults` and `Instrument.range` reach only the + non-canonical acceleration snapshot."* Already false; G2a makes it doubly so. +* `operation_catalog.tex` — G1 added `\section{CreateInstrument}` with **no + version bump and no changelog paragraph**, against that document's own + convention (`:301`, `:315`, `:335`). + +So the contract's earlier "no `binary_format.tex`" line is **struck**. Scope: + +**`spec/binary_format.tex` → version 0.11.0 (`:243`) becomes 0.12.0**, with a +Revision History row (`:3323`) in the established shape — the 0.2.0 row is the +exact precedent, since it appended kinds 24–27 with their payload layouts and +their matching tag discriminants. The edit adds **three** payload-layout rows +(31 `CreateInstrument` — the G1 repair — plus 32 `SetCanvasLayoutDefaults` and +33 `SetSpellingPrecedence`) and the same three tag rows. Each new payload is +`lp(T)` over the carried type; use the `SetMetadata` row as the shape. +Rewrite the `:2432` bullet: `Canvas.layout_defaults` and `Instrument` now reach +the canonical operation layer, and the "no `CreateInstrument`" clause goes. The +canvas *itself* is still not minted by any operation — say that instead, since +it remains true and is the reason the bullet existed. + +**`spec/operation_catalog.tex` → 0.9.0 (`:234`) becomes 0.10.0**, with **two** +changelog paragraphs: one retroactively recording G1's `CreateInstrument` +section as the 0.10.0 entry's first half (flag it explicitly as a G1 omission +being repaired, not as new work), and the two new `\section`s for this packet. + +**`spec/core_spec.tex`** — two edits, both narrow: + +* `:12186` — correct the sentence to name which values now reach the canonical + operation layer. Do not restructure the surrounding schema-major-1 narrative. +* `:5114` — the Pass-12 K8 doctrine paragraph, which still reads *"genesis is + the creation of an empty score together with its bundle, **outside the + operation set**"*. `spec/RULING_GENESIS_PERSISTENCE.md` reverses precisely + that clause. **Amend it narrowly and do not improvise:** the score root and + the canvas remain structural givens that no operation mints, addresses, or + deletes, and there is still no `TypedObjectId` kind for either — *that half + survives the ruling intact and is the load-bearing half*. What is superseded + is only the claim that the score's **contents** arrive outside the operation + set. Cite the ruling. Leave `:16475`'s Pass-12 history entry alone — it is a + record of what was ratified then, not a live claim — but append a + parenthetical noting the reversal. + +**If a fifth normative falsehood turns up while doing this, report it; do not +silently widen scope beyond the five above.** + ## The companion version bump Two new **kind** productions are a document-surface grammar change, so @@ -168,13 +230,17 @@ unless noted. | 16 | `ops/src/fuzz.rs` | generator arm (`:197`) | | 17 | `ops/src/valuegen.rs` | LWW generator (`:325`) | | 18 | `ops/src/vectors.rs` | a decode vector per new payload | +| 18a | `ops/src/lib.rs` | **public re-export** of both op types beside `SetMetadataOp` (`:126`) — downstream crates cannot name them otherwise | | 19 | `editor-core/src/barriers.rs` | join the score-level arm (`:468`) + module doc (`:22`) | | 20 | `layout-ir/src/barrier.rs` | literal 32 → 34 (`:1105`) | | 21 | `testkit/tests/text_projection_grammar.rs` | count 32 → 34 (`:307`) | | 22 | `textproj/src/lib.rs` | `COMPANION_VERSION` → `(0, 9, 0)` (`:29`) | | 23 | `textproj/src/vectors.rs` | flip the superseded-version negative vector | +| 23a | `textproj/src/parse.rs` | the literal `HEADER` fixture `"(text-projection (0 8 0))"` → `(0 9 0)` (`:645`). **`the_test_header_tracks_the_implemented_version` fails deliberately until this moves** — it is a tripwire, not a breakage | | 24 | `spec/text_projection.tex` | `kind` production + five version sites + changelog row | -| 25 | `spec/operation_catalog.tex` | §`SetCanvasLayoutDefaults`, §`SetSpellingPrecedence` | +| 25 | `spec/operation_catalog.tex` | two new `\section`s + version 0.9.0 → 0.10.0 + changelog, **including the retroactive G1 entry** | +| 26 | `spec/binary_format.tex` | three payload-layout rows + three tag rows + the `:2432` bullet rewrite + version 0.11.0 → 0.12.0 + Revision History row | +| 27 | `spec/core_spec.tex` | the `:12186` correction and the narrow `:5114` amendment | The tag vocabulary macro (#6) is a **single source of truth**: the decoder, fuzz corpus, conformance vectors, and edit-barrier round-trip all read @@ -194,8 +260,13 @@ show it dies. A test that cannot see its own bug is not a test. the field stays at its `Default` while the effect still reads `Applied`. * **(s2) LWW, last write wins, no conflict.** Two concurrent differing writes resolve to the later in canonical order and record **no** conflict — matching - `SetMetadata` (`ops/tests/graph_reduction.rs:1408`). **Mutation:** reverse the - canonical comparison → the earlier value wins. + `SetMetadata` (`ops/tests/graph_reduction.rs:1408`). **Mutation:** *not* a + reversed comparison — `set_metadata` contains **no comparison at all** + (`reduce.rs:2814`); it records and overwrites unconditionally, and the LWW + outcome falls out of canonical reduction order upstream. Mutate + setter-locally to first-write-wins instead: skip the record-and-overwrite + when the chain already holds a write → the earlier value survives and the + test dies. * **(s3) re-write of an identical value is a new write, not a no-op.** Assert the effect is `Applied` and the chain grew. **Mutation:** add an `AlreadyApplied` short-circuit → dies. *This test exists because pin 5 is the @@ -216,31 +287,43 @@ show it dies. A test that cannot see its own bug is not a test. block carrying either new kind stamps at major 0. **Mutation:** stamp a payload at 3 → the assertion fires. *This is the packet's boundary against G2b.* -* **(s7) undo survives snapshot/restore** — the pin-8 site. Snapshot the - reducer mid-run, restore, then undo. **Mutation:** omit either chain from the - snapshot/restore pair → the restored reducer loses the write history and the - undo silently no-ops. +* **(s7) transaction rollback discards the write** — the pin-8 site. + `WorkingSnapshot` (`reduce.rs:7371`) is the **transaction rollback** + mechanism, not a persistence feature: snapshot before, restore on failure. + Author a setter inside a transaction that then fails, and assert the field + returns to its pre-transaction value. **Mutation:** omit either chain from + the snapshot/restore pair → the failed transaction's write is **retained** + rather than rolled back. (It does not "lose history" — the earlier framing + was backwards, and the wrong framing would have sent the test looking for + the wrong symptom.) * **(s8) decode vectors pinned to literal bytes**, not round-trip. Round-trip locking cannot see a self-consistent encoder/decoder reorder — the 3b-i lesson, where a swap applied to both halves passed 1283 tests and 8/8 - conformance. + conformance. **Mutation:** swap two adjacent fields in *both* the encoder and + the decoder. Every round-trip test must stay green while the literal-byte + vector dies. If the vector survives too, it is pinned to a round-trip and is + not the test it claims to be. * **(s9) text-projection round-trip** for both kinds, matching the existing per-kind coverage, plus a negative test that a `(0 8 0)` header is now - **rejected**. + **rejected**. **Mutation (round-trip half):** emit one kind's production + under the other's tag → parse must reject or mis-round-trip. **Mutation + (version half):** widen the parser to accept any `(0 x 0)` → the negative + test dies. That second one also guards `req:textproj:header-version`'s + reject-all-others clause, which is the actual normative claim here. ## Blast radius `crates/epiphany-core/src/codec.rs` + its `DECISIONS.md`; -`crates/epiphany-ops/src/{payload,envdecode,v0,migrate,reduce,textproj_kind,fuzz,valuegen,vectors}.rs` -+ its `DECISIONS.md`; `crates/epiphany-textproj/src/{lib,vectors}.rs`; +`crates/epiphany-ops/src/{lib,payload,envdecode,v0,migrate,reduce,textproj_kind,fuzz,valuegen,vectors}.rs` ++ its `DECISIONS.md`; `crates/epiphany-textproj/src/{lib,parse,vectors}.rs`; `crates/epiphany-editor-core/src/barriers.rs` and `crates/epiphany-layout-ir/src/barrier.rs` (the two authorized crossings only); `crates/epiphany-testkit/tests/text_projection_grammar.rs`; -`spec/{operation_catalog,text_projection}.tex` **and their rebuilt PDFs**; -`spec/vectors/*.txt` (regenerated, not hand-edited). +`spec/{binary_format,core_spec,operation_catalog,text_projection}.tex` **and all +four rebuilt PDFs**; `spec/vectors/*.txt` (regenerated, not hand-edited). -**Nothing else.** No `epiphany-bundle`, no `binary_format.tex`, no -`epiphany-editor-gui`, no golden re-blessing. +**Nothing else.** No `epiphany-bundle` — the accept-set is G2b's and this packet +must not anticipate it. No `epiphany-editor-gui`, no golden re-blessing. Expect `the_canonical_base_is_byte_identical_across_data_model_majors` (`reduce.rs:10715`) to require a **conscious re-pin**: the seeded corpus's @@ -263,8 +346,12 @@ easy mistake is to re-pin without checking. * Vector corpora **regenerated** via `cargo run -q -p epiphany-testkit --example generate_vectors`, never hand-edited, with the decode-vector count reported before and after. -* Both `.tex` PDFs rebuilt (`latexmk -xelatex`) with **0 undefined references** - reported. A `.tex` commit without its PDF has been a repeat lapse. +* **All four** `.tex` PDFs rebuilt (`latexmk -xelatex`) with **0 undefined + references** reported for each. A `.tex` commit without its PDF has been a + repeat lapse, and this packet touches four documents. +* The normative repairs actually landed: grep `binary_format.tex` for + `CreateInstrument` and confirm the `:2432` bullet no longer denies it, and + confirm both tables reach 33. * No `crates/epiphany-editor-gui/goldens/*.png` byte changes. ## What I will verify independently before committing diff --git a/spec/PLAN_GENESIS_OPS.md b/spec/PLAN_GENESIS_OPS.md index 54c679a..b475873 100644 --- a/spec/PLAN_GENESIS_OPS.md +++ b/spec/PLAN_GENESIS_OPS.md @@ -188,6 +188,27 @@ disposition C) stops being a performance concern the moment G2b lands: pruning would then discard authored genesis state, not merely re-derivable state. G2b's contract must state this as an explicit non-goal. +**G2b holdout — `accidental_extensions`, and why a naïve full-value +`SetTuningContext` would be wrong.** `ScoreTuningContext`'s `Codec` +**deliberately drops** `accidental_extensions` on encode and default-fills it +to `Vec::new()` on decode (`core/src/codec.rs:1939`) — the field is staged out +of schema major 3 and lands at a later one. Meanwhile `OperationSet::accept` +stores the authored envelope **as an object**, not as bytes +(`ops/src/opset.rs:70`). So a `SetTuningContext` carrying a non-empty +`accidental_extensions` would reduce with those extensions present on the +authoring replica, and reduce *without* them on any replica that received the +document through serialization — a silent divergence between a live session and +the same document reloaded. + +**`canonical_value!` does not catch this.** Its generated `decode_canonical` +compares *bytes* (decode → `finish()` → re-encode → reject on mismatch); it +never compares against the originating value, so a field that never reached the +bytes is invisible to it. **G2b needs a normalization-or-subset pin before +dispatch** — either the payload carries a wire-complete subset type, or the +operation normalizes the field away at construction and refuses a non-empty +one. Decide that in the G2b contract, not in its implementation. Does not block +G2a. + ### G3 — the remaining entity families `CreateStaffGroup`, `CreatePartDefinition`, `CreateAnalysisLayer`, `CreateView`,