From c39e58a7557d3544812f98365c4dd5754cdda43d Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Tue, 28 Jul 2026 14:48:10 -0400 Subject: [PATCH] Contract G2a round two: the schema-minor debt, and a test that could not fail All four findings verified against the tree before fixing; all four hold. The schema-minor MUST (binary_format.tex:2330) has never been implemented. SchemaVersion::for_major accepts no minor and returns {major, 0}, and both staging paths derive only the major, so kinds 24-27, 28-29, 30, and 31 already carry no additive-version record -- and the requirement's own rationale is exactly what the gap defeats: a reader meeting an appended discriminant cannot tell a stale vocabulary from damaged bytes. Filed as P13-S14 and ruled a separate rung after G2a, sweeping 24-33 in one retroactive pass rather than blocking G2a on a debt eight kinds deep or paying for two partial sweeps. G2a now says explicitly that it extends the violation by two, knowingly, and forbids working around the absence. s7 could not fail. WorkingSnapshot::restore reassigns the whole graph independently of every write chain, so omitting a chain leaves stale history while the field still rolls back -- the prescribed assertion passed under its own mutation. Two framings of this test were wrong; the third asserts against a later undo's predecessor, and the contract now requires the mutation be run rather than reasoned about. s8's mutation was impossible as written: each payload has one field, so there are no adjacent fields to swap. Swapping discriminants 32/33 in both halves is the self-consistent mutation that leaves round-trips green and kills correctly-named literal vectors. The normative repair surface doubles: eleven sites across four documents, five of them G1 debt. Core's normative OperationKind and OperationKindTag listings are missing CreateInstrument as well as both new kinds; the catalog's value-restoration family list is normative for undo and omitting a family is a silent semantic gap; two spelled-out payload counts move. Since two independent reviews each found sites the other missed, the list is a floor and the contract now prescribes grepping the load-bearing phrases. Also: core_spec said "two edits" and prescribed more, and the split-cost accounting counted only Text Projection when G2b repeats the Binary Format and Operation Catalog work too. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV --- spec/CONTRACT_GENESIS_G2A_SETTINGS.md | 119 ++++++++++++++++++++------ spec/PASS13_CANDIDATES.md | 1 + spec/PLAN_GENESIS_OPS.md | 60 +++++++++++-- 3 files changed, 144 insertions(+), 36 deletions(-) diff --git a/spec/CONTRACT_GENESIS_G2A_SETTINGS.md b/spec/CONTRACT_GENESIS_G2A_SETTINGS.md index 3d946cd..3cb1491 100644 --- a/spec/CONTRACT_GENESIS_G2A_SETTINGS.md +++ b/spec/CONTRACT_GENESIS_G2A_SETTINGS.md @@ -65,11 +65,25 @@ one must not anticipate it in any way. `SetMetadata` would be the bug** — `SetMetadata` is there because `ScoreMetadata` has six mandatory major-2 appends, which is a property of *that* type and nothing else. Test i5 exists to catch exactly this. -4. **No `epiphany-bundle` change of any kind.** - `max_supported_major(OperationEnvelopeBlock)` is 2 (`bundle.rs:69`) and +4. **No `epiphany-bundle` change of any kind — and this now has two distinct + reasons, one of which is a deliberate deferral rather than a non-event.** + (a) `max_supported_major(OperationEnvelopeBlock)` is 2 (`bundle.rs:69`) and **stays 2**. Do not edit `bundle.rs`, and do not touch its cap comment at `:58` — that prose belongs to G2b and moving it early leaves a different - falsehood behind. + falsehood behind. (b) **The chunk schema *minor* is not stamped, and this + packet does not fix that.** `binary_format.tex:2330` is a MUST: a writer must + raise the chunk schema minor when it emits a discriminant appended after the + minor it declares, so an unknown-discriminant decode failure is attributable + to skew rather than corruption. `SchemaVersion::for_major` + (`bundle/src/ids.rs:204`) returns `{major, 0}` unconditionally and takes no + minor, and both staging paths derive only the major + (`testkit/src/bundle_harness.rs:25`, `textproj/src/serialize.rs:183`). So + kinds 24–31 already have no additive-version record, and **G2a knowingly + takes that from eight kinds to ten.** Filed as **P13-S14** and ruled + 2026-07-28 to be its own rung, sequenced after G2a, sweeping 24–33 in one + retroactive pass. **Do not implement minor stamping in this packet, and do + not work around its absence** — if something here appears to need it, that is + a finding about the ruling, so report it. 5. **Reduction copies `set_metadata` structurally** (`reduce.rs:2814`): advisory LWW, **no conflict, no idempotence short-circuit**, no `AlreadyApplied`. The write chain records unconditionally; the graph field is overwritten when a @@ -163,10 +177,9 @@ 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: +**`spec/core_spec.tex`** — five edits. The first is a doctrine amendment and +must be done exactly as pinned; the rest are corrections of fact: -* `: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 @@ -175,12 +188,52 @@ being repaired, not as new work), and the two new `\section`s for this packet. 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. + set. Cite the ruling. +* `:6899` — the `pub enum OperationKind` listing, introduced by its own prose + as **normative for the core**. It is missing `CreateInstrument` (G1 debt) as + well as both new kinds. Add all three in the listing's existing grouped-by- + comment style. +* `:11862` — the `OperationKindTag` listing, same treatment, same three. +* `:12186` — correct the sentence naming which values reach only the + non-canonical snapshot. Do not restructure the surrounding schema-major-1 + narrative. +* `:12207` — *"through the **eight** operation payloads that embed them"*, + followed by an explicit enumeration. Both the count and the list move. + **Count them from the enumeration you write, not from arithmetic on the old + number.** +* `:16475`'s Pass-12 history entry is a record of what was ratified then, not a + live claim: **leave the entry**, 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.** +**`spec/operation_catalog.tex`** — beyond the two new sections and the version +work above, two live-text repairs: + +* `:1495` — the *Value restoration* passage enumerates the LWW families + exhaustively ("… metadata, metric grid, meter change, tempo segment, staff + layout, and the user break advisories"). Both new setters join that list. + This list is normative for undo behaviour, so an omission here is a silent + semantic gap, not a typo. +* `:1633` — the *Retired slots* section, which asserts *"genesis is normatively + the empty-document constructor plus bundle creation, outside the operation + set"*. Same narrow amendment as `core_spec.tex:5114` and the **same trap**: + the *Create score / canvas* slot stays retired and the canvas is still never + op-minted. Only the outside-the-operation-set clause for score **contents** + is superseded. + +**`spec/binary_format.tex`** — beyond the tables and `:2432` above: + +* `:2595` — *"Snapshot-only: `Instrument` (no operation embeds one)"*. False + since G1. +* `:2604` — *"Canonical operation layer: **eight** operation payloads"* and the + minimal-stamping list that follows. Same rule as `core_spec.tex:12207`: + recount from the list you write. + +**That is eleven normative sites across four documents, of which five are G1 +debt.** Two independent reviews each found sites the other missed, so treat the +list as a floor, not a ceiling: **grep for the load-bearing phrases** — +`CreateInstrument`, `layout_defaults`, `SpellingPrecedence`, "outside the +operation set", "snapshot-only", and every spelled-out payload count — and +report anything the list does not already name rather than silently fixing or +silently skipping it. ## The companion version bump @@ -238,9 +291,9 @@ unless noted. | 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` | 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 | +| 25 | `spec/operation_catalog.tex` | two new `\section`s + the `:1495` and `:1633` repairs + 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`, `:2595`, `:2604` rewrites + version 0.11.0 → 0.12.0 + Revision History row | +| 27 | `spec/core_spec.tex` | five edits (`:5114`, `:6899`, `:11862`, `:12186`, `:12207`) + the `:16475` parenthetical | The tag vocabulary macro (#6) is a **single source of truth**: the decoder, fuzz corpus, conformance vectors, and edit-barrier round-trip all read @@ -287,22 +340,34 @@ 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) 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.) +* **(s7) transaction rollback discards the write — and the assertion must not + be on the field.** `WorkingSnapshot` (`reduce.rs:7371`) is the **transaction + rollback** mechanism: snapshot before, restore on failure. But `restore` + reassigns the **whole graph** (`self.graph = s.graph`, `reduce.rs:7441`) + independently of every write chain, so a setter's *field* rolls back whether + or not its chain was snapshotted. **An assertion on the field cannot see this + bug** — it passes under the mutation. Two framings of this test have now been + wrong; this is the third and it must be verified by actually running the + mutation, not by inspection. + + Shape it so the stale chain is observable: author inside a **failed** + transaction, then perform a **successful** write in a second transaction, then + **undo that second transaction** and assert it restores the genuine + predecessor — the pre-failure value, not the rolled-back one. Alternatively + assert against chain contents directly. **Mutation:** omit either chain from + the snapshot/restore pair (`reduce.rs:7386`, `:7423`) → the failed + transaction's write survives in the chain and the undo restores it instead of + the true predecessor. **If the mutation does not kill the test, the test is + wrong — report that rather than weakening the mutation.** * **(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. **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. + conformance. **Mutation:** *not* a field swap — each new payload carries + exactly **one** field, so there are no adjacent fields to reorder. Swap the + two new **discriminants** (32 ↔ 33) in both the encoder and the decoder + instead: self-consistent, so every round-trip test stays green, while + correctly-named literal vectors die. If the vectors survive too, they are + pinned to a round-trip and are not the test they claim 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**. **Mutation (round-trip half):** emit one kind's production diff --git a/spec/PASS13_CANDIDATES.md b/spec/PASS13_CANDIDATES.md index 4e817c7..4c1cacc 100644 --- a/spec/PASS13_CANDIDATES.md +++ b/spec/PASS13_CANDIDATES.md @@ -91,3 +91,4 @@ visible — a value with a wire form and no canonical carrier to reach it.) | P13-S11 | **`AnchorPoint`, referenced by `AccidentalEngraving.anchor`, is defined nowhere.** `core_spec.tex:3166` names `pub anchor: AnchorPoint`; no struct or enum of that name exists in the spec, and `epiphany-core` (whose `Cargo.toml` has no `epiphany-layout-ir` dependency) cannot borrow any layout-ir type even if one shared the name — the same core-native requirement that forced `EngravingBoundingBox`. An undefined leaf frozen onto the wire is the `KeyContext`-shaped gap. Compounding it: the bounding box is documented "relative to the glyph's anchor point" (`:3160`), so freezing an anchor with no defined coordinate frame freezes a point with an undefined origin | this file (verified 2026-07-23; `AnchorPoint` appears once in `core_spec.tex`, defined nowhere; `epiphany-core/Cargo.toml` lists no layout-ir) | **resolved** (ratified: core-native `AnchorPoint { x: SpaceUnit, y: SpaceUnit }`, over the same `SpaceUnit` as `advance_width` and `EngravingBoundingBox`. **Plus one normative sentence pinning the frame**: x/y in canonical space units, y-up, relative to the glyph's coordinate origin — matching the repo's existing Bravura outline convention — so the anchor and the box it anchors share an unambiguous origin. Freezes in tranche 3b) | | P13-S12 | **`SmuflVersion` is undefined, and its obvious representation orders SMuFL's real history backwards.** `SmuflVersionRequirement` (`core_spec.tex:3269`) carries `minimum`/`authored_against` of type `SmuflVersion`, which Chapter 4 references but does not define (it exists only as a **Chapter 7 / layout-ir** type, `glyph.rs:29`, with literal-minor encoding — see the resolution). Ordering is load-bearing (`SmuflVersionRequirement.minimum`, `:3271`, gates the fallback of `req:tuning:smufl-version-fallback`), and the type is dual-purpose — it also anchors Chapter 9's `GlyphCatalog::smufl_version()` / `GlyphCatalogIdentity` (`:10420`, `:10460`), so this freeze touches layout-conformance identity, not just tuning. The trap: SMuFL versions are decimal fractions — 1.12 (2015), 1.18, 1.20 (2016), 1.3 (2019), 1.4 (2021) — that succeed by fraction (0.12 < 0.18 < 0.20 < 0.30 < 0.40). A `{ major: u16, minor: u16 }` storing literal digits with derived `Ord` orders the minors 3 < 4 < 12 < 18 < 20, placing 1.3 and 1.4 **before** 1.12; old fonts declaring 1.18-era versions exist forever | this file (verified 2026-07-23 against `core_spec.tex:3269-3274` and SMuFL's published release history) | **resolved** (ratified shape `SmuflVersion { major: u16, minor_centi: u16 }`, **literal-minor storage rejected**: the minor is stored **fraction-normalized to hundredths** — 1.12→(1,12), 1.18→(1,18), 1.20→(1,20), 1.3→(1,30), 1.4→(1,40), rule normative, release table a note. Derived `Ord` is then correct across the whole real history and collapses the 1.2/1.20 ambiguity (both → (1,20)). **Correction on filing: the leaf is NOT undefined — it exists in `epiphany-layout-ir/src/glyph.rs:29` as `SmuflVersion { major, minor }` with LITERAL minor (`{1,4}`) and derived `Ord`, so the backwards-ordering bug is LIVE there today (1.3 < 1.12), and it is a direct field of `GlyphCatalogIdentity` — conformance identity.** `epiphany-core` cannot depend on layout-ir, and `SmuflVersionRequirement` is core, so the type MUST be defined in core and layout-ir must reuse it — a **unification**, not a fresh definition, which moves `GlyphCatalogIdentity` (`{1,4}`→`minor_centi 40`). Tranche 3a defines `core::SmuflVersion` for the tuning use and leaves layout-ir's alone (a bounded, core-invisible homonym since core can't import layout-ir's); tranche 3b performs the unification and the deliberate `GlyphCatalogIdentity` move. **Correction (2026-07-23, tranche 3b-ii): "with golden/vector regen" above was verified false before that tranche's dispatch.** No golden, baseline, or vector is pinned to the catalog identity — every assertion on `ResolvedLayoutIR::canonical_bytes()` is relative (stability, determinism, and a `metrics_hash[0] ^= 1` sensitivity check that never touches `smufl_version`), and the committed SVG/PNG goldens do not embed it. The move changes the catalog's emitted bytes (`encode_catalog`'s minor field: `04 00` → `28 00`) in *value*, with nothing to regenerate. The hundredths scale is blocking for 3b, free to adjust in 3a) | | P13-S13 | **The score tuning context has no canonical persistence path: it can be saved, but never authored, replicated, or merged.** Schema major 3 (Push 4b tranche 3b-i) put `smufl` and `overrides` on the wire, but the only *persisted* carrier that embeds a `ScoreTuningContext` is the **acceleration snapshot**, which Chapter 8 makes explicitly non-canonical and regenerable and which the bundle may discard and rebuild at will. No **canonical** carrier embeds it at all: no operation authors it (`epiphany-ops` has no tuning-context payload anywhere — the vocabulary's only `tuning` references are the per-pitch `TuningReference::Inherit`), and `MaterializedState` carries effects, conflicts, anomalies, objects, spellings, breaks, page-breaks, and pending, but no tuning context. So a user who selects a tuning system or sets a per-voice override has authored something the format cannot durably represent, and `req:tuning:tuning-resolution-order`'s scopes 2–4 (`overrides`) are in practice unreachable from any document a replica could exchange | this file (found 2026-07-24 while scoping text-projection parity; verified by searching the whole `epiphany-ops` operation vocabulary and `MaterializedState`'s field list) | **resolved by the genesis tranche** — `spec/RULING_GENESIS_PERSISTENCE.md` (ratified 2026-07-24, 011c68a) reverses Pass-12 K8 and absorbs genesis into the operation set, naming `SetTuningContext` in its §2 settings table. **This was not a tuning-specific defect and did not get a tuning-specific fix:** the operation arrives as one of nine surfaces in a single coordinated tranche, and the wire layout is untouched. **The disposition, and the evidence that produced it:** `spec/ANALYSIS_GENESIS_PERSISTENCE.md` maps the same gap across the whole `Score`: **eight fields have no operation that can produce them** — `canvas.layout_defaults`, `instruments`, `staff_groups`, `parts`, `tuning_context` (this entry, its field 10), `spelling_precedence`, `analysis_layers`, `views`, plus `identity` — and one more (`decomposition_attachments`) can only be pruned back, never authored. Independently re-verified against the working tree: each field's sole `reduce.rs` mention is a read-only base-seed read. Of the four dispositions weighed there (a canonical genesis block; closing the op-coverage gap; promoting the canonical base to carry graph values; scope-limiting), **the second was taken** — every mutable `Score` field becomes operation-authored — so S13 closes when that tranche lands and is tracked *there*, not here. **One cost this entry contributes as evidence:** the per-field operation route is not the free schema-minor it appears. Blocks stamp *minimally*, at the lowest major whose layouts decode them, and `bundle.rs`'s `max_supported_major` currently caps `OperationEnvelopeBlock` at **2** on the explicit ground that no operation payload embeds the tuning context. A `SetTuningContext`-style operation would make its blocks stamp v3, dragging a role accept-set raise along with the kind append — for one field of eight. That is a concrete instance of the analysis's closing constraint, that any option enumerating fields must be re-audited against its table whenever `Score` gains a field. **This cost is now paid deliberately rather than avoided — but NOT amortised, as first written.** The ruling's "one accept-set raise, spent once" implied a single batch landing every new kind at `OperationEnvelopeBlock` major 3, spreading the raise across nine surfaces. Re-derived 2026-07-28 against the working tree, that is wrong: minimal stamping is a pure function of each payload's value, so the other eight surfaces stamp at major 0, 2, or 2 and never reach the raised bound at all. `SetTuningContext` is the sole payload born at v3, so the raise is charged to **exactly this surface** after all — which is why `spec/PLAN_GENESIS_OPS.md` §4 splits G2 and isolates it in **G2b**. **And the closure argument is not the canonical base:** the base is role-bound to major 0 (`mis_stamped_canonical_base`) and is a `MaterializedState`, which embeds no graph values for *any* field — including `metadata`, op-authored since M2d and durable purely through its operations. S13 closes on that precedent: the op log is canonical, and G2b makes an operation author the tuning context. Consequence to carry forward: once G2b lands, pruning would discard *authored* genesis state rather than merely re-derivable state, so the standing prohibition on pruning (blocked on disposition C) gains real teeth. Note `bundle.rs` documents the cap of 2 *with the tuning-context rationale in prose*, so that comment must move with the cap. Note the wire layouts themselves are already frozen and correct under every disposition: this was never about how the tuning context encodes, only about which carrier embeds it. Not a regression — nothing ever worked; major 3 made the gap visible by giving the value a wire form and no way to reach it) | +| P13-S14 | **The chunk schema *minor* is a normative MUST that no writer has ever honoured, so every appended operation discriminant is unattributable to version skew.** `binary_format.tex:2330` requires a writer to raise the chunk schema minor when it emits any discriminant appended after the minor it otherwise declares, with a stated rationale: so that a decode failure on an unknown appended discriminant is attributable to a version skew rather than to corruption. `SchemaVersion::for_major` (`bundle/src/ids.rs:204`) returns `{major, 0}` unconditionally — it takes only a major and has no minor input — and **both** writer-side staging paths derive nothing but the major (`testkit/src/bundle_harness.rs:25`, `textproj/src/serialize.rs:183`, each mapping `max(OperationEnvelope::schema_major)` through `for_major`). So every kind appended after the initial vocabulary has no additive-version record: **24–27** (Phase-3 first tranche), **28–29** (the schema-major-2 repeat revision), **30** (Push 4a `TransposeInterval`), and **31** (genesis G1 `CreateInstrument`). The requirement's own failure mode is exactly what the gap produces: a reader meeting kind 31 from a newer writer cannot distinguish "my vocabulary is stale" from "these bytes are damaged" | this file (found 2026-07-28 during G2a contract review; verified against the working tree — `for_major`'s signature admits no minor, and neither staging path computes one) | **open, and deliberately extended.** Ruled 2026-07-28: the fix is its own rung, sequenced **after** G2a. G2a appends kinds 32/33 and takes the violation from eight kinds to ten, knowingly — the debt is already eight deep, the +2 is immaterial against it, and one retroactive sweep over 24–33 is strictly less work than 24–31 now and 32–33 later. **What the rung owes:** a minor-assignment policy (which minor each past append belongs to — this is a retroactive judgement, not a derivation), a per-kind `schema_minor()` or equivalent, block minor = max over payloads, a `for_major` replacement that accepts one, and both staging paths. **Check before scoping it** whether the chunk header's minor reaches any pinned corpus, since that decides whether this is a vector-moving change. Note this is orthogonal to the *major* accept-set: `OperationEnvelopeBlock` stays at 2 through G2a and rises to 3 only at G2b | diff --git a/spec/PLAN_GENESIS_OPS.md b/spec/PLAN_GENESIS_OPS.md index b475873..c02cd03 100644 --- a/spec/PLAN_GENESIS_OPS.md +++ b/spec/PLAN_GENESIS_OPS.md @@ -8,8 +8,15 @@ which questions must be answered before a dispatch contract can be written. **Status:** **G1 landed** (3b09595, CI green) via `spec/CONTRACT_GENESIS_G1_INSTRUMENT.md`. **G2a contracted** -(`spec/CONTRACT_GENESIS_G2A_SETTINGS.md`); G2b and G3 scoped, not contracted. -§6 lists what still needs ratification. +(`spec/CONTRACT_GENESIS_G2A_SETTINGS.md`); **G-minor**, G2b, and G3 scoped, not +contracted. §6 lists what still needs ratification. + +**G1 shipped documentation debt** — five normative falsehoods across +`binary_format.tex`, `core_spec.tex`, and `operation_catalog.tex`, because its +contract declared the normative wire surfaces out of scope. G2a repairs them. +The lesson generalises: **an operation-vocabulary append is a documented event +in four specification documents**, and any contract on this track that does not +name them all is wrong. See §4's split-cost accounting. --- @@ -165,13 +172,25 @@ context, so no op block is ever born at v3". G2b is precisely what falsifies that sentence, so the comment must move with the number. Same for `DECISIONS.md`'s superseded prohibition, which is already marked. -**The cost of splitting, stated honestly:** each packet appends *kind* -productions to the text-projection grammar, and a kind append is a -document-surface change (the G1 precedent). So the companion bumps twice — -0.8.0 → 0.9.0 → 0.10.0 — and each bump re-sweeps five live version sites in -`text_projection.tex` plus a changelog row, re-flips the negative -`superseded_companion_version` vector, and regenerates the vector corpora. -That is mechanical and pre-1.0; it is the cheaper of the two risks. +**The cost of splitting, stated honestly — and it is larger than first +written.** Each packet appends to the operation vocabulary, and that is a +documented event in **three** companions, not one: + +* `text_projection.tex` — a new *kind* production is a document-surface change + (the G1 precedent), so `COMPANION_VERSION` bumps per packet: 0.8.0 → 0.9.0 → + 0.10.0, each re-sweeping five live version sites plus a changelog row and + re-flipping the negative `superseded_companion_version` vector. +* `binary_format.tex` — payload-layout and tag rows per kind, plus a version + bump and a Revision History row (the 0.2.0 entry is the precedent). +* `operation_catalog.tex` — a `\section` per kind, plus a version bump and a + changelog paragraph. + +Plus `core_spec.tex`'s normative `OperationKind`/`OperationKindTag` listings +and its spelled-out payload counts, and a regenerated vector corpus, per +packet. So the split roughly **doubles the documentation work**, and G2b pays +it again in full. Still the cheaper of the two risks — burying a one-way +accept-set door in a packet of routine work is worse than repeating a +mechanical sweep — but it is not the small tax the first draft implied. **P13-S13 closes at G2b — and on the metadata precedent, not on the canonical base.** The base cannot carry a v3 tuning context: it is role-bound to major 0 @@ -209,6 +228,29 @@ 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. +### G-minor — the schema-minor sweep (ruled 2026-07-28, sequenced after G2a) + +`binary_format.tex:2330` requires a writer to raise the chunk schema **minor** +when it emits a discriminant appended after the minor it declares — a MUST with +a stated rationale, so that an unknown-discriminant decode failure is +attributable to version skew rather than corruption. No writer has ever done +it: `SchemaVersion::for_major` (`bundle/src/ids.rs:204`) returns `{major, 0}` +unconditionally and accepts no minor, and both staging paths derive only the +major (`testkit/src/bundle_harness.rs:25`, `textproj/src/serialize.rs:183`). + +So kinds **24–27**, **28–29**, **30**, and **31** already carry no additive +record, and G2a takes that to **32–33** knowingly. Filed as **P13-S14**. Ruled: +one retroactive sweep over 24–33 after G2a, rather than blocking G2a on a debt +already eight kinds deep or paying for two partial sweeps. + +What the rung owes: a minor-assignment policy (which minor each past append +belongs to — a retroactive judgement, not a derivation), a per-kind minor, block +minor = max over payloads, a `for_major` replacement that accepts one, and both +staging paths. **Scope it only after checking whether the chunk header's minor +reaches a pinned corpus** — that decides whether it is a vector-moving change. +Orthogonal to the *major* accept-set, which stays 2 through G2a and rises to 3 +only at G2b. + ### G3 — the remaining entity families `CreateStaffGroup`, `CreatePartDefinition`, `CreateAnalysisLayer`, `CreateView`,