457 lines
28 KiB
Markdown
457 lines
28 KiB
Markdown
# 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:4119`). 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. **How much of the staff-group half
|
||
it closes depends on §1.1, which is unresolved.**
|
||
|
||
---
|
||
|
||
## 1.1 The pin, UNRESOLVED — `StaffGroup`/`Staff` authorship authority
|
||
|
||
**Must be ratified before dispatch.** 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:4117`). `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:1126`), a group's members are declared
|
||
(`:1135`) — and **agreement in neither**. No agreement check exists anywhere in
|
||
the crate. The specification declares `StaffGroup.members`
|
||
(`core_spec.tex:4231`) and `Staff.group` (`:5578`, "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
|
||
|
||
`Staff.group` is authoritative; `StaffGroup.members` is a denormalized
|
||
projection that **G3a does not maintain**. `CreateStaffGroup` carries `members`
|
||
as given and validates resolution only. Bidirectional agreement is stated as a
|
||
**known, filed gap** — a P13 candidate and a candidate invariant 21 — and G3a
|
||
claims nothing about it.
|
||
|
||
Cost: a from-empty score can hold a grouped staff whose group lists no members.
|
||
That is *incomplete* data under a stated deferral, not silently wrong data —
|
||
but it is real, and the contract must not describe the defect as closed.
|
||
|
||
### Recommendation
|
||
|
||
**Disposition B**, and narrow the claim to match. A is the better end state,
|
||
but it invents an authoritative/derived split the specification does not make,
|
||
in the packet whose entire value is that it moves no bound and adds no
|
||
semantics. Ruling the authority now costs nothing and unblocks A later;
|
||
implementing A here buys a consistency property no consumer reads yet, at the
|
||
cost of turning a mint into a mint-plus-maintenance.
|
||
|
||
**Under either disposition, 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*.
|
||
|
||
---
|
||
|
||
## 2. Design pins
|
||
|
||
### Pin 1 — kinds and tags are 35–38, 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 nothing else 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 0–15 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 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 V1–V3.
|
||
|
||
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.
|
||
|
||
* `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 35–38 are **outside** it, exactly as 30–34 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/DECISIONS.md` | the rung's record |
|
||
|
||
**Not touched:** `graph.rs` (all four types exist), `textvalue_graph.rs` (pin
|
||
3), `lib.rs` (already exported).
|
||
|
||
### 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. All five
|
||
re-verified 2026-07-29; earlier revisions of the plan carried three drifted
|
||
citations.
|
||
|
||
| 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 35–38 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** | **Swap two fields in the carried type's `struct_codec!` declaration** — e.g. `PartDefinition { id, name, staves }` → `{ id, staves, name }` (`core/src/codec.rs:1790`); the literal-byte vector must fail. *(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** | 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. **Claim narrowed per §1.1: this asserts satisfiability, NOT that a bidirectionally consistent group is authorable.** Under disposition B the test must additionally assert the known gap explicitly — `g.members` is empty while `s.group` is `Some(g)` — so the deferral is pinned rather than merely unmentioned |
|
||
| 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`) |
|
||
|
||
**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; **which §1.1 disposition was implemented 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-crossing literals with their new values. 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.
|