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. which questions must be answered before a dispatch contract can be written.
**Status:** the ladder **G1 → G2a → G-minor → G2b** is **complete**; only **G3** **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. * **G1 landed** (3b09595) — `CreateInstrument`, kind/tag 31.
* **G2a landed** (7df5ca1 + 55eff00) — `SetCanvasLayoutDefaults` and * **G2a landed** (7df5ca1 + 55eff00) — `SetCanvasLayoutDefaults` and
@ -22,8 +22,15 @@ remains.
`epiphany_core::TuningContextSettings`, **not** the full graph type — §5 `epiphany_core::TuningContextSettings`, **not** the full graph type — §5
trap 7's holdout, resolved in the contract as *subset over normalization*. trap 7's holdout, resolved in the contract as *subset over normalization*.
Closed **P13-S13**. Closed **P13-S13**.
* **G3** — the five remaining mint families, plus the delete/modify coverage * **G3a** — the four root-level mint families (`CreateStaffGroup`,
design. Scoped, not contracted. `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 §6 lists what still needs ratification. **Standing constraint, now with real
teeth:** pruning MUST NOT be implemented until disposition C lands — after G2b 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 mismatch. So every new operation payload gets strict canonical-form enforcement
for free, on the same seam the decode-vector corpus uses. 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** `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 design wire layouts. Every layout it carries is already frozen and already
shipping inside `Score`. 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; 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 `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`. 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 ## 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 Orthogonal to the *major* accept-set, which stays 2 through G2a and rises to 3
only at G2b. only at G2b.
### G3 — the remaining entity families ### G3 — the remaining entity families, split in two (ratified 2026-07-29)
`CreateStaffGroup`, `CreatePartDefinition`, `CreateAnalysisLayer`, `CreateView`, All five ride the `CreateStaff` set-union mint pattern (`reduce.rs:4075`) with
`CreateMeasure`, on the `CreateStaff` set-union mint pattern (`reduce.rs:3850`) byte-identical re-carry idempotence. Graph-aware referential preconditions per
with byte-identical re-carry idempotence, plus whatever delete/modify coverage the ruling §2:
§6.1 rules owed. Graph-aware referential preconditions per the ruling §2:
* `CreateStaffGroup.members`, `CreatePartDefinition.staves` → live `Staff`s * `CreateStaffGroup.members`, `CreatePartDefinition.staves` → live `Staff`s
* `CreateView.active_layers` → live `AnalysisLayer`s * `CreateView.active_layers` → live `AnalysisLayer`s
* `CreateMeasure` → a live `StaffInstance` * `CreateMeasure` → a live `StaffInstance`
* deleting an entity with live dependents → refuse (container-not-empty)
`CreateMeasure` is shaped differently from its five siblings: `measures` is **Mints only. Deletes are deferred out of G3** — see §6.1, which also
nested on `StaffInstance` (`graph.rs:611`), not a `Score`-level vector, so its supersedes this section's former "deleting an entity with live dependents →
precondition reaches three levels down through `canvas.regions[].staff_instances()`. refuse (container-not-empty)" line.
Consider splitting it out if G3 runs long.
**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 ## 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** 6. **Adding an `OperationKind` variant is NOT containable to core + ops**
the G1 lesson, and the one claim this plan previously got wrong. Rust the G1 lesson, and the one claim this plan previously got wrong. Rust
exhaustiveness forces an arm in `epiphany-editor-core`'s `subjects_of` 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 a missing arm blocks conformance *and* `requirement_labels` — the gate
cannot run at all. Three further sites bake in a literal that only surfaces cannot run at all. Four further sites bake in a literal that only surfaces
once the workspace compiles: `layout-ir/src/barrier.rs:1105` (a tag once the workspace compiles: `layout-ir/src/barrier.rs:1156` (a tag
"one past the vocabulary"), `testkit/tests/text_projection_grammar.rs:307` "one past the vocabulary"), `testkit/tests/text_projection_grammar.rs:315`
(a hardcoded kind *count*), and `textproj/src/vectors.rs` (a negative vector (a hardcoded kind *count*, with its message string at the same site),
whose "wrong version" is the one each bump moves to). Every G2/G3 contract `testkit/src/generators.rs:1908` (a drawn-discriminant range plus a
MUST enumerate these and budget the boundary crossing up front. 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 ## 6. Open rulings — needed before a dispatch contract
1. **Delete/modify coverage per family.** The ruling leaves this as the 1. ~~**Delete/modify coverage per family.**~~ **Ratified 2026-07-29: mints
contract's design work and explicitly declines to assume full CRUD: only. All five deletes are deferred out of G3.**
`CreateStaff` ships today with no `DeleteStaff`. Group 3's precedent is
"mint + empty-only delete" for containers. Which families get deletes in G3? The precedent is not one precedent but a clean split, verified against the
2. **`decomposition_attachments`.** The ruling calls it derived, not authored — vocabulary: every paired create+delete family — CrossCutting, Region,
the prepass creates it (`prepass.rs:382`) and reduction only ever *retains* StaffInstance, Voice, RepeatStructure — is a **nested container with owned
(`reduce.rs:2342`, its sole mention). It leaves the eight-field table rather children**, while both unpaired mints, `CreateStaff` (23) and
than gaining operations. **Flagged for ratification with the tranche.** `CreateInstrument` (31), are **root-level `Score` vectors**. G3's families
Verified: the citation is accurate. land on both sides of that line.
3. **The measure/meter invariant.** Measures are authored, not derived (ruling
§2), so measure/meter consistency becomes an authoring obligation backed by a **This section's former sentence "deleting an entity with live dependents →
graph invariant. That invariant needs specifying — it belongs in G3. 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 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 2026-07-24**, and amended 2026-07-28: G2 splits into **G2a** (the two
major-0 setters) and **G2b** (`SetTuningContext` alone, carrying the major-0 setters) and **G2b** (`SetTuningContext` alone, carrying the