# Plan — the genesis operation tranche: scope, ladder, and open rulings **Governed by** `spec/RULING_GENESIS_PERSISTENCE.md` (ratified 2026-07-24, 011c68a; `identity` sub-decision ruled 2026-07-24, ec17d06). That ruling reverses Pass-12 K8 and makes every mutable field of `Score` operation-authored. This plan is the execution scope: what the tranche touches, in what order, and which questions must be answered before a dispatch contract can be written. **Status:** **G1 landed** (3b09595, CI green) via `spec/CONTRACT_GENESIS_G1_INSTRUMENT.md`. **G2a contracted** (`spec/CONTRACT_GENESIS_G2A_SETTINGS.md`); **G-minor**, G2b, and G3 scoped, not contracted. §6 lists what still needs ratification. **G1 shipped documentation debt** — five normative falsehoods across `binary_format.tex`, `core_spec.tex`, and `operation_catalog.tex`, because its contract declared the normative wire surfaces out of scope. G2a repairs them. The lesson generalises: **an operation-vocabulary append is a documented event in four specification documents**, and any contract on this track that does not name them all is wrong. See §4's split-cost accounting. --- ## 1. The mechanism, and why the tranche is tractable Every one of the nine surfaces carries a `Score` field whose type already has a `Codec` in `epiphany-core`. `Codec` is `pub(crate)`, so operations cannot use it — but they do not need to. The established seam is `CanonicalValue` (`codec.rs:3443`), and the template is one line: ```rust impl CanonicalEncode for CreateStaffOp { fn encode_canonical(&self, out: &mut Vec) { push_lp_bytes(out, &self.staff.canonical_bytes()); // payload.rs:1411 } } ``` `canonical_value!` (`codec.rs:3455`) implements the trait by delegating to the existing `Codec`, **introducing no new byte layout**, and its generated `decode_canonical` already does decode → `finish()` → re-encode → reject on mismatch. So every new operation payload gets strict canonical-form enforcement for free, on the same seam the decode-vector corpus uses. Concretely: the tranche adds the eight remaining carried types to `canonical_value!` and writes one `push_lp_bytes` line per op. It does **not** design wire layouts. Every layout it carries is already frozen and already shipping inside `Score`. ## 2. Two facts that de-risk this, and one that does not **The tag layer is now safe.** `operation_kind_tag_vocabulary!` (`payload.rs:440`) is a single source of truth generating the discriminant map, its inverse, and `PAYLOAD_FREE` — which the decoder, fuzz corpus, conformance vectors, and edit-barrier round-trip all read. A variant added to `OperationKindTag` without an entry **fails to compile**. This macro exists because Push 4a added `TransposeInterval` to a hand-written match and nothing else: the decoder rejected its own encoding, an edit barrier naming it could not be reopened, and four hand-maintained lists — two asserting the tag was *unknown* — stayed green. **Canonical-form enforcement is inherited, not written.** Per §1. **But `OperationKind::discriminant()` is still a hand-written match** (`payload.rs:253`), and it is a *different* space from the tag space. They are not aligned and must not be assumed so — `RespellPitch` is kind 2 and tag 3; `InsertEvent` is 0 in both. The tag space reserves 16 for `Registered`; the kind space does not. Both are append-only under `req:binfmt:kind-discriminants`. Next free: **kind 31, tag 31.** ## 3. The nine surfaces, and what each costs Only one of the nine forces the accept-set raise. Verified by reading each carried type's fields for mandatory (non-`Option`) appends above major 0: | Surface | Op | Carried type | `schema_major()` | Raise? | |---|---|---|---|---| | `instruments` | `CreateInstrument` | `Instrument` | **2** (mandatory `sound_config`, `default_clef`) | no | | `canvas.layout_defaults` | `SetCanvasLayoutDefaults` | `CanvasLayoutDefaults` | 0 | no | | `spelling_precedence` | `SetSpellingPrecedence` | `SpellingPrecedence` | 0 | no | | `staff_groups` | `CreateStaffGroup` | `StaffGroup` | 0 | no | | `parts` | `CreatePartDefinition` | `PartDefinition` | 0 | no | | `analysis_layers` | `CreateAnalysisLayer` | `AnalysisLayer` | 0 | no | | `views` | `CreateView` | `ViewDefinition` | 0 | no | | `StaffInstance.measures` | `CreateMeasure` | `Measure` | 0 | no | | `tuning_context` | `SetTuningContext` | `ScoreTuningContext` | **3** | **YES** | The op-block accept-set is already 2 (`bundle.rs:69`), so **`CreateInstrument` at major 2 costs nothing** — it sits exactly where `CreateStaff` already sits. `SetTuningContext` is the sole surface that drags `OperationEnvelopeBlock` to 3. Each op touches roughly thirteen places: the op struct, the `OperationKind` variant, `discriminant()`, `tag()`, `schema_major()`, `CanonicalEncode`, the tag vocabulary entry, the `envdecode.rs` decode arm and its validation, the `reduce.rs` arm, both `textproj_kind.rs` arms (production `:217`, parse `:523`), `validate.rs`, `vectors.rs`, the fuzz/`valuegen.rs` generators, plus `operation_catalog.tex`. Nine surfaces at thirteen touch points is not one dispatch. ## 4. The ladder ### G1 — the spine (`CreateInstrument` only) **`CreateInstrument` is the single missing link between an empty score and a note.** `Score::empty` (`graph.rs:1747`) gives `Canvas::default()` and empty vectors; from there the chain is ``` Score::empty → CreateInstrument (MISSING) → CreateStaff ✓ → CreateRegion ✓ → CreateStaffInstance ✓ → CreateVoice ✓ → InsertEvent ✓ ``` Every arrow but the first already exists and already has graph-aware preconditions. So one operation satisfies the ruling's acceptance criteria 1 and 3 — a document created empty and given only operations materializes a note-bearing `Score`, and opening it from a bundle reaches a note, which is what unblocks T1b. G1 also carries the two constraints that are not per-surface: * **The identity cursor.** 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 (ruling §3). This is the first time reduction writes `identity` at all — `epiphany-ops` has no `.identity` reference today. Wants a test that mints from a reduced score and asserts no collision with the log. * **From-empty must reduce with a graph.** `new_onto` with an empty score enforces preconditions from the first operation; the base-free mode skips them by design, because it has no universe to check against (`reduce.rs:3721`, `:3824`). A from-empty document through the wrong entry point silently loses referential enforcement. Name it and test it. No accept-set raise. No wire change. Highest value per unit of risk in the whole tranche. ### G2 — the settings setters, split in two All three ride the `SetMetadata` LWW pattern (`reduce.rs:2814`), seeded for value-restoring undo (`:1385`). But they do **not** sit at the same major, and only one of them is a compatibility event. Verified 2026-07-28 against the working tree: * `SetSpellingPrecedence` → `SpellingPrecedence`. Major **0**: the frozen `decode_v0_score`/`decode_v1_score`/`decode_v2_score` walks and the live `Codec` all read this field through plain `Codec::dec` (`codec.rs:2673`, `:3249`, `:3372`) — it has never been versioned. * `SetCanvasLayoutDefaults` → `CanvasLayoutDefaults`. Major **0**. The type is labelled "schema major 1" (`graph.rs:836`), but the versioning lives in the *containing* `Canvas` walk, not the leaf: `dec_canvas_v0` (`codec.rs:2775`) default-fills the whole field while `enc_canvas_v1` (`:3158`) writes it through the live `Codec`. As a standalone payload it has exactly one layout. * `SetTuningContext` → `ScoreTuningContext`. Major **3**, and **unconditionally** so. `enc_tuning_context_v2` (`codec.rs:3293`) writes three fields; the live `Codec` writes five. A default `smufl` and an empty `overrides` still append bytes, so there is no value for which a lower-major layout exists — this is a `CreateInstrument`-shaped arm, not a `CreateRegion`-shaped one. So the split is not tidiness. Two of the three move no wire bound at all, and the accept-set raise is a **one-way door**: once a block can be born at v3, an older reader meeting one preserves the bundle read-only (`bundle/src/error.rs:257`). Spending that in the same packet as two major-0 leaf setters buries it. **G2a — `SetCanvasLayoutDefaults` + `SetSpellingPrecedence`.** Both land in `schema_major()`'s catch-all `_ => 0` arm (`payload.rs:262`) with **no arm added**; adding them to the `=> 2` arm would be the bug. No `epiphany-bundle` change of any kind. **G2b — `SetTuningContext` alone**, carrying the raise, the `bundle.rs` prose, and the S13 close. **Sequenced after G-minor, not immediately after G2a** — see that section: G2b appends kind 34, and the sweep is scoped to 24–33. `bundle.rs:58` documents the current cap **with the tuning-context rationale in prose** — "no operation payload embeds the tuning context, so no op block is ever born at v3". G2b is precisely what falsifies that sentence, so the comment must move with the number. Same for `DECISIONS.md`'s superseded prohibition, which is already marked. **The cost of splitting, stated honestly — and it is larger than first written.** Each packet appends to the operation vocabulary, and that is a documented event in **three** companions, not one: * `text_projection.tex` — a new *kind* production is a document-surface change (the G1 precedent), so `COMPANION_VERSION` bumps per packet: 0.8.0 → 0.9.0 → 0.10.0, each re-sweeping five live version sites plus a changelog row and re-flipping the negative `superseded_companion_version` vector. * `binary_format.tex` — payload-layout and tag rows per kind, plus a version bump and a Revision History row (the 0.2.0 entry is the precedent). * `operation_catalog.tex` — a `\section` per kind, plus a version bump and a changelog paragraph. Plus `core_spec.tex`'s normative `OperationKind`/`OperationKindTag` listings and its spelled-out payload counts, and a regenerated vector corpus, per packet. So the split roughly **doubles the documentation work**, and G2b pays it again in full. Still the cheaper of the two risks — burying a one-way accept-set door in a packet of routine work is worse than repeating a mechanical sweep — but it is not the small tax the first draft implied. **P13-S13 closes at G2b — and on the metadata precedent, not on the canonical base.** The base cannot carry a v3 tuning context: it is role-bound to major 0 (`mis_stamped_canonical_base`, `bundle.rs:866`). It does not need to. The canonical base is a `MaterializedState` (`reduce.rs:504`) — effects, conflicts, anomalies, objects, spellings, breaks, page-breaks, pending — which embeds **no graph values for any field**, including `metadata`, op-authored since M2d and durable purely through its operations. S13's claim was "no canonical carrier embeds it at all: no operation authors it". G2b makes an operation author it, and the op log is canonical. **What that sharpens.** The standing prohibition on pruning (blocked on disposition C) stops being a performance concern the moment G2b lands: pruning would then discard authored genesis state, not merely re-derivable state. G2b's contract must state this as an explicit non-goal. **G2b holdout — `accidental_extensions`, and why a naïve full-value `SetTuningContext` would be wrong.** `ScoreTuningContext`'s `Codec` **deliberately drops** `accidental_extensions` on encode and default-fills it to `Vec::new()` on decode (`core/src/codec.rs:1939`) — the field is staged out of schema major 3 and lands at a later one. Meanwhile `OperationSet::accept` stores the authored envelope **as an object**, not as bytes (`ops/src/opset.rs:70`). So a `SetTuningContext` carrying a non-empty `accidental_extensions` would reduce with those extensions present on the authoring replica, and reduce *without* them on any replica that received the document through serialization — a silent divergence between a live session and the same document reloaded. **`canonical_value!` does not catch this.** Its generated `decode_canonical` compares *bytes* (decode → `finish()` → re-encode → reject on mismatch); it never compares against the originating value, so a field that never reached the bytes is invisible to it. **G2b needs a normalization-or-subset pin before dispatch** — either the payload carries a wire-complete subset type, or the operation normalizes the field away at construction and refuses a non-empty one. Decide that in the G2b contract, not in its implementation. Does not block G2a. ### G-minor — the schema-minor sweep (ruled 2026-07-28) **Ladder order is G2a → G-minor → G2b, and the order is load-bearing.** The sweep is scoped to kinds 24–33, which is exactly what exists once G2a lands. Run G2b first and it appends kind 34 into a vocabulary the sweep has already been scoped against, so either the sweep grows mid-flight or 34 ships with the same defect the rung exists to retire. G2b therefore sequences *after* G-minor and inherits working machinery — the same reasoning that put the accept-set raise in its own packet. `binary_format.tex:2330` requires a writer to raise the chunk schema **minor** when it emits a discriminant appended after the minor it declares — a MUST with a stated rationale, so that an unknown-discriminant decode failure is attributable to version skew rather than corruption. No writer has ever done it: `SchemaVersion::for_major` (`bundle/src/ids.rs:204`) maps a major to a fixed constant — `V0` is `{0, 1}`, not `{0, 0}` (`ids.rs:173`); `V1`/`V2`/`V3` carry minor 0 — and, decisively, **accepts only a major**, so no per-kind additive minor can reach it. Both staging paths likewise derive only the major (`testkit/src/bundle_harness.rs:25`, `textproj/src/serialize.rs:183`). So kinds **24–27**, **28–29**, **30**, and **31** already carry no additive record, and G2a takes that to **32–33** knowingly. Filed as **P13-S14**. Ruled: one retroactive sweep over 24–33 after G2a, rather than blocking G2a on a debt already eight kinds deep or paying for two partial sweeps. What the rung owes — **policy and epoch ladder both ratified 2026-07-28**; see `spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4 and the governing inventory `spec/AUDIT_GMINOR_VOCABULARIES.md`: * a **global additive epoch** (minors 2–9, one per additive event), **not** the per-kind minor this paragraph originally proposed — that was rejected, because it cannot represent multiple independent vocabularies inside one block, and its obvious generalisation is worse: an old `OperationKind` 23 would numerically mask a newly appended `OperationPayload` 3; * `introduced_minor` assigned **per variant, per vocabulary**, co-located with the discriminant in an exhaustive macro/match with **no wildcard arm**, so a new variant cannot compile without an epoch; * an **envelope's** required minor = the max over **every discriminant it actually emits** (outer payload variant, primitive kind, every nested additive variant), and a **block's** = the max over its envelopes; * **content-minimal derivation for the other payload roles** — including the manifest, which takes a barrier tag's epoch when it names one; * a `for_major` replacement that accepts a minor, and both staging paths. **The scope is not "kinds 24–33."** The audit found appends in three further vocabularies (`OperationPayload` 3, `ReanchorReason` 6, `PreconditionFailureReason` 10–15) and a reachability path to `OperationKindTag` through the *manifest* with no operation envelope involved. Orthogonal to the *major* accept-set, which stays 2 through G2a and rises to 3 only at G2b. ### G3 — the remaining entity families `CreateStaffGroup`, `CreatePartDefinition`, `CreateAnalysisLayer`, `CreateView`, `CreateMeasure`, on the `CreateStaff` set-union mint pattern (`reduce.rs:3850`) with byte-identical re-carry idempotence, plus whatever delete/modify coverage §6.1 rules owed. Graph-aware referential preconditions per the ruling §2: * `CreateStaffGroup.members`, `CreatePartDefinition.staves` → live `Staff`s * `CreateView.active_layers` → live `AnalysisLayer`s * `CreateMeasure` → a live `StaffInstance` * deleting an entity with live dependents → refuse (container-not-empty) `CreateMeasure` is shaped differently from its five siblings: `measures` is nested on `StaffInstance` (`graph.rs:611`), not a `Score`-level vector, so its precondition reaches three levels down through `canvas.regions[].staff_instances()`. Consider splitting it out if G3 runs long. ## 5. Traps 1. **Two unaligned discriminant spaces**, one macro-guarded and one not (§2). 2. **`bundle.rs`'s cap comment is prose that encodes a rationale**, not just a number. Moving the number without the prose leaves a confident falsehood in the file that most directly governs accept-sets. 3. **Minimal stamping is a pure function of the value.** `CreateInstrument` is *unconditionally* 2 (its major-2 appends are not `Option`s), unlike `CreateRegion`/`SetStaffLayout`, which are value-dependent. Do not copy the value-dependent arm shape. 4. **Round-trip locking cannot see a self-consistent reorder.** The lesson of 3b-i: a swap applied to both codec halves passed 1283 tests and 8/8 conformance. New payloads want decode-vector entries pinned to literal bytes, not just round-trip tests. 5. ~~**`Score::empty` seeds `tuning_context` with a default**, so `SetTuningContext`'s reduction must distinguish "never authored" from "authored to the default value" if undo is to restore correctly.~~ **Withdrawn 2026-07-28 — this is not a trap, and `SetMetadata` already proves it.** `Score::empty` seeds `metadata` with a default exactly as it seeds `tuning_context` (`graph.rs:1749`, `:1757`), and the base ingest then runs `metadata_chain.seed(score.metadata.clone())` (`reduce.rs:1385`) under a comment stating the purpose outright: the score-level LWW chains seed with the base values so a value-restoring undo of the *first* operational write restores the pre-operational state. Since from-empty reduces **onto** `Score::empty` (trap-free only through `reduce_operation_set_onto` — G1 pin 10), the seed runs, and undoing the first write yields `Restore(Some(Predecessor::Base(default)))`. Restoring the default is correct in both the never-authored and the authored-to-default case, so the distinction is unobservable **and must stay so**. The `Predecessor::Base` vs `::Write` distinction earns its keep only for the canonical bookkeeping families (`spellings`, `breaks`, `page_breaks`, `reduce.rs:707`), where a base predecessor returns a *map key* to absence. `ScoreTuningContext` is an always-valued `Score` field, like metadata: there is no absent state to return to. All three G2 setters copy `set_metadata` structurally. 6. **Adding an `OperationKind` variant is NOT containable to core + ops** — the G1 lesson, and the one claim this plan previously got wrong. Rust exhaustiveness forces an arm in `epiphany-editor-core`'s `subjects_of` (`barriers.rs:437`), and because `epiphany-testkit` depends on editor-core, a missing arm blocks conformance *and* `requirement_labels` — the gate cannot run at all. Three further sites bake in a literal that only surfaces once the workspace compiles: `layout-ir/src/barrier.rs:1105` (a tag "one past the vocabulary"), `testkit/tests/text_projection_grammar.rs:307` (a hardcoded kind *count*), and `textproj/src/vectors.rs` (a negative vector whose "wrong version" is the one each bump moves to). Every G2/G3 contract MUST enumerate these and budget the boundary crossing up front. ## 6. Open rulings — needed before a dispatch contract 1. **Delete/modify coverage per family.** The ruling leaves this as the contract's design work and explicitly declines to assume full CRUD: `CreateStaff` ships today with no `DeleteStaff`. Group 3's precedent is "mint + empty-only delete" for containers. Which families get deletes in G3? 2. **`decomposition_attachments`.** The ruling calls it derived, not authored — the prepass creates it (`prepass.rs:382`) and reduction only ever *retains* (`reduce.rs:2342`, its sole mention). It leaves the eight-field table rather than gaining operations. **Flagged for ratification with the tranche.** Verified: the citation is accurate. 3. **The measure/meter invariant.** Measures are authored, not derived (ruling §2), so measure/meter consistency becomes an authoring obligation backed by a graph invariant. That invariant needs specifying — it belongs in G3. 4. ~~**Ladder shape.** G1/G2/G3 as above, or a different cut.~~ **Ratified 2026-07-24**, and amended 2026-07-28: G2 splits into **G2a** (the two major-0 setters) and **G2b** (`SetTuningContext` alone, carrying the accept-set raise and the S13 close). See §4. *Related: `spec/RULING_GENESIS_PERSISTENCE.md`, `spec/ANALYSIS_GENESIS_PERSISTENCE.md`, `spec/PLAN_EDITOR_APP.md` §Ruling B / §3.7, `spec/PLAN_PUSH4B_TUNING.md` (the tranche mold), `spec/PASS13_CANDIDATES.md` (S13).*