Ratify the G3 rulings and draft the G3a contract

Folds the three open PLAN_GENESIS_OPS.md §6 rulings, ratified 2026-07-29:

1. Deletes: mints only, all five deferred out of G3. The former "live
   dependents -> container-not-empty" sentence is superseded outright --
   ContainerNotEmpty concerns owned children (effect.rs:156), while the G3
   hazard is dangling inbound references from independently-live objects,
   which would need a new typed reason and its own epoch.
2. decomposition_attachments: derived, not authored.
3. Measure.time_signature: resolution-plus-agreement. An optional explicit
   declaration at the measure start, neither an override of the metric grid
   nor a cache of it. Invariant 20 (G3b) covers agreement and boundary
   consistency only, not reference resolution.

G3 splits: G3a (four root-level mints, kinds/tags 35-38, epoch 11, all
schema major 0) and G3b (CreateMeasure, kind/tag 39, epoch 12, carrying
invariant 20 and a new precondition reason at discriminant 16).

Correction of record: an earlier scoping claimed invariant 10 "covers
cross-cutting refs, not this". False -- read off the variant's doc comment
rather than the check body. Invariant 10 already resolves a staff's group, a
group's members, a part's staves, a view's active layers, and measure and
grid time-signature references (invariants.rs:1122-1156, :1180-1212), tested
at :3596. Its doc comment names none of them, so G3a owes a prose
reconciliation and no enum entry.

Also corrects five drifted citations that a touch table exists to prevent:
reduce.rs 3850->4075 and 2342->2559, barriers.rs 437->313, barrier.rs
1105->1156, text_projection_grammar.rs 307->315; and "five siblings" ->
"four siblings" now that CreateMeasure is its own packet.

Verified against the tree, not assumed: all four carried types are schema
major 0 (no versioned walk exists; both decode_v0_score and the live walk
read them through plain Codec::dec), TypedObjectId already carries all five
variants, and struct_codec! generates TextValue as well as Codec -- so G3a
moves no wire bound, appends no typed id, and needs no textvalue_graph.rs
work. It does close a live defect: every G3 object kind becomes Live only
through base ingest, so CreateStaff's group precondition is currently
unsatisfiable under from-empty reduction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
This commit is contained in:
Levi Neuwirth 2026-07-29 11:12:49 -04:00
parent c10449a964
commit f1ce25f8f8
2 changed files with 459 additions and 34 deletions

View File

@ -0,0 +1,329 @@
# 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 closes the staff-group half and completes the four root-level vectors. It
is the last genesis rung that moves no wire bound.
---
## 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 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 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 }`.
**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 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.
* `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.10.0 → 0.11.0), re-sweeping five live
version sites plus a changelog row and re-flipping the negative
`superseded_companion_version` vector.
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/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`) |
| `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-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.** The one-time
authorization to edit them is per-packet and **does not generalise**; it is
granted for this packet for these two files only, for the exhaustiveness arms
and the literal bump. Touch nothing else in either crate.
### Text projection
| File | What |
|---|---|
| `crates/epiphany-textproj/src/lib.rs` | `COMPANION_VERSION` 0.10.0 → 0.11.0 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** | Swap two fields in one op's `encode_canonical`; must fail. *(A self-consistent reorder applied to both codec halves passes round-trip tests — trap 4, the 3b-i lesson. Literal-byte vectors are what catch 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 |
| t6 | Referential preconditions refuse under a graph: a `CreateStaffGroup` naming a non-live `Staff`, and a `CreateView` naming a non-live `AnalysisLayer`, are both `TargetMissing` | Drop the members loop from `create_staff_group`; must fail |
| 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 | **The defect closes**: from empty, `CreateInstrument``CreateStaffGroup``CreateStaff` **with `group: Some(...)`** succeeds and reaches a note | 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 |
| t9 | A score reduced from empty through all four ops **passes `check_invariants`** (pin 5) | Make `create_view` skip the `active_layers` check *and* author a dangling layer reference; invariant 10 must fire |
| 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.11.0 | Drop one parse arm; 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; revert the comment to see it fail |
**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; each precondition and its invariant-10 correspondence; the from-empty
grouped-staff defect closing; epoch 11 at both sites; the four-document sweep;
and the five boundary-crossing literals with their new values. Report each
mutation's **observed** output. Anything not done, say so plainly.

View File

@ -7,7 +7,7 @@ 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:** the ladder **G1 → G2a → G-minor → G2b** is **complete**; only **G3**
remains.
remains, and it **splits into G3a and G3b** (ratified 2026-07-29, §4).
* **G1 landed** (3b09595) — `CreateInstrument`, kind/tag 31.
* **G2a landed** (7df5ca1 + 55eff00) — `SetCanvasLayoutDefaults` and
@ -22,8 +22,15 @@ remains.
`epiphany_core::TuningContextSettings`, **not** the full graph type — §5
trap 7's holdout, resolved in the contract as *subset over normalization*.
Closed **P13-S13**.
* **G3** — the five remaining mint families, plus the delete/modify coverage
design. Scoped, not contracted.
* **G3a** — the four root-level mint families (`CreateStaffGroup`,
`CreatePartDefinition`, `CreateAnalysisLayer`, `CreateView`), kinds/tags
**3538**, epoch **11**, all schema major **0**. Contracted.
* **G3b**`CreateMeasure` alone, kind/tag **39**, epoch **12**, carrying
graph invariant **20** and a new `PreconditionFailureReason` at discriminant
**16**. Scoped, not contracted.
**Deletes are deferred out of G3 entirely** (§6.1, ratified 2026-07-29). Both
packets are mints only.
§6 lists what still needs ratification. **Standing constraint, now with real
teeth:** pruning MUST NOT be implemented until disposition C lands — after G2b
@ -59,7 +66,7 @@ existing `Codec`, **introducing no new byte layout**, and its generated
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
Concretely: the tranche adds the 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`.
@ -83,7 +90,8 @@ be reopened, and four hand-maintained lists — two asserting the tag was
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.**
Next free: **kind 35, tag 35** (re-verified 2026-07-29 against
`payload.rs:390`, `:714`; this line read "kind 31, tag 31" before G1 landed).
## 3. The nine surfaces, and what each costs
@ -298,22 +306,57 @@ vocabularies (`OperationPayload` 3, `ReanchorReason` 6,
Orthogonal to the *major* accept-set, which stays 2 through G2a and rises to 3
only at G2b.
### G3 — the remaining entity families
### G3 — the remaining entity families, split in two (ratified 2026-07-29)
`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:
All five ride the `CreateStaff` set-union mint pattern (`reduce.rs:4075`) with
byte-identical re-carry idempotence. 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.
**Mints only. Deletes are deferred out of G3** — see §6.1, which also
supersedes this section's former "deleting an entity with live dependents →
refuse (container-not-empty)" line.
**Neither packet moves a wire bound, and neither touches a typed-id
vocabulary.** Verified 2026-07-29 against the working tree:
* All five carried types are **schema major 0**. No versioned walk exists for
`StaffGroup`, `PartDefinition`, `AnalysisLayer`, or `ViewDefinition` — both
`decode_v0_score` (`codec.rs:2698`) and the live walk (`:3274`) read them
through plain `Codec::dec`. `Measure` is the `CanvasLayoutDefaults` shape:
the versioning lives in the containing `enc_staff_instance_v1`
(`codec.rs:3070`), not the leaf, and every walk carries `measures`.
* `TypedObjectId` **already** carries all five variants (`ids.rs:496`, `:499`,
`:500`, `:512`, `:517`). No append, no discriminant event.
* All four G3a types already have a `Codec` **and** a `TextValue` — both are
generated by the one `struct_codec!` macro (`codec.rs:510`, `:522`), so no
`textvalue_graph.rs` work. What they lack is a `canonical_value!` entry.
**G3a closes a live defect, not merely a completeness gap.** All five object
kinds become `Live` **only** through base ingest (`reduce.rs:1449``:1563`);
no operation mints any of them. So `CreateStaff`'s group precondition
(`reduce.rs:4119`) is **currently unsatisfiable under from-empty reduction**
a document built only from operations can never author a grouped staff — and
`TimeAnchor::Measure` (`reduce.rs:1280`) can never resolve. G1 opened this by
making from-empty reachable; G3a closes the staff-group half of it.
**G3a — the four root-level mints.** Kinds/tags 3538, epoch 11. Self-contained:
`CreateView`'s precondition target is minted by `CreateAnalysisLayer` in the
same packet. No new graph invariant; it owes only the invariant-10 prose
reconciliation (§6.3).
**G3b — `CreateMeasure` alone.** Kind/tag 39, epoch 12. `CreateMeasure` is
shaped differently from its four 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()`. It additionally carries graph invariant
**20** and a new `PreconditionFailureReason` at discriminant **16** — a second
wire vocabulary append at the same epoch. **The split exists so that a
normative Chapter 5 listing append is not buried inside a packet of routine
mints** — the same reasoning that kept the accept-set raise out of G2a.
## 5. Traps
@ -352,29 +395,82 @@ Consider splitting it out if G3 runs long.
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,
(`barriers.rs:313`), 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.
cannot run at all. Four further sites bake in a literal that only surfaces
once the workspace compiles: `layout-ir/src/barrier.rs:1156` (a tag
"one past the vocabulary"), `testkit/tests/text_projection_grammar.rs:315`
(a hardcoded kind *count*, with its message string at the same site),
`testkit/src/generators.rs:1908` (a drawn-discriminant range plus a
never-drawn guard at `:1947`), 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.
*(All four citations re-verified 2026-07-29; the three carried in earlier
revisions had drifted — `barriers.rs:437`, `barrier.rs:1105`,
`text_projection_grammar.rs:307` — which is exactly the failure mode a
contract's touch table exists to prevent.)*
## 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.
1. ~~**Delete/modify coverage per family.**~~ **Ratified 2026-07-29: mints
only. All five deletes are deferred out of G3.**
The precedent is not one precedent but a clean split, verified against the
vocabulary: every paired create+delete family — CrossCutting, Region,
StaffInstance, Voice, RepeatStructure — is a **nested container with owned
children**, while both unpaired mints, `CreateStaff` (23) and
`CreateInstrument` (31), are **root-level `Score` vectors**. G3's families
land on both sides of that line.
**This section's former sentence "deleting an entity with live dependents →
refuse (container-not-empty)" is superseded and MUST NOT be treated as a
contract.** It conflates two different semantics. `ContainerNotEmpty` is
explicitly about owned children — "a container that still has live children"
(`ops/src/effect.rs:156`). The G3 hazard is the opposite shape: deleting a
`StaffGroup` orphans no children, it dangles **inbound** references from
independently-live objects (`Staff.group`; likewise `AnalysisLayer`
`View.active_layers`). A correct refusal needs a **new** typed
`PreconditionFailureReason` — the space currently runs 015 — which is a
further wire vocabulary append with its own epoch. Each delete is also a
full kind+tag append carrying its own four-document sweep. Deferred as its
own rung if wanted; nothing in G3 depends on it.
2. ~~**`decomposition_attachments`.**~~ **Ratified 2026-07-29: derived, not
authored.** It stays out of the operation vocabulary and keeps its place in
the eight-field table. The creation path is the prepass
(`core/src/prepass.rs:382`); reduction only ever *removes invalidated*
attachments (`ops/src/reduce.rs:2559`, its sole mention, a `retain`).
*(Citation re-verified 2026-07-29; the earlier `reduce.rs:2342` had
drifted.)*
3. **The measure/meter invariant.** **Semantics ruled 2026-07-29; the invariant
itself lands in G3b.** `Measure.time_signature` is an optional explicit
display/declaration at the measure start — **neither an override of the
metric grid nor a cache of it**:
* the effective grid is `StaffInstance.local_metric_grid`, falling back to
the region's `default_metric_grid`;
* `Some(id)` MUST resolve **and** equal the grid signature active at that
measure's start;
* `None` means inherit the active signature and display no new signature;
* where positions are determinable, authored measure boundaries MUST stay
consistent with the active signature's `measure_duration`. **Pickup
handling remains deferred.**
**Invariant 20 covers agreement and boundary consistency only — it MUST NOT
duplicate reference resolution**, which invariant 10 already performs. G3b
additionally adds a typed failure reason (e.g. `MeasureMeterMismatch`) at
discriminant **16**, epoch **12**.
**Correction of record.** An earlier scoping claimed invariant 10 "covers
cross-cutting refs, not this". That was false, and it was read off the
variant's doc comment rather than the check body. Invariant 10's body
already resolves a staff's group, a group's members, a part's staves, a
view's active layers (`core/src/invariants.rs:1122``:1156`) and measure and
grid time-signature references (`:1180``:1212`), with a direct test at
`:3596`. Its **doc comment** (`invariants.rs:59``:62`) names only
cross-cutting structures and event-internal references, so the prose
materially understates the check across exactly the five reference classes
G3 authors. **G3a owes that prose reconciliation**; it adds no invariant
enum entry.
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