epiphany/spec/CONTRACT_GENESIS_G3A_ENTITI...

525 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 `Codec``struct_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:848``Staff.group` | **Currently has no doc comment at all.** Write one: this field is the **sole authority** for group membership. |
| `core/src/graph.rs:1650``StaffGroup.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
documents** — `operation_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 `35``39` (`: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 `35``39` and the message string (`:315`) | Hand-maintained literal parallel to a derived list |
| `crates/epiphany-testkit/src/generators.rs` | drawn range `30..=34``30..=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.rs` — **the four required exhaustive `subjects_of` arms, and
nothing else.**
* `barrier.rs` — **the 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 `encode``envdecode` → 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, `CreateInstrument``CreateStaffGroup``CreateStaff` **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.