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:
parent
c10449a964
commit
f1ce25f8f8
|
|
@ -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 35–38, in **both** spaces, and they are aligned here
|
||||
|
||||
Next free is kind 35, tag 35 (`payload.rs:390`, `:714`, re-verified
|
||||
2026-07-29). The two spaces are **not** aligned in general —
|
||||
`OperationKind::discriminant()` is a hand-written match (`payload.rs:253`)
|
||||
while `OperationKindTag` is macro-generated (`payload.rs:440`); `RespellPitch`
|
||||
is kind 2 and tag 3. They happen to coincide from 24 upward. **Assign each
|
||||
space explicitly and never derive one from the other**; pin 1's test asserts
|
||||
both independently.
|
||||
|
||||
Assignment order is fixed as the table in §Scope: StaffGroup 35, PartDefinition
|
||||
36, AnalysisLayer 37, View 38.
|
||||
|
||||
### Pin 2 — `schema_major()` gains **no arm**; the catch-all `_ => 0` is correct
|
||||
|
||||
This is G2a's shape, not G2b's. All four carried types have exactly one byte
|
||||
layout: no versioned walk exists for any of them, and both `decode_v0_score`
|
||||
(`codec.rs:2698`) and the live walk (`:3274`) read all four through plain
|
||||
`Codec::dec`. **Adding them to the `=> 2` arm would be the bug.** Verify by
|
||||
reading the walks, not by assuming.
|
||||
|
||||
Consequence: **no `epiphany-bundle` change of any kind.** The op-block
|
||||
accept-set stays at 3 where G2b left it. If the implementation touches a bundle
|
||||
file, something is wrong.
|
||||
|
||||
### Pin 3 — the four types need `canonical_value!` entries, and nothing else in core
|
||||
|
||||
Each already has a `Codec` — `struct_codec!(PartDefinition …)`
|
||||
(`codec.rs:1790`), `AnalysisLayer` (`:1791`), `ViewDefinition` (`:1792`),
|
||||
`StaffGroup` (`:2329`) — and each is already exported from `lib.rs`. **None is
|
||||
in `canonical_value!`** (`codec.rs:3518`); G3a adds exactly four entries, in
|
||||
the §Scope order, under one comment naming this contract.
|
||||
|
||||
`canonical_value!` introduces **no new byte layout** — it makes the existing
|
||||
whole-score layout reachable per-value, and its generated `decode_canonical`
|
||||
gives strict canonical-form enforcement (decode → `finish()` → re-encode →
|
||||
reject on mismatch) for free.
|
||||
|
||||
**No `textvalue_graph.rs` work.** `struct_codec!` generates the `TextValue`
|
||||
impl as well as the `Codec` (`codec.rs:510`, `:522`), so all four types already
|
||||
project and parse. This is the one place G3a is *cheaper* than G2b, which had
|
||||
to hand-write `TextValue for TuningContextSettings`. Confirm it by compiling,
|
||||
not by assuming.
|
||||
|
||||
### Pin 4 — referential preconditions are **graph-aware**, mirroring `CreateStaff`
|
||||
|
||||
Per ruling §2, and copying `create_staff`'s structure (`reduce.rs:4102`–`:4125`)
|
||||
including its `if self.graph.is_some()` guard — base-free reduction has no
|
||||
universe to check against and MUST NOT enforce these:
|
||||
|
||||
| Op | Precondition | Failure reason |
|
||||
|---|---|---|
|
||||
| `CreateStaffGroup` | every `members[i]` is a live `Staff` | `TargetMissing` |
|
||||
| `CreatePartDefinition` | every `staves[i]` is a live `Staff` | `TargetMissing` |
|
||||
| `CreateAnalysisLayer` | *(none — no outbound references)* | — |
|
||||
| `CreateView` | every `active_layers[i]` is a live `AnalysisLayer` | `TargetMissing` |
|
||||
|
||||
**Reuse `TargetMissing` (discriminant 0). Add no new
|
||||
`PreconditionFailureReason`** — that space stays at 0–15 until G3b.
|
||||
|
||||
Mint preconditions are `CreateStaff`'s exactly: a live id re-carried with a
|
||||
byte-identical value is `NoOp { AlreadyApplied }`; a live id with a differing
|
||||
value is `NoOp { PreconditionFailedUnderReduction { RecreateContentMismatch } }`;
|
||||
a tombstoned id is `NoOp { TargetTombstoned }`.
|
||||
|
||||
**The packet is self-contained**: `CreateView`'s precondition target is minted
|
||||
by `CreateAnalysisLayer` in this same packet, so the ordering is testable
|
||||
end-to-end without a base score.
|
||||
|
||||
### Pin 5 — every precondition must correspond to an existing invariant-10 check
|
||||
|
||||
Invariant 10's **body** already resolves a staff's group, a group's members, a
|
||||
part's staves, and a view's active layers (`core/src/invariants.rs:1122`–`:1156`).
|
||||
The reducer's new preconditions and that checker must agree: **a score reduced
|
||||
from empty through these operations MUST pass `check_invariants`.** This is the
|
||||
oracle, and it is stronger than any assertion the reducer can make about itself.
|
||||
|
||||
### Pin 6 — the invariant-10 **prose** reconciliation (§6.3), and it is doc-only
|
||||
|
||||
Invariant 10's doc comment (`invariants.rs:59`–`:62`) names only cross-cutting
|
||||
structures and event-internal references. Its body checks materially more: the
|
||||
four reference classes above, plus measure and grid time-signature references
|
||||
(`:1180`–`:1212`). **G3a repairs the doc comment to describe what the check
|
||||
actually enforces.**
|
||||
|
||||
**No enum entry, no discriminant, no behaviour change, no `all()` count
|
||||
change.** `GraphInvariant` stays at 19. It does not reach the wire — no
|
||||
reference from `epiphany-ops` or `epiphany-bundle` — so this is not a schema
|
||||
event. Invariant 20 is G3b's.
|
||||
|
||||
### Pin 7 — G-minor interaction: all four kinds carry epoch 11
|
||||
|
||||
`introduced_minor()` returns `Option<u16>` and has **no wildcard arm** by
|
||||
design, so a new variant cannot compile without an epoch. All four take
|
||||
`Some(11)` — one epoch for one additive event, per the ratified policy
|
||||
(`spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4); G2a's precedent put two kinds at the
|
||||
single epoch 9.
|
||||
|
||||
**The sentinel must not be 0.** `0` is a real baseline minor for V1–V3.
|
||||
|
||||
Two sites, both required: the `@ Some(11)` annotations in the tag vocabulary
|
||||
(`payload.rs:714` region) and the ratified-table transcription in test `s1`
|
||||
(`payload.rs:2475` region). **An epoch omitted from the s1 table is an epoch
|
||||
that test cannot see go wrong** — that comment is already in the file; honour it.
|
||||
|
||||
Append the epoch-11 row to `spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4's ladder,
|
||||
naming the introducing commit once it exists.
|
||||
|
||||
### Pin 8 — the four-document append ritual applies in full
|
||||
|
||||
An operation-vocabulary append is a documented event in **four** specification
|
||||
documents. G1 shipped five normative falsehoods by declaring them out of scope;
|
||||
that is not repeatable.
|
||||
|
||||
* `operation_catalog.tex` — a `\section` per kind (four), version bump,
|
||||
changelog paragraph.
|
||||
* `binary_format.tex` — payload-layout and tag rows per kind, version bump,
|
||||
Revision History row.
|
||||
* `core_spec.tex` — the normative `OperationKind`/`OperationKindTag` listings
|
||||
and the spelled-out payload counts.
|
||||
* `text_projection.tex` — four new kind productions are a document-surface
|
||||
change, so `COMPANION_VERSION` bumps (0.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 35–38 are **outside** it, exactly as 30–34 already are.
|
||||
**Do not extend the lock in this packet.** P13-S15 lands as its own rung with
|
||||
its own mutation evidence; widening it here would ship the extension without
|
||||
that evidence.
|
||||
|
||||
---
|
||||
|
||||
## 3. Touch table
|
||||
|
||||
Derived from `git show 3b09595 --name-only` (G1, the closest mint precedent)
|
||||
and `git show 13c3d2f --name-only` (G2b), minus what pins 2 and 3 exclude.
|
||||
Every line number below re-verified against the working tree 2026-07-29.
|
||||
|
||||
### Core
|
||||
|
||||
| File | What |
|
||||
|---|---|
|
||||
| `crates/epiphany-core/src/codec.rs` | four `canonical_value!` entries (`:3518` list) |
|
||||
| `crates/epiphany-core/src/invariants.rs` | pin 6: invariant-10 doc comment (`:59`–`:62`) |
|
||||
| `crates/epiphany-core/DECISIONS.md` | the rung's record |
|
||||
|
||||
**Not touched:** `graph.rs` (all four types exist), `textvalue_graph.rs` (pin
|
||||
3), `lib.rs` (already exported).
|
||||
|
||||
### Ops
|
||||
|
||||
| File | What |
|
||||
|---|---|
|
||||
| `crates/epiphany-ops/src/payload.rs` | four op structs + `CanonicalEncode`; `OperationKind` variants; `discriminant()` (`:390` region); `schema_major()` — **no arm**, pin 2; `introduced_minor()` (`:449` region); `tag()` (`:497` region); `encode_canonical` (`:543` region); tag vocabulary `@ Some(11)` (`:714` region); s1 epoch table (`:2475` region) |
|
||||
| `crates/epiphany-ops/src/envdecode.rs` | decode arms (`:599` region) and the tag-dispatch arms (`:901` region), plus validation |
|
||||
| `crates/epiphany-ops/src/reduce.rs` | four dispatch arms + four mint reducers, on `create_staff`'s shape (`:4075`, `:4148`) |
|
||||
| `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 35–38 in **both** spaces, and the discriminant byte leads each canonical encoding | Move any one kind to 39; then, separately, move its tag. Both must fail — the spaces are asserted independently (pin 1) |
|
||||
| t2 | `schema_major()` returns **0** for all four | Add them to the `=> 2` arm; must fail (pin 2's stated bug) |
|
||||
| t3 | A block containing all four stamps major **0**, and the op-block accept-set is untouched at 3 | Make one kind report major 2; must fail |
|
||||
| t4 | Each op round-trips through `encode` → `envdecode` → reduce, byte-identical, with its decode vector pinned to **literal bytes** | Swap two fields in 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.
|
||||
|
|
@ -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
|
||||
**35–38**, 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 35–38, 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 0–15 — 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
|
||||
|
|
|
|||
Loading…
Reference in New Issue