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`,