epiphany/spec/CONTRACT_GENESIS_G3A_ENTITI...

34 KiB
Raw Blame History

Contract: Genesis G3a — the four root-level entity mints

Governed by spec/RULING_GENESIS_PERSISTENCE.md and spec/PLAN_GENESIS_OPS.md §4 (G3 split ratified 2026-07-29) and §6 (rulings 1, 2, 3 ratified 2026-07-29). Predecessor rung: G2b, signed off at 25c4733.

Scope. Four operations that mint the four remaining root-level Score entity vectors:

Op Kind Tag Carried type Score field schema_major()
CreateStaffGroup 35 35 StaffGroup staff_groups 0
CreatePartDefinition 36 36 PartDefinition parts 0
CreateAnalysisLayer 37 37 AnalysisLayer analysis_layers 0
CreateView 38 38 ViewDefinition views 0

All four ride the CreateStaff set-union mint pattern (reduce.rs:4075) with byte-identical re-carry idempotence. Epoch 11 for all four.

Explicitly out of scope: CreateMeasure (G3b), every delete (§6.1, deferred), graph invariant 20 (G3b), any new PreconditionFailureReason (G3b), and pruning or compaction of any kind (standing prohibition, see pin 9).


1. Why this rung exists — a live defect, not a completeness item

All five G3 object kinds become Live in the reducer's object map only through base ingest (reduce.rs:1449:1563). No operation mints any of them. Two consequences hold in the tree today:

  • CreateStaff validates Staff.group against a live StaffGroup (reduce.rs:4372). Under from-empty reduction — the path G1 created and T1b depends on — that precondition is unsatisfiable. A document built only from operations can never author a grouped staff.
  • TimeAnchor::Measure (reduce.rs:1280) can never resolve from empty. That half is G3b's.

G3a completes the four root-level vectors. It closes the satisfiability half only — §1.1 (ratified) rules that a bidirectionally consistent staff group remains unauthorable in this packet, by design and on the record.


1.1 The pin, RESOLVED — StaffGroup/Staff authorship authority

Ratified 2026-07-29: disposition B. Drafted in the contract, decided by the user — the shape G2b's accidental_extensions pin took.

The cycle

CreateStaff requires its group to be a live StaffGroup (reduce.rs:4372). CreateStaffGroup (pin 4) requires each of its members to be a live Staff. Neither can name the other first, so with mints only, no authoring order produces a bidirectionally consistent group:

  • CreateStaffGroup(g, members: [])CreateStaff(s, group: Some(g)) leaves s.group == Some(g) while g.members == [].
  • CreateStaff(s, group: None)CreateStaffGroup(g, members: [s]) leaves the mirror image, and re-carrying CreateStaff with group: Some(g) is RecreateContentMismatch, not an amendment.

There is no ModifyStaffGroup and no ModifyStaff, and deletes are deferred (§6.1). So the inconsistency is not merely unchecked — it is unrepairable within this packet.

Why nothing in the tree decides it

Invariant 10 checks resolution in both directions independently — a staff's group is declared (invariants.rs:1188), a group's members are declared (:1196) — and agreement in neither. No agreement check exists anywhere in the crate. The specification declares StaffGroup.members (core_spec.tex:4231) and Staff.group (:5585, "Visual grouping: which staff group (if any) this staff belongs to") without stating which is authoritative or requiring them to agree. This is genuinely unspecified, not merely unimplemented.

Disposition A — Staff.group authoritative, members reduction-maintained

CreateStaffGroup mints identity, name, and kind, and MUST carry members: []; a non-empty members is refused or normalized away at construction (the "subset over normalization" choice G2b faced). Thereafter StaffGroup.members is maintained by reduction: create_staff with group: Some(g) appends to g.members. Fully authorable from empty; joining is commutative and set-union, which fits the CRDT discipline.

Costs, stated honestly: it makes members a derived field of a stored type, so byte-identical re-carry of CreateStaffGroup MUST compare against the carried value (empty), never the current derived state — an explicit sub-pin, and exactly the class of confusion canonical_value! cannot catch. It also expands G3a beyond a pure mint packet, which is the property that made this rung cheap.

Disposition B — declare the authority, defer the enforcement — RATIFIED

Ratified 2026-07-29 with this precise meaning, which is normative:

  • Staff.group is the sole authority for membership. Every consumer reads membership from Staff.group. Nothing may read StaffGroup.members to decide whether a staff is in a group.

  • StaffGroup.members is a non-authoritative denormalized projection.

  • G3a stores it but neither maintains nor trusts it. CreateStaffGroup carries members as given and validates resolution only (pin 4).

  • G3a permits both stale forms, and they are equally permitted:

    • a missing member — s.group == Some(g) while g.members omits s;
    • a spurious member — g.members contains s while s.group is None or names a different group.

    An earlier draft named only the first. Naming only the missing-member form would have understated the ruling and left the spurious form looking like a bug rather than a permitted state.

This is a normative semantic ruling, not a deferral of one. The earlier draft's argument for B — that it "adds no semantics" while A does — was wrong and is withdrawn. B assigns authority to a field the specification left unranked, which is a semantic change; what it defers is enforcement, not meaning. The honest statement of B's advantage over A is narrower: it adds semantics without adding machinery, leaving the mint a mint.

Filed as P13-S16 (spec/PASS13_CANDIDATES.md) — the concrete gap, both stale forms, the disposition-A fix, the candidate invariant 21, and the instruction that consumers read Staff.group only. The earlier draft claimed a "filed gap" while no such entry existed; it does now.

Disposition A remains the later maintenance/enforcement fix, sequenced after G3b so an invariant append is not competing with invariant 20.

What t8 must therefore claim

t8's claim is narrowed — see §4. The unqualified "the defect closes" framing was wrong: what closes is that CreateStaff.group's precondition becomes satisfiable, not that a consistent group becomes authorable.

t8 must pin both asymmetric authoring orders, not only the first:

  • CreateStaffGroup(g, members: [])CreateStaff(s, group: Some(g))s.group == Some(g), g.members == [] — the missing form;
  • CreateStaff(s, group: None)CreateStaffGroup(g, members: [s])g.members == [s], s.group == None — the spurious form.

Both must be asserted to hold, as permitted states under the ruling. A test that pins only one leaves the other free to change silently.


2. Design pins

Pin 1 — kinds and tags are 3538, in both spaces, and they are aligned here

Next free is kind 35, tag 35 (payload.rs:390, :714, re-verified 2026-07-29). The two spaces are not aligned in general — OperationKind::discriminant() is a hand-written match (payload.rs:253) while OperationKindTag is macro-generated (payload.rs:440); RespellPitch is kind 2 and tag 3. They happen to coincide from 24 upward. Assign each space explicitly and never derive one from the other; pin 1's test asserts both independently.

Assignment order is fixed as the table in §Scope: StaffGroup 35, PartDefinition 36, AnalysisLayer 37, View 38.

Pin 2 — schema_major() gains no arm; the catch-all _ => 0 is correct

This is G2a's shape, not G2b's. All four carried types have exactly one byte layout: no versioned walk exists for any of them, and both decode_v0_score (codec.rs:2698) and the live walk (:3274) read all four through plain Codec::dec. Adding them to the => 2 arm would be the bug. Verify by reading the walks, not by assuming.

Consequence: no epiphany-bundle change of any kind. The op-block accept-set stays at 3 where G2b left it. If the implementation touches a bundle file, something is wrong.

Pin 3 — the four types need canonical_value! entries, and no other code change in core

Each already has a Codecstruct_codec!(PartDefinition …) (codec.rs:1790), AnalysisLayer (:1791), ViewDefinition (:1792), StaffGroup (:2329) — and each is already exported from lib.rs. None is in canonical_value! (codec.rs:3518); G3a adds exactly four entries, in the §Scope order, under one comment naming this contract.

canonical_value! introduces no new byte layout — it makes the existing whole-score layout reachable per-value, and its generated decode_canonical gives strict canonical-form enforcement (decode → finish() → re-encode → reject on mismatch) for free.

No textvalue_graph.rs work. struct_codec! generates the TextValue impl as well as the Codec (codec.rs:510, :522), so all four types already project and parse. This is the one place G3a is cheaper than G2b, which had to hand-write TextValue for TuningContextSettings. Confirm it by compiling, not by assuming.

Pin 4 — referential preconditions are graph-aware, mirroring CreateStaff

Per ruling §2, and copying create_staff's structure (reduce.rs:4102:4125) including its if self.graph.is_some() guard — base-free reduction has no universe to check against and MUST NOT enforce these:

Op Precondition Failure reason
CreateStaffGroup every members[i] is a live Staff TargetMissing
CreatePartDefinition every staves[i] is a live Staff TargetMissing
CreateAnalysisLayer (none — no outbound references)
CreateView every active_layers[i] is a live AnalysisLayer TargetMissing

Reuse TargetMissing (discriminant 0). Add no new PreconditionFailureReason — that space stays at 015 until G3b.

Mint preconditions are CreateStaff's exactly: a live id re-carried with a byte-identical value is NoOp { AlreadyApplied }; a live id with a differing value is NoOp { PreconditionFailedUnderReduction { RecreateContentMismatch } }; a tombstoned id is NoOp { TargetTombstoned }.

Pin 4a — byte-identical re-carry needs a carried-value map, at seven sites each

The first draft of pin 4 promised re-carry idempotence without the machinery that makes it work, which would have shipped the promise and not the property. Comparing "the same value" requires retaining the value; the object map holds only Live/Tombstoned.

Exactly three such maps exist today — staff_values, time_signature_values, instrument_values (reduce.rs:999:1007) — and each is threaded through seven sites. G3a adds four more (staff_group_values, part_definition_values, analysis_layer_values, view_values), so this is 28 touch points, none of which appeared in the first draft's touch table:

# Site Existing lines
1 reducer state declaration reduce.rs:999:1007
2 WorkingSnapshot declaration :1106:1108
3 initialization :1388:1390
4 base seeding in seed_from_graph :1437, :1445, :1460
5 mint insertion in the reducer :4134, :4183, :4219
6 snapshot :7637:7639
7 restore :7677:7679

Site 4 is the one that fails silently. Without a base seed, a re-carry against an entity that came from the base score — not from the log — finds no retained value and misclassifies. G1 already documents this exact hazard and its mutation for instrument_values (reduce.rs:13264:13268); copy that shape. Each of the four omitted seeds must be killed separately (t13), and a base-recarry test must exist (t5b) — a re-carry test that only ever reduces from empty cannot see a missing seed at all.

The packet is self-contained: CreateView's precondition target is minted by CreateAnalysisLayer in this same packet, so the ordering is testable end-to-end without a base score.

Pin 4b — §1.1's ruling MUST land on the normative surfaces, not only here

A normative ruling that lives only in a contract and a candidate ledger is not normative. §1.1 assigns authority; until the specification and the field declarations say so, the normative record stays ambiguous and every reader outside this packet is entitled to the opposite reading. Pin 3's "nothing else in core" is therefore narrowed to the codec surface only — §1.1 adds a documentation obligation to graph.rs, and pin 8's four-document ritual gains two specific normative additions.

Required, all four:

Site What it must say
core/src/graph.rs:848Staff.group Currently has no doc comment at all. Write one: this field is the sole authority for group membership.
core/src/graph.rs:1650StaffGroup.members Currently has no doc comment at all. Write one: a non-authoritative denormalized projection; MUST NOT be read to decide membership; may be stale in both directions.
core_spec.tex — the Staff/StaffGroup declarations (:5564, :4231) The same authority rule, normatively. This is the document that declared both fields without ranking them, so this is where the ambiguity actually lives.
operation_catalog.tex — the new CreateStaffGroup section and the existing CreateStaff section (:1104) Explicit stale-form semantics: both the missing and the spurious form are permitted outcomes, named as such, with the authoring orders that produce them. CreateStaff needs it too — it is the operation that creates the missing form, and its section currently promises nothing about the projection.

Cross-reference P13-S16 from each, so a reader who meets the disagreement finds the ruling rather than filing it again.

Pin 5 — every precondition must correspond to an existing invariant-10 check

Invariant 10's body already resolves a staff's group, a group's members, a part's staves, and a view's active layers (core/src/invariants.rs:1122:1156). The reducer's new preconditions and that checker must agree: a score reduced from empty through these operations MUST pass check_invariants. This is the oracle, and it is stronger than any assertion the reducer can make about itself.

Pin 6 — the invariant-10 prose reconciliation (§6.3), and it is doc-only

Invariant 10's doc comment (invariants.rs:59:62) names only cross-cutting structures and event-internal references. Its body checks materially more: the four reference classes above, plus measure and grid time-signature references (:1180:1212). G3a repairs the doc comment to describe what the check actually enforces.

No enum entry, no discriminant, no behaviour change, no all() count change. GraphInvariant stays at 19. It does not reach the wire — no reference from epiphany-ops or epiphany-bundle — so this is not a schema event. Invariant 20 is G3b's.

Pin 7 — G-minor interaction: all four kinds carry epoch 11

introduced_minor() returns Option<u16> and has no wildcard arm by design, so a new variant cannot compile without an epoch. All four take Some(11) — one epoch for one additive event, per the ratified policy (spec/PLAN_GMINOR_SCHEMA_MINOR.md §4); G2a's precedent put two kinds at the single epoch 9.

The sentinel must not be 0. 0 is a real baseline minor for V1V3.

Two sites, both required: the @ Some(11) annotations in the tag vocabulary (payload.rs:714 region) and the ratified-table transcription in test s1 (payload.rs:2475 region). An epoch omitted from the s1 table is an epoch that test cannot see go wrong — that comment is already in the file; honour it.

Append the epoch-11 row to spec/PLAN_GMINOR_SCHEMA_MINOR.md §4's ladder, naming the introducing commit once it exists.

Pin 8 — the four-document append ritual applies in full

An operation-vocabulary append is a documented event in four specification documents. G1 shipped five normative falsehoods by declaring them out of scope; that is not repeatable.

Two of the four carry pin 4b's normative additions on top of the routine sweep: core_spec.tex gains the authority rule at the Staff/StaffGroup declarations, and operation_catalog.tex gains explicit stale-form semantics in both the new CreateStaffGroup section and the existing CreateStaff section (:1104). Neither is optional, and neither is satisfied by the routine append alone.

  • operation_catalog.tex — a \section per kind (four), version bump, changelog paragraph.
  • binary_format.tex — payload-layout and tag rows per kind, version bump, Revision History row.
  • core_spec.tex — the normative OperationKind/OperationKindTag listings and the spelled-out payload counts.
  • text_projection.tex — four new kind productions are a document-surface change, so COMPANION_VERSION bumps 0.11.0 → 0.12.0, re-sweeping five live version sites plus a changelog row and re-flipping the negative superseded_companion_version vector so it rejects 0.11.0. The tree is already at 0.11.0 — G2b bumped it (textproj/src/lib.rs:47 and its doc comment). An earlier draft of this contract repeated G2b's 0.10.0 → 0.11.0, which would have re-declared the current version as new and left the negative vector rejecting a version no longer superseded.

Use \sectionsc{...} for cross-document references. \ref cannot cross documentsoperation_catalog.tex shipped an undefined reference that way.

Regenerate all four PDFs; they are tracked.

Pin 9 — explicit non-goal: G3a authorizes no pruning or compaction

The standing prohibition holds and has had real teeth since G2b: pruning would discard authored genesis state, not merely re-derivable state. G3a adds four more authored families to that surface. Blocked on disposition C.

Pin 10 — P13-S15 stays open, and these kinds stay outside the golden lock

The [(OperationKind, u8); 30] golden lock (payload.rs:2011) ends at discriminant 29. Kinds 3538 are outside it, exactly as 3034 already are. Do not extend the lock in this packet. P13-S15 lands as its own rung with its own mutation evidence; widening it here would ship the extension without that evidence.


3. Touch table

Derived from git show 3b09595 --name-only (G1, the closest mint precedent) and git show 13c3d2f --name-only (G2b), minus what pins 2 and 3 exclude. Every line number below re-verified against the working tree 2026-07-29.

Core

File What
crates/epiphany-core/src/codec.rs four canonical_value! entries (:3518 list)
crates/epiphany-core/src/invariants.rs pin 6: invariant-10 doc comment (:59:62)
crates/epiphany-core/src/graph.rs pin 4b: write the authority doc comment on Staff.group (:848) and the projection doc comment on StaffGroup.members (:1650) — neither field is documented today
crates/epiphany-core/DECISIONS.md the rung's record

Not touched: textvalue_graph.rs (pin 3), lib.rs (already exported). No type changes anywhere in core — graph.rs gains documentation only, per pin 4b; the four carried types already exist unchanged.

Ops

File What
crates/epiphany-ops/src/payload.rs four op structs + CanonicalEncode; OperationKind variants; discriminant() (:390 region); schema_major()no arm, pin 2; introduced_minor() (:449 region); tag() (:497 region); encode_canonical (:543 region); tag vocabulary @ Some(11) (:714 region); s1 epoch table (:2475 region)
crates/epiphany-ops/src/envdecode.rs decode arms (:599 region) and the tag-dispatch arms (:901 region), plus validation
crates/epiphany-ops/src/reduce.rs four dispatch arms + four mint reducers, on create_staff's shape (:4075, :4148); plus four carried-value maps at seven sites each — 28 touch points, see pin 4a
crates/epiphany-ops/src/textproj_kind.rs production arms (:232 region) and parse arms (:572 region)
crates/epiphany-ops/src/migrate.rs both directions (:192, :356 regions)
crates/epiphany-ops/src/v0.rs V0OperationKind variants (:118 region)
crates/epiphany-ops/src/fuzz.rs generator arms (:304 region)
crates/epiphany-ops/src/valuegen.rs value generators
crates/epiphany-ops/src/vectors.rs four envelope decode vectors, pinned to literal bytes (trap 4)
crates/epiphany-ops/src/lib.rs re-exports (:135 region)
crates/epiphany-ops/DECISIONS.md the rung's record

Boundary crossings — budgeted up front (trap 6)

An OperationKind append is not containable to core + ops. Six crossings: one exhaustive-match site plus five literal/prose sentinels. All six re-verified 2026-07-29; earlier revisions of the plan carried three drifted citations, and earlier revisions of this contract alternated between five and six by counting the exhaustive-match site inconsistently.

The two classes fail differently, which is why they are counted together but named apart. The exhaustive-match site fails loudly — the workspace will not compile. The five sentinels fail silently: each stays green while meaning something narrower than it says.

File What Why it bites
crates/epiphany-editor-core/src/barriers.rs four arms in subjects_of (:313, pattern at :444) Rust exhaustiveness; testkit depends on editor-core, so a missing arm blocks conformance and requirement_labels — the gate cannot run at all
crates/epiphany-layout-ir/src/barrier.rs the "one past the vocabulary" tag 3539 (:1156, assertion at :1170/:1176) and its comment Deliberately a literal; unbumped, it pins a bug — a barrier prohibiting a new op encodes fine and cannot read back
crates/epiphany-testkit/tests/text_projection_grammar.rs count 3539 and the message string (:315) Hand-maintained literal parallel to a derived list
crates/epiphany-testkit/src/generators.rs drawn range 30..=3430..=38 (:1908) and the never-drawn guard (:1947) A kind never drawn is a kind never fuzzed
crates/epiphany-testkit/src/layout_stub.rs the 30..=34 range in the s10/row-29 comment (:1373) Prose, but it states the coverage claim the test rests on; stale text here is how a narrowed guard reads as a broad one
crates/epiphany-textproj/src/vectors.rs the negative vector whose "wrong version" moves with each bump Silently passes for the wrong reason otherwise

Both barriers.rs and barrier.rs are editor-track files. Authorization is per-packet and does not generalise. Granted 2026-07-29, narrowly:

  • barriers.rsthe four required exhaustive subjects_of arms, and nothing else.
  • barrier.rsthe invalid-tag literal, its comment, and its assertions, 35 → 39, and nothing else.

No other change in either crate is authorized by this packet. If the workspace appears to need one, stop and report rather than widening.

Text projection

File What
crates/epiphany-textproj/src/lib.rs COMPANION_VERSION 0.11.0 → 0.12.0 (:47) and the live version sites
crates/epiphany-textproj/src/parse.rs kind productions
crates/epiphany-textproj/src/vectors.rs four positive document vectors + the negative-vector flip

Bind vector sources by name, never by positional index. Inserting a document repointed positionally-bound negative vectors in G2b and broke generation. by_name(...) exists for this.

Do not conflate version domains. A corpus fixture's manifest_schema_version is the manifest's version, not the epoch a block requires. G2b shipped SchemaVersion::new(0, 10) here with a comment making exactly the inference text_projection.tex:1367 forbids. Use SchemaVersion::V0.

Normative documents (pin 8)

spec/operation_catalog.tex + .pdf, spec/binary_format.tex + .pdf, spec/core_spec.tex + .pdf, spec/text_projection.tex + .pdf.

Vectors and tracking

spec/vectors/decode_vectors.txt, spec/vectors/textproj_document_vectors.txt (regenerated; any hardcoded corpus count moves with them), spec/PLAN_GENESIS_OPS.md, spec/PLAN_GMINOR_SCHEMA_MINOR.md §4.


4. Tests — each with the mutation that must kill it

Each row names a mutation that MUST be observed failing and then reversed by editing back — never git checkout, never git stash. A mutation that does not compile produces no test output and signs nothing. A mutation absorbed by the compiler (e.g. deleting a match arm) proves nothing about the test: prefer a mutation that keeps the workspace compiling and isolates the behaviour under test. A shared mutation that leaves a test green signs nothing — the t9 lesson from G2b.

# Test Mutation that must kill it
t1 All four kinds and tags are 3538 in both spaces, and the discriminant byte leads each canonical encoding Move any one kind to 39; then, separately, move its tag. Both must fail — the spaces are asserted independently (pin 1)
t2 schema_major() returns 0 for all four Add them to the => 2 arm; must fail (pin 2's stated bug)
t3 A block containing all four stamps major 0, and the op-block accept-set is untouched at 3 Make one kind report major 2; must fail
t4 Each op round-trips through encodeenvdecode → reduce, byte-identical, with its decode vector pinned to literal bytes Four independent mutations, one per carried type — swap two fields in each struct_codec! declaration: PartDefinition { id, name, staves }{ id, staves, name } (core/src/codec.rs:1790), and likewise AnalysisLayer (:1791), ViewDefinition (:1792), StaffGroup (:2329). Each type's literal-byte vector must fail on its own. These are four separate layouts; one mutation signs one of them. Collapse to a single mutation only if the implementation consolidates all four behind one shared mechanism, and say so explicitly if it does. (A create op's encode_canonical is a single push_lp_bytes line over one carried value, so there is nothing in the op to reorder — the first draft's mutation was impossible. The reorder must go where the layout actually lives, and because struct_codec! moves both halves at once it is self-consistent and passes round-trip tests: trap 4, the 3b-i lesson. Literal-byte vectors are the only thing that catches it.)
t5 Re-carrying a live id with a byte-identical value is AlreadyApplied; with a differing value is RecreateContentMismatch; a tombstoned id is TargetTombstoned Return Applied for the differing-value case; must fail
t5b Re-carry against a base-derived entity: reduce new_onto a score whose four vectors are already populated, re-carry each byte-identically, and get AlreadyApplied Covered by t13 per-seed. A re-carry test that only reduces from empty cannot see a missing base seed at all — that is why this row is separate from t5
t6 All three referential loops refuse under a graph, each asserted separately: CreateStaffGroup.members, CreatePartDefinition.staves, and CreateView.active_layers naming a non-live target are each TargetMissing Three independent mutations, one per loop: drop the members loop from create_staff_group; drop the staves loop from create_part_definition; drop the active-layers loop from create_view. Each must fail on its own row. (The first draft omitted PartDefinition.staves entirely — one uncovered loop is one loop that can be deleted green.)
t7 Those same preconditions are not enforced base-free Remove the if self.graph.is_some() guard from one reducer; must fail — base-free has no universe to check against
t8 From empty, CreateInstrumentCreateStaffGroupCreateStaff with group: Some(...) succeeds and reaches a note — i.e. CreateStaff's group precondition becomes satisfiable. Claim narrowed per §1.1: this asserts satisfiability, NOT that a bidirectionally consistent group is authorable Replace the CreateStaffGroup dispatch arm with OperationKind::CreateStaffGroup(_) => OperationEffect::Applied, keeping the match exhaustive; must fail at the grouped-staff assertion while the spine stays applied
t8b Both permitted stale forms are pinned (§1.1): the missing form (CreateStaffGroup(g, [])CreateStaff(s, Some(g))s.group == Some(g), g.members == []) and the spurious form (CreateStaff(s, None)CreateStaffGroup(g, [s])g.members == [s], s.group == None). Both asserted to hold, as states the ruling permits Two mutations, each in the reducer that actually runs second in its order. Missing form → mutate create_staff: when group is Some(g), append the newly minted staff to g.members (disposition A's maintenance rule); the missing-form assertion must fail. Spurious form → mutate create_staff_group: reject or normalize away a non-empty carried members; the spurious-form assertion must fail. (An earlier draft assigned these the other way round, which is impossible in both directions: create_staff_group runs first in the missing order and cannot append a staff that does not exist yet, and create_staff runs first in the spurious order and has no later group to repair. A mutation in the operation that runs first cannot reach the state the assertion names.) A test pinning only one order leaves the other free to change silently
t9 A score reduced from empty through all four ops passes check_invariants, and each skipped reducer check independently makes invariant 10 fire (pin 5) The fixture is constant across all mutations: the baseline input already attempts a dangling reference in each of the three loops, and passes because the reducer refuses them. Then mutate only production, one skipped check at a time — three separate mutations, each of which must let its dangling reference through and make invariant 10 fire. (The first draft moved the fixture and the production code together, which proves only that a hand-built bad score fails a checker — not that the reducer's refusal is what was holding the line.)
t10 All four kinds carry epoch 11, and a block containing them stamps minor 11 Assign epoch 10 to one kind; must fail. Run against both epoch sites separately (vocabulary annotation, s1 table) — each must be independently able to fail
t11 Text projection round-trips all four kinds, and the companion version is 0.12.0, with the negative vector rejecting 0.11.0 Drop one parse arm; must fail. Separately, leave COMPANION_VERSION at 0.11.0; the negative vector must fail
t12 Invariant 10's doc comment names the four reference classes its body checks (pin 6) Grep-assert the repaired prose is present within the invariant-10 doc block only — slice the source from the /// 10. line to the CrossCuttingRefsResolve, line and search that. Revert the comment to see it fail. Searching the whole file passes on the implementation body, which contains the same identifiers the doc comment is supposed to gain — a guard that cannot fail
t13 Each of the four base seeds is load-bearing (pin 4a, site 4) Four separate mutations: skip seeding each of staff_group_values, part_definition_values, analysis_layer_values, view_values in seed_from_graph, one at a time. Each must make t5b fail. Model on G1's documented precedent for instrument_values (reduce.rs:13264:13268)
t14 The authority rule reached the field declarations (pin 4b): Staff.group's doc comment states it is authoritative, and StaffGroup.members' states it is a non-authoritative projection that may be stale in both directions Grep-assert each, slicing to that field's doc block only — same discipline as t12, since graph.rs mentions both identifiers throughout and a file-wide search cannot fail. Delete either doc comment to see the corresponding assertion fail. (The .tex halves of pin 4b are not machine-checkable here; they are gate items, reported explicitly.)

On t12's grep shape: a self-matching needle is a real hazard — G2b hit it twice, once when a multi-line needle matched the test's own source and once when the assertion message contained the searched phrase. Keep the needle short, keep it out of the message, and if the guard reads more than one file, iterate include_str! over each.


5. Gate

  • cargo test --workspace — full pass, zero failures, with the count reported.
  • cargo clippy --workspace --all-targets — zero warnings.
  • cargo fmt --check — clean.
  • git diff --check — clean.
  • All four PDFs regenerated; no undefined LaTeX references.
  • Every t-row mutation observed failing and reversed by editing back, with the observed failure quoted. Not "would fail".
  • Stage only the files in §3's touch table, explicitly named. Never git add -A. The editor track has parallel work in spikes/ and elsewhere that MUST NOT be staged.

6. Report

State, with evidence: kinds/tags assigned and the two spaces asserted separately; that schema_major() gained no arm and no bundle file was touched; the four canonical_value! entries; that textvalue_graph.rs needed no change; the four carried-value maps at all seven sites each, named site by site; each of the three referential loops and its invariant-10 correspondence; that §1.1's ruling reached all four pin-4b sites — both field doc comments, core_spec.tex, and both operation_catalog.tex sections — quoting the text written at each, and what t8 therefore does and does not claim; epoch 11 at both sites; the companion bump 0.11.0 → 0.12.0 with the negative vector rejecting 0.11.0; the four-document sweep; and the six boundary crossings — the one exhaustive-match site and the five sentinels, each with its new value. Report each mutation's observed output — t6, t9, and t13 are multi-mutation rows and each sub-mutation must be reported separately. Anything not done, say so plainly.