epiphany/spec/CONTRACT_GENESIS_G2A_SETTIN...

32 KiB
Raw Blame History

Contract: genesis tranche G2a — the two major-0 settings setters

Repo root /home/jeans/Repos/active/epiphany. Governed by spec/RULING_GENESIS_PERSISTENCE.md (011c68a) and spec/PLAN_GENESIS_OPS.md §4 (ladder ratified 2026-07-24, split into G2a/G2b 2026-07-28). Predecessor: spec/CONTRACT_GENESIS_G1_INSTRUMENT.md, landed at 3b09595.

Execution model as every tranche on this track: Sonnet subagent, coordinator line-level review with independent mutation re-runs, user deep-dive at contract sign-off and final report. Mutation discipline throughout: anchor-assert before substituting, restore by reversing, never git checkout.

Parallel safety. The editor track owns epiphany-editor-gui, epiphany-render-svg, epiphany-glyphs, every crates/epiphany-editor-gui/goldens/*.png, spec/PLAN_EDITOR_APP.md, spec/CONTRACT_EDITOR_*.md, and spec/ANALYSIS_TEXT_RUN_PRIMITIVES.md (currently untracked — do not stage it). This packet does cross into epiphany-editor-core and epiphany-layout-ir, by necessity, at the two sites named in §"The boundary crossing". That crossing is bounded to those two files and is authorized for this packet only. Stage files explicitly; never git add -A.


What this packet does, in one sentence

Adds two LWW settings operations — SetCanvasLayoutDefaults and SetSpellingPrecedence — on the SetMetadata pattern, so that two more of the ruling's nine Score surfaces become operation-authored, without touching any wire bound.

Why these two, and why not the third

G2's three setters do not sit at the same schema major. Verified by reading the frozen codec walks, not by inference:

Op Carried type Major Evidence
SetSpellingPrecedence SpellingPrecedence 0 plain Codec::dec at v0/v1/v2 and live (core/src/codec.rs:2673, :3249, :3372) — never versioned
SetCanvasLayoutDefaults CanvasLayoutDefaults 0 versioning lives in the containing Canvas walk, not the leaf: dec_canvas_v0 default-fills the field (codec.rs:2775), enc_canvas_v1 writes it through the live Codec (:3158)
SetTuningContext ScoreTuningContext 3 enc_tuning_context_v2 writes 3 fields (codec.rs:3293), the live Codec writes 5; a default smufl + empty overrides still append bytes

SetTuningContext alone drags max_supported_major(OperationEnvelopeBlock) from 2 to 3, and that raise is a one-way door — an older reader meeting a v3 block preserves the bundle read-only. It is G2b, a separate packet, and this one must not anticipate it in any way.

Design pins

  1. No wire layout is designed. Both carried types already have Codec impls and already ship inside Score. Add CanvasLayoutDefaults and SpellingPrecedence to canonical_value! (core/src/codec.rs:3455) and encode each payload as push_lp_bytes(out, &self.<field>.canonical_bytes()) — the SetMetadataOp template verbatim (ops/src/payload.rs:1360). The generated decode_canonical already does decode → finish() → re-encode → reject-on-mismatch, so strict-form enforcement is inherited, not written.
  2. Discriminants: kind 32/33, tag 32/33. Both spaces currently top out at 31 (CreateInstrument). Assign SetCanvasLayoutDefaults = 32, SetSpellingPrecedence = 33 in both spaces. They coincide here by accident, not by rule — the spaces are independent and misaligned elsewhere (RespellPitch is kind 2 / tag 3). Append-only under req:binfmt:kind-discriminants.
  3. schema_major() gains NO arm. Both fall into the existing catch-all _ => 0 (payload.rs:262). Adding them to the => 2 arm alongside SetMetadata would be the bugSetMetadata 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 — 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. (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) maps a major to a fixed constant — note V0 is {0, 1}, not {0, 0} (ids.rs:173), while V1/V2/V3 carry minor 0 — and, decisively, accepts only a major, so no per-kind additive minor can reach it. Both staging paths likewise derive only the major (testkit/src/bundle_harness.rs:25, textproj/src/serialize.rs:183). So kinds 2431 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 2433 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 graph is present. Do not import the create_staff mint discipline (AlreadyApplied / RecreateContentMismatch) — these are field overwrites, not mints, and a re-write of an identical value is a legitimate new write.
  6. Both chains must be seeded from the base, beside metadata_chain.seed(...) (reduce.rs:1385). This is what makes value-restoring undo of the first operational write restore the pre-operational value rather than nothing. Under from-empty the base is Score::empty, so the seed is the type's Default — and restoring the default is correct for both the never-authored and the authored-to-default case. There is no "never authored" state to distinguish: these are always-valued Score fields, not map keys, so the Predecessor::Base vs ::Write distinction that matters for spellings/breaks (reduce.rs:707) does not apply. Do not invent an Option wrapper.
  7. Undo plumbing is three sites per setter, and one is easy to miss. Mirror metadata_chain at: the undo_verdict walk (reduce.rs:5184), the ValueRestoration enum variant (:785), and the restoration application (:5404). Missing the application site fails silently — the verdict is computed and discarded.
  8. The snapshot/restore pair. Both chains must appear in the snapshot/restore pair alongside metadata_chain (reduce.rs:7386, :7423). This is the G1 silent-failure site: omitting it surfaces only under undo/replay, never in a straight-line test.
  9. No deletes. These are field overwrites; there is nothing to tombstone.

The boundary crossing

Per PLAN_GENESIS_OPS.md §5 trap 6 — the G1 lesson, budgeted up front rather than discovered mid-dispatch. Adding an OperationKind variant is not containable to core + ops:

  • crates/epiphany-editor-core/src/barriers.rs:468subjects_of is exhaustive; without arms the workspace does not compile, and because epiphany-testkit depends on editor-core, the entire gate is blocked — conformance and requirement_labels included. Both new kinds are score-level field overwrites with no resolvable region or object, exactly like SetMetadata: join the existing OperationKind::SetMetadata(_) | OperationKind::DeclareTransaction(_) arm rather than writing new ones. That file's module doc (:22) already names SetMetadata as the score-level exemplar; extend that sentence.
  • crates/epiphany-layout-ir/src/barrier.rs:1105 — the "one past the vocabulary" literal, currently 32, becomes 34. It is deliberately a literal rather than PAYLOAD_FREE.len() so the bump is a conscious act; keep it that way and update the comment's kind list.
  • crates/epiphany-testkit/tests/text_projection_grammar.rs:307 — a 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 2427 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 \sections for this packet.

spec/core_spec.texfive live-text edits plus one historical annotation. The first is a doctrine amendment and must be done exactly as pinned; the rest are corrections of fact:

  • :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.
  • :6899 — the pub enum OperationKind listing, introduced by its own prose as normative for the core. It needs four additions, not two: TransposeInterval (kind 30, Push-4a debt — the listing carries the older Transpose and never gained its successor), CreateInstrument (kind 31, G1 debt), and both new setters. Add them in the listing's existing grouped-by-comment style.
  • :11862 — the OperationKindTag listing, same treatment, the same four. It likewise has Transpose and no TransposeInterval.
  • :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.

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, and most of the work is not this packet's own: five sites are G1 debt, and the two core listings are additionally Push-4a debt — they carry Transpose and never gained TransposeInterval, appended at kind 30 in 2026-07. So the vocabulary has been drifting from its normative listings for two tranches, not one.

Three independent reviews each found sites the previous ones missed, so treat the list as a floor, not a ceiling: grep for the load-bearing phrasesCreateInstrument, TransposeInterval, 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. A count that disagrees with the enumeration beside it is the signature to look for; two such counts are already known (core_spec.tex:12207, binary_format.tex:2604).

The companion version bump

Two new kind productions are a document-surface grammar change, so COMPANION_VERSION moves 0.8.0 → 0.9.0 (textproj/src/lib.rs:29). This is the G1 precedent and it is not optional: holding the version while extending the grammar leaves two incompatible grammars claiming (0 8 0), and req:textproj:header-version requires a parser to accept exactly the version it implements and reject all others. Cached projections do not migrate — TextProjection is a non-canonical accelerator, so stale ones regenerate.

Sites: textproj/src/lib.rs:29 (the constant and its doc block), and in spec/text_projection.tex the five live sites at :237, :470, :521, :1121, :1309 plus a new changelog row.

Do not bulk-replace 0.8.0 / (0 8 0) across the .tex. The 0.8.0 changelog row is history and must keep its version; a blind sweep falsified exactly that row during G1 and had to be reverted. Edit the five live sites individually and append the new row.

Also flip textproj/src/vectors.rs's negative superseded_companion_version vector: its "wrong version" must become the newly superseded (0 8 0), since (0 9 0) is now the accepted one.

Touch points

The earlier claim that this table was "derived by enumerating every SetMetadata site" was false. That grep was run, and rows 28 and 29 below appeared in its output and were not carried into the table — the evidence was on screen and went unused, which is worse than not having looked. Treat the table as reviewed rather than derived, and check it against a fresh grep.

Each row is per setter unless noted.

# File Site (SetMetadata analogue)
1 core/src/codec.rs canonical_value! — add both carried types (:3455)
2 ops/src/payload.rs op struct + CanonicalEncode (:1356, :1360)
3 ops/src/payload.rs OperationKind variant (:167)
4 ops/src/payload.rs discriminant() → 32 / 33 (:289)
5 ops/src/payload.rs tag() (:336) + encode dispatch (:385)
6 ops/src/payload.rs OperationKindTag variant (:427) + operation_kind_tag_vocabulary! entry (:533) — compile-enforced
7 ops/src/payload.rs schema_major()no edit (pin 3)
8 ops/src/envdecode.rs discriminant decode (:530) + tag→kind (:819)
9 ops/src/v0.rs V0OperationKind variant (:92)
10 ops/src/migrate.rs both directions (:167, :323)
11 ops/src/reduce.rs dispatch (:2736) + the setter fn beside set_metadata (:2814)
12 ops/src/reduce.rs chain decls (:901, :1012), init (:1292), base seed (:1385)
13 ops/src/reduce.rs undo verdict (:5184), ValueRestoration variant (:785), restoration apply (:5404)
14 ops/src/reduce.rs snapshot / restore (:7386, :7423)
15 ops/src/textproj_kind.rs production (:174) + parse (:443)
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 two new \sections + 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
28 testkit/src/generators.rs the operation generator (:647), rng.below(30) over a hand-maintained arm list. Already stale: it stops after the repeat pair, so TransposeInterval (Push-4a debt) and CreateInstrument (G1 debt) are absent from every corpus it feeds. Add all four and widen the bound
29 testkit/src/layout_stub.rs gen_operation_kind_tag (:951), whose doc claims "every variant Agent C's type provides" while rng.below(30) omits tags 30 and 31. Do not just extend it — derive the built-ins from OperationKindTag::PAYLOAD_FREE and append Registered (excluded from PAYLOAD_FREE by design, payload.rs:469). payload.rs:1991 already does exactly this and is the in-repo precedent
30 ops/src/textproj_kind.rs a fourth deliberate literal count (:597), currently 32 → 34

The tag vocabulary macro (#6) is a single source of truth: the decoder, fuzz corpus, conformance vectors, and edit-barrier round-trip all read PAYLOAD_FREE, and a variant missing an entry fails to compile. It exists because Push 4a added TransposeInterval to a hand-written match and nothing else, and four hand-maintained lists stayed green while the decoder rejected its own encoding. OperationKind::discriminant() (#4) has no such guard.

Tests + minimum mutations

Every test must be mutation-verified: re-introduce the bug it exists to catch and show it dies. A test that cannot see its own bug is not a test.

  • (s1) from-empty authoring. From Score::empty(identity) through reduce_operation_set_onto, each setter produces the authored value in the materialized Score. Mutation: drop the graph write in the setter fn → 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: 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 most likely thing for a subagent to get wrong by pattern-matching on create_staff.

  • (s4) value-restoring undo reaches the seeded base. Author once inside a transaction, undo it, and assert the field returns to the base value — run this both from-empty (base = Default) and onto a loaded base with a non-default value, so the test distinguishes "restored the base" from "restored the type default". Mutation: remove the .seed(...) call → the undo produces Restore(None) and the field does not move.

  • (s5) minimal stamping stays 0. Both kinds report schema_major() == 0 for every value, including a non-default one. Extend schema_majors_follow_the_minimal_stamping_rule (reduce.rs:10546). Mutation: move either kind into the => 2 arm → dies.

  • (s6) the accept-set did not move. Assert max_supported_major(ChunkKind::OperationEnvelopeBlock) == 2, and assert a 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 — 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: 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 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.

  • (s10) the generators actually emit the new kinds. Rows 2829 feed corpora other suites treat as exhaustive, so a kind absent from them is untested everywhere downstream while every suite stays green — which is how TransposeInterval and CreateInstrument came to be missing from both. For row 28, assert that a bounded draw yields all four appended kinds (3033). Mutation: drop one arm / leave the bound unwidened → the assertion fires.

    For row 29, do not assert 3033 — that would pass with the derivation's Registered append deleted, even though PAYLOAD_FREE excludes it by design (payload.rs:469) and the generator's own doc promises every variant. Assert the draw covers exactly PAYLOAD_FREE {Registered}. Mutation: remove the Registered append → the assertion fires. Future built-ins then follow structurally with no test change, while the one payload-bearing variant that cannot follow structurally stays locked. If either half of s10 can survive its mutation, the derivation is wrong — report that rather than weakening the assertion.

Blast radius

crates/epiphany-core/src/codec.rs + its DECISIONS.md; 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/{binary_format,core_spec,operation_catalog,text_projection}.tex and all four rebuilt PDFs; spec/vectors/*.txt (regenerated, not hand-edited).

Plus crates/epiphany-testkit/src/{generators,layout_stub}.rs (rows 2829) — generator source, not test enrichment: both are already stale by two tranches, and both feed corpora that other suites treat as exhaustive.

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.

Note what rows 2830 are evidence of. operation_kind_tag_vocabulary! exists precisely because Push 4a added TransposeInterval to a hand-written match and nothing else, and its own doc says four hand-maintained lists stayed green (payload.rs:461). The macro made the compile-enforced half safe — and these three lists went stale at that same append anyway, because nothing forces a hand-written match arm, an rng.below(N) bound, or a literal count to move. So every new kind must visit six hand-maintained sites, none of which the compiler checks:

  1. ops/src/payload.rs:266OperationKind::discriminant(), a hand-written match. This is the exact site Push 4a got wrong, and it is still unguarded; the macro guards the tag space beside it, not this one.
  2. layout-ir/src/barrier.rs:1105 — the "one past the vocabulary" literal.
  3. testkit/tests/text_projection_grammar.rs:307 — a kind count.
  4. ops/src/textproj_kind.rs:597 — a second kind count.
  5. testkit/src/generators.rs:647 — an rng.below(N) bound over hand-written arms.
  6. testkit/src/layout_stub.rs:951 — the same, over tags.

Row 29 converts 6 into a derivation, leaving five manual visits for every future append. That is the only structural fix available in this packet — so prefer deriving over extending wherever a list can be derived, and name in the report every site that could not be, so the next tranche inherits an accurate count rather than this one's.

Expect the_canonical_base_is_byte_identical_across_data_model_majors (reduce.rs:10715) to require a conscious re-pin: the seeded corpus's gen_payload gains discriminants 32/33, shifting the RNG stream, exactly as at Phase D and at G1. That is a corpus shift, not a value leak. State in the report which it is, and how you distinguished them — a genuine leak of a settings value into the canonical base would be a ruling violation, and the easy mistake is to re-pin without checking.

Gate (report actual output, never stale numbers)

cargo fmt --check; cargo clippy --workspace --all-targets -D warnings at 0 warnings; cargo test --workspace at 0 failed; cargo test --doc at 0 failed; conformance 8/8, and 9/9 with --features golden-gate; requirement_labels 6/6 with its three observed counts reported as seen (212/282/282 at 439e1e2; they move with the catalog sections). Plus:

  • max_supported_major(OperationEnvelopeBlock) is still 2 — assert it in code and state it in the report.
  • 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.
  • 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

Build to survive this. I re-run every mutation myself, and I check specifically: that #4's hand-written discriminant match and #6's macro entry agree; that pin 3 really added no schema_major arm; that pin 8's snapshot/restore pair was not missed; that s3 fails for the stated reason rather than incidentally; that the canonical-base re-pin is a corpus shift and not a value leak; that the .tex 0.8.0 changelog row was not rewritten; and that no claim in the report is copied forward from this contract rather than observed — the recurring failure mode on this project is a plausible claim propagating because nobody re-derived it.

Report

Files + summary, exact asserted values, every mutation with kill evidence, gate output verbatim, deviations flagged explicitly. If any pin here turns out to be wrong, say so rather than working around it silently. In particular: if the boundary crossing turns out to be wider than the three sites named above, that is a finding about the contract, not a nuisance — report it.