# Contract: genesis tranche G1 — `CreateInstrument`, and the from-empty spine Repo root `/home/jeans/Repos/active/epiphany`. Governed by `spec/RULING_GENESIS_PERSISTENCE.md` (011c68a; `identity` sub-decision ruled at ec17d06) and `spec/PLAN_GENESIS_OPS.md` §4 (439e1e2), whose G1/G2/G3 ladder the user ratified 2026-07-24. Execution model as every tranche on this track: Sonnet subagent, coordinator line-level review with **independent mutation re-runs**, user deep-dive at contract sign-off and final report. Mutation discipline throughout: anchor-assert before substituting, restore by reversing, never `git checkout`. **Parallel safety.** The editor track owns `epiphany-editor-core`, `epiphany-editor-gui`, `epiphany-layout-ir`, `epiphany-render-svg`, the new `epiphany-glyphs` crate, every `crates/epiphany-editor-gui/goldens/*.png`, and `spec/PLAN_EDITOR_APP.md` + `spec/CONTRACT_EDITOR_*.md`. **This packet touches none of them.** Do not stage them; do not `git add -A`. --- ## What this packet does, in one sentence Adds one operation kind, `CreateInstrument`, so that a `Score::empty(identity)` plus an operation log can materialize a note-bearing score — and makes the from-empty path correct in the two ways the ruling requires. ## Why it is this small `CreateInstrument` is **the single missing link** between an empty score and a note. `Score::empty` (`core/src/graph.rs:1747`) yields `Canvas::default()` and empty vectors; from there the chain is ``` Score::empty → CreateInstrument (MISSING) → CreateStaff ✓ → CreateRegion ✓ → CreateStaffInstance ✓ → CreateVoice ✓ → InsertEvent ✓ ``` Every later arrow exists today with graph-aware preconditions. `CreateStaff` already requires a **live `Instrument`** (`reduce.rs:3824-3833`) — which nothing can currently create. That is the whole gap. `Instrument` (`core/src/graph.rs`, fields `id, name, range, abbreviation, sound_config, transposition, default_clef, default_staff_lines, unpitched_members`) holds **no outbound entity references**. It is a root, so this operation needs **no referential preconditions** — only mint/idempotence. ## Design pins 1. **No wire layout is designed.** `Instrument` already has a `Codec` and already ships inside `Score`. Add `Instrument` to `canonical_value!` (`core/src/codec.rs:3484`) and encode the payload as `push_lp_bytes(out, &self.instrument.canonical_bytes())` — the `CreateStaffOp` template verbatim (`ops/src/payload.rs:1411`). The generated `decode_canonical` already rejects non-canonical encodings, so strict-form enforcement is inherited, not written. 2. **Discriminants: kind `31`, tag `31`.** Both spaces currently top out at 30 (`TransposeInterval`). **They coincide here by accident, not by rule** — the two spaces are independent and misaligned elsewhere (`RespellPitch` is kind 2 / tag 3; `CreateVoice` is kind 19 / tag 20). Append-only under `req:binfmt:kind-discriminants`. 3. **Name it `CreateInstrument` in both spaces**, catalog name `"create-instrument"`. The tag layer's older Create→Insert convention (`InsertStaff` for kind `CreateStaff`) is **not** followed: the two most recent additions, `CreateVoice` and `CreateRepeatStructure`, use `Create` in the tag, and a gratuitous homonym costs more than the convention buys. 4. **`schema_major()` is unconditionally `2`.** `Instrument`'s major-2 appends (`sound_config`, `default_clef`, `default_staff_lines`) are **mandatory, not `Option`-hidden**, so no lower-major layout for this payload exists. Put it in the existing `| OperationKind::CreateStaff(_) | OperationKind::SetMetadata(_) => 2` arm (`payload.rs:219`). **Do not** copy the value-dependent arm shape used by `CreateRegion`/`SetStaffLayout`. 5. **No accept-set change.** `max_supported_major(OperationEnvelopeBlock)` is already 2 (`bundle.rs:69`) and **stays 2**. Do not touch `epiphany-bundle`. The raise to 3 belongs to G2 (`SetTuningContext`) and to nothing else. 6. **Reduction follows `create_staff` exactly** (`reduce.rs:3794-3855`): live + byte-identical value → `NoOp{AlreadyApplied}`; live + differing value → `NoOp{PreconditionFailedUnderReduction{RecreateContentMismatch}}`; tombstoned → `NoOp{TargetTombstoned}`; otherwise `mint_container` + `Applied`. 7. **No `DeleteInstrument`.** `CreateStaff` ships today with no `DeleteStaff`; the ruling states full CRUD is not automatically owed. Delete coverage is G3's design work. ## The two cross-cutting items These are the reason G1 is not merely "one more kind". 8. **`instrument_values` must exist and be seeded.** `TypedObjectId::Instrument` liveness is already seeded from the base (`reduce.rs:1319-1321`), but there is **no value map**, so re-carry idempotence against a *base* instrument has nothing to compare. Add `instrument_values: BTreeMap` mirroring `staff_values` at every one of its sites: the two struct decls (`:914`, `:1004`), the init (`:1282`), the base seed (beside `:1327`), and the snapshot/restore pair (`:7234`, `:7270`). **Missing the snapshot/restore pair is the silent failure here** — it would surface only under undo/replay. 9. **The identity cursor** (ruling §3). From-empty reduction sets `next_counter` to `1 + max(counter)` over ids authored by the reducing replica in the log, leaving the seed untouched when that replica authored none. This is **the first time reduction writes `identity` at all** — `epiphany-ops` has no `.identity` reference today. Note `Score::identity` is an authoring cursor reduction has never advanced; under from-empty it would otherwise sit at the seed while the log already holds that replica's ids at `0..N`, so minting from a reduced score re-issues used counters. Nothing catches that: invariant 11 (`core/src/invariants.rs:1746-1752`) checks only the reserved replica, never the counter. 10. **From-empty must reduce with a graph.** Use `reduce_operation_set_onto(&set, &Score::empty(identity))` (`reduce.rs:617`), **never** `reduce_operation_set(&set)` (`:612`). Graph-aware preconditions are skipped in the base-free mode *by design* — it has no universe to check against (`reduce.rs:3721`, `:3824`). A from-empty document through the base-free entry point silently loses referential enforcement. Document this at both entry points. ## Touch points Derived by enumerating every `CreateStaff` site. Twelve non-test surfaces — **two of which the plan under-counted** (`v0.rs`, `migrate.rs`): | # | File | Site (CreateStaff analogue) | |---|---|---| | 1 | `core/src/codec.rs` | `canonical_value!` — add `Instrument` (`:3484`) | | 2 | `ops/src/payload.rs` | `CreateInstrumentOp` struct + `CanonicalEncode` (`:1400`, `:1411`) | | 3 | `ops/src/payload.rs` | `OperationKind` variant (`:177`) | | 4 | `ops/src/payload.rs` | `schema_major()` → 2 arm (`:219`) | | 5 | `ops/src/payload.rs` | `discriminant()` → 31 (`:280`) | | 6 | `ops/src/payload.rs` | `tag()` (`:323`) + encode dispatch (`:367`) | | 7 | `ops/src/payload.rs` | `operation_kind_tag_vocabulary!` entry (`:440`) — **compile-enforced** | | 8 | `ops/src/envdecode.rs` | discriminant-31 decode (`:542`) + tag→kind (`:830`) | | 9 | `ops/src/v0.rs` | `V0OperationKind` variant (`:96`) | | 10 | `ops/src/migrate.rs` | both directions (`:171`, `:325`) | | 11 | `ops/src/reduce.rs` | dispatch (`:2632`) + `create_instrument` + pin 8's map | | 12 | `ops/src/textproj_kind.rs` | production (`:185`) + parse (`:470`) | | 13 | `ops/src/fuzz.rs` | generator arm (`:214`) | | 14 | `ops/src/vectors.rs` | a decode vector for the new payload | | 15 | `spec/operation_catalog.tex` | §`CreateInstrument` | The tag vocabulary macro (#7) is a **single source of truth**: the decoder, fuzz corpus, conformance vectors, and edit-barrier round-trip all read `PAYLOAD_FREE`, and a variant missing an entry **fails to compile**. It exists because Push 4a added `TransposeInterval` to a hand-written match and nothing else, and four hand-maintained lists stayed green while the decoder rejected its own encoding. `OperationKind::discriminant()` (#5) has **no such guard** — it is hand-written. ## Tests + minimum mutations Every test must be mutation-verified: re-introduce the bug it exists to catch and show it dies. A test that cannot see its own bug is not a test. * **(i1) the spine reaches a note.** From `Score::empty(identity)`, operations only — `CreateInstrument` → `CreateStaff` → `CreateRegion` → `CreateStaffInstance` → `CreateVoice` → `InsertEvent` — materializes a note-bearing `Score`. No fixture, no base. *This is the packet's load-bearing test and the ruling's acceptance criterion 1.* **Mutation:** drop the `CreateInstrument` envelope → `CreateStaff` must `NoOp{TargetMissing}` and no note appears. * **(i2) re-carry idempotence.** The same `CreateInstrument` twice → `AlreadyApplied`, and the state is byte-identical. **Mutation:** make the second carry differ in one field → `RecreateContentMismatch`. * **(i3) re-carry against a *base* instrument** — the pin-8 case. Reduce onto a base that already contains the instrument. **Mutation:** skip seeding `instrument_values` → the byte-identical re-carry is misreported as a mismatch. * **(i4) order independence.** Two replicas applying a concurrent genesis-era set in opposite delivery orders converge to byte-identical `MaterializedState` (ruling acceptance criterion 2; the existing pattern is `reduce.rs:10031`). **Mutation:** perturb one op's reduction order. * **(i5) the identity cursor.** Mint from a reduced-from-empty score and assert the id collides with nothing in the log. **Mutation:** leave `next_counter` at the seed → collision. * **(i6) base-free loses enforcement.** Assert explicitly that `reduce_operation_set` (base-free) does **not** enforce the instrument precondition while `reduce_operation_set_onto` does. This test documents a designed asymmetry rather than guarding a bug — state that in its doc comment so a later reader does not "fix" it. * **(i7) minimal stamping.** `CreateInstrument.schema_major() == 2` *unconditionally* — including for an instrument whose optional fields are all `None`. Extend `schema_majors_follow_the_minimal_stamping_rule` (`reduce.rs:10386`). **Mutation:** make the arm value-dependent → dies. * **(i8) decode vector pinned to literal bytes**, not round-trip. Round-trip locking cannot see a self-consistent encoder/decoder reorder — that is the 3b-i lesson, where a swap applied to both halves passed 1283 tests and 8/8 conformance. * **(i9) text-projection round-trip** for the new kind, matching the existing per-kind coverage. ## Blast radius `crates/epiphany-core/src/codec.rs` (one `canonical_value!` line) and its `DECISIONS.md`; `crates/epiphany-ops/src/{payload,envdecode,v0,migrate,reduce,textproj_kind,fuzz,vectors}.rs` and its `DECISIONS.md`; `spec/operation_catalog.tex` (+ its rebuilt PDF — every other `.tex` commit carries one). **Nothing else.** No `epiphany-bundle`, no `binary_format.tex`, no editor-track crate, no golden re-blessing. ## Gate (report actual output, never stale numbers) `cargo fmt --check`; `cargo clippy --workspace --all-targets` at **0** warnings; `cargo test --workspace` at **0** failed; `cargo test --doc` at 0 failed; conformance **8/8**; `requirement_labels` **6/6** with its three observed counts reported as seen (they were 212/282/282 at 439e1e2 and will move with the catalog section). Plus: * `max_supported_major(OperationEnvelopeBlock)` is **still 2** — assert it, and state it in the report. * No `crates/epiphany-editor-gui/goldens/*.png` byte changes. ## What I will verify independently before committing Build to survive this. I re-run every mutation myself, and I check specifically: that #5's hand-written discriminant match and #7's macro entry agree; that pin 8's snapshot/restore pair (`:7234`/`:7270`) was not missed; that i1 fails for the stated reason rather than incidentally; that the accept-set really did not move; and that no claim in the report is copied forward from this contract rather than observed — the recurring failure mode on this project is a plausible claim propagating because nobody re-derived it. ## Report Files + summary, exact asserted values, every mutation with kill evidence, gate output verbatim, deviations flagged explicitly. If any pin here turns out to be wrong, say so rather than working around it silently.