epiphany/spec/CONTRACT_GENESIS_G3B_MEASUR...

1042 lines
61 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Contract: genesis tranche G3b — `CreateMeasure`, invariant 20, and the close of the genesis ladder
**Status:** RATIFIED.
**Governs:** the final rung of `spec/PLAN_GENESIS_OPS.md`. One kind, one
invariant, three precondition reasons, one epoch.
**Predecessors:** G3a `6c5e69f`; the G3a undo repair `4b0abaf`; P13-S17
`6170015`.
---
## §0. What was verified before drafting
Read out of the tree, not assumed. Every number below was checked.
**`Measure` is a nested container child, not a root-level entity.** It lives
in `StaffInstance.measures` (`crates/epiphany-core/src/graph.rs:611`), not in
a `Score` root vector. **Every one of G3a's four families was root-level**, so
G3a's shape does not transfer: G3b's precedent is `CreateStaffInstance`
(`payload.rs:1510`, reducer `reduce.rs:4026`), which carries a parent id
beside the value:
```rust
pub struct CreateStaffInstanceOp {
pub region: RegionId,
pub instance: StaffInstance,
}
```
**`Measure` is schema major 0, so G3b moves no wire bound.** `struct_codec!(Measure { id, start, time_signature, explicit_number, number_visibility })`
at `codec.rs:1825` is a plain unversioned walk, and its only non-scalar field
`start: TimeAnchor` has a hand-written `Codec` (`codec.rs:750``:800`) with
**no version branching of any kind** — I read both. `MeasureNumberVisibility`
is a `cstyle_enum_codec!` (`codec.rs:1526`). Therefore
`OperationKind::schema_major()` (`payload.rs:311`) gains **no arm**;
`CreateMeasure` falls through `_ => 0`, and `OperationEnvelopeBlock` stays at
**3** where G2b left it. G3b is *not* a G2b-shaped rung.
**No id-vocabulary append.** `TypedObjectId::Measure(MeasureId)` already
exists (`ids.rs:500`) with kind byte **9** (`ids.rs:536`), round-trip at
`:572`/`:666`.
**`Measure` is absent from `canonical_value!`.** It has a `Codec` and — via
`struct_codec!` — a `TextValue`, but the strict-decode list
(`codec.rs:3597`ff, where G3a added its four) does not contain it. Verified by
scanning the macro invocation body: zero occurrences.
**Measures currently reach the graph only through base ingest.**
`create_staff_instance` explicitly rejects a carried instance bearing
measures (`reduce.rs:4047`: `!op.instance.measures.is_empty()`
`container_not_empty()`). This is the same from-empty condition that made
G3a's four families base-ingest-only, and it is what `CreateMeasure` retires.
**Discriminants 16, 17 and 18 are free on `PreconditionFailureReason`.** The
`discriminant()` match (`effect.rs:190`ff) ends at
`TranspositionOutOfRange => 15`. `introduced_minor()` is exhaustive with no
wildcard arm, so a new variant **cannot compile** without an epoch — the
G-minor control, working as designed.
**Invariant 20 is free.** `GraphInvariant::number()`
(`invariants.rs:96``:119`) runs 119, ending `BarlineGroupSameRegion => 19`.
`all()` returns **`[GraphInvariant; 19]`** (`invariants.rs:121`) — a
hand-maintained count literal — under a doc comment reading "All 19
invariants."
**Invariant 10 already checks measure-signature *resolution*.** Its body
covers a measure's signature reference at `invariants.rs:1180``:1212`, with
a direct test `inv10_flags_unresolved_time_signature_reference` at `:3596`.
Invariant 20 therefore MUST NOT re-check resolution (pin 9b).
**The effective grid, from the actual field types.**
`StaffInstance.local_metric_grid: Option<MetricGrid>` (`graph.rs:608`),
falling back to the enclosing region's
`default_metric_grid: Option<MetricGrid>` (`graph.rs:655`).
`MetricGrid { meter_sequence: Vec<MeterChange> }` (`graph.rs:358`), and
`MeterChange { anchor: TimeAnchor, time_signature: TimeSignatureId }`
(`graph.rs:237`) — the change is positioned by a **`TimeAnchor`**, not a
scalar offset, which pins 6 and 6b must account for.
**`Measure` has a live inbound-reference surface.** Cross-cutting structures
anchor to measures through `TimeAnchor::Measure { id, .. }` — see
`payload.rs:1051` (spanner endpoints → `TypedObjectId::Measure`),
`indexes.rs:56`, and `invariants.rs:452`. **This is the Packet A defect class,
live for G3b before a line is written** (pin 10).
**The six boundary crossings, at their post-G3a values.** One compiles
loudly; five fail silently:
| # | Site | Now | Becomes |
|---|---|---|---|
| 1 | `epiphany-editor-core/src/barriers.rs` `subjects_of` | exhaustive match | one new arm — **compiles loudly** |
| 2 | `epiphany-layout-ir/src/barrier.rs:1179` | `tag: 39` | `tag: 40` |
| 3 | `epiphany-testkit/tests/text_projection_grammar.rs:316``:317` | `39`, `"38 payload-free kinds plus `Registered`"` | `40`, `"39 payload-free kinds…"` |
| 4 | `epiphany-testkit/src/generators.rs:1932` + test body | comment `30..=38`; `saw_*` flags per kind | `30..=39` + a `saw_create_measure` flag and its assertion |
| 5 | `epiphany-testkit/src/layout_stub.rs:1375` | comment `30..=38` | `30..=39` |
| 6 | `epiphany-textproj/src/vectors.rs:533``:534`, `:941` | `(0 12 0)` / `(0 11 0)` | `(0 13 0)` / `(0 12 0)` |
Sites 4 and 5 are **not** comment-only: both test bodies enumerate appended
kinds explicitly, so site 4 needs a new boolean and assertion, not a literal
bump. `COMPANION_VERSION` is `(0, 12, 0)` at
`epiphany-textproj/src/lib.rs:59`.
**The golden lock still stops at 30.** `payload.rs:2165` reads
`let table: [(OperationKind, u8); 30] = [`. Kinds 3039 stay outside it —
**P13-S15 remains open by design**; G3b does not extend it (pin 12).
**Packet B's own guard now constrains this rung.** The history guard
(`epiphany-testkit/tests/binary_format_history.rs`) asserts a principal
marker per standalone-row rung. G3b must add its Revision History row *and*
its marker to `PRINCIPAL_MARKERS`, or the guard fails — by construction
(pin 14).
---
## §1. Pins
**Pin 1 — kind and tag 39, epoch 12, in both discriminant spaces.** The two
spaces are unaligned and both hand-touched: `OperationKind::discriminant()`
(hand-written match, `payload.rs:253`ff) and `OperationKindTag` (macro
`operation_kind_tag_vocabulary!`, `payload.rs:440`ff). Add
`CreateMeasure => 39` to the first and
`CreateMeasure = 39 => "create-measure" @ Some(12)` to the second.
**Pin 2 — `schema_major()` gains no arm.** Per §0, `Measure` is major 0.
`OperationEnvelopeBlock` stays at 3. If an arm for `CreateMeasure` appears in
`schema_major()`, the packet is wrong.
**Pin 3 — `Measure` joins `canonical_value!`** (`codec.rs:3597`ff), one
entry. This introduces **no new wire layout** — the macro delegates to the
existing `Codec` and generates strict `decode_canonical` (decode →
`finish()` → re-encode → reject on byte mismatch). No `textvalue_graph.rs`
work: `struct_codec!` already generated `Measure`'s `TextValue`.
**Pin 4 — the operation shape follows `CreateStaffInstance`, not G3a.**
```rust
pub struct CreateMeasureOp {
pub instance: StaffInstanceId,
pub measure: Measure,
}
```
with `measure_id()` returning `self.measure.id`, and `encode_canonical`
pushing the parent id then the value — mirroring `CreateStaffInstanceOp`
(`payload.rs:1522`ff). The parent is the `StaffInstance`, because
`measures` is per-instance and admits polymeter (`graph.rs:609``:611`).
**Pin 5 — the carried-value map must carry the OWNING INSTANCE.** A
`BTreeMap<MeasureId, Measure>` is **insufficient**: `Measure` carries no
back-pointer to its parent (`graph.rs:588``:594`), so a map keyed by
`MeasureId` alone cannot tell the graph-removal arm (pin 10) which
`StaffInstance.measures` to remove from, and cannot support any per-instance
ordering check. Every G3a family was root-level and had no parent to lose;
`Measure` does. The map is therefore
```rust
measure_values: BTreeMap<MeasureId, (StaffInstanceId, Measure)>,
```
threaded through the **same seven sites** — reducer state declaration,
`WorkingSnapshot` declaration, initialization, **base seeding in
`seed_from_graph`** (recording the enclosing instance id as it walks), mint
insertion, snapshot, restore. Site 4 fails silently when omitted (the G1
`instrument_values` hazard).
Set-union discipline: byte-identical re-carry → `AlreadyApplied`; differing
value under a live id → `RecreateContentMismatch`; tombstoned →
`TargetTombstoned`. **A re-carry naming a *different* parent instance with an
otherwise identical measure is `RecreateContentMismatch`, not
`AlreadyApplied`** — the parent is part of identity here, and the comparison
MUST include it.
**Pin 6 — the comparable relation: exact shapes, exhaustively.**
The first two drafts were both wrong about this. Draft 1 said only
cross-variant comparison failed. Draft 2 said "same referent, compare
offsets", which ignores that offsets themselves are not always comparable and
that three anchor shapes carry no referent id at all.
**Offsets first.** `AnchorOffset` (`time.rs:557`) is
`Musical(MusicalDuration) | WallClock(WallClockDuration) | Zero` — three
clocks with **no cross-clock ordering**. Normalization: `Zero` is read as the
additive identity **of whichever clock it is compared against**, so
`Zero``Musical(d)` and `Zero``WallClock(d)` are both defined. Two
offsets are **comparable** iff they are the same variant, or at least one is
`Zero`. `Musical` against `WallClock` is **never** comparable — that is the
deferred wall-clock/musical reconciliation, not an implementation detail.
**Anchor shapes.** `MeasurePosition` and `RegionEdge` are each
`Start | End` (`time.rs:540`, `:547`). **The boundary selector must be
IDENTICAL — it is never ordered.** Two anchors are **comparable** iff they
match one of these five shapes and their offsets are comparable:
| # | Shape | Order by |
|---|---|---|
| c1 | `Event{id: a, off}` vs `Event{id: a, off}`**same id** | offset |
| c2 | `Measure{id: a, pos: p, off}` vs `Measure{id: a, pos: p, off}` — same id **and same `pos`** | offset |
| c3 | `Measure{id: a, pos: Start, off: Zero}` vs `Measure{id: b, pos: Start, off: Zero}`, `a`/`b` **in the same `StaffInstance.measures`** | vector index |
| c4 | `Region{id: a, edge: e, off}` vs `Region{id: a, edge: e, off}` — same id **and same `edge`** | offset |
| c5 | `WallClock{time}` vs `WallClock{time}`**no referent id** | `time` |
**Draft 3 ordered `Start < End` and then compared offsets. That is unsound,
and core says so.** With a nonzero offset the selector does not bound the
point: `Region{edge: Start, off: Musical(100)}` is **not** provably before
`Region{edge: End, off: Zero}` without knowing the region's length. The same
holds for `Measure` Start vs End — and
`crates/epiphany-core/src/invariants.rs:400`ff records exactly this: the
prototype anchor resolver places `Measure` **start** anchors and `Region`
edges but returns `None` for a `Measure` **end**, because a coordinate
"cannot be placed without the deferred tempo/measure-length machinery."
Cross-boundary comparison therefore stays **unverifiable** unless the
boundary's own duration is resolved — which is the deferred machinery, not
this rung's business.
**c3's restriction to `pos: Start` and `off: Zero` is load-bearing.** A
vector index orders measure *reference points*, not arbitrary points near
them: a nonzero offset or an `End` position can carry a point past its
neighbour, so the index no longer bounds it.
**Everything not in that table is NOT comparable**, and no other relation may
be invented — defining one is a specification question this rung has no
authority over. In particular: never `Event`↔`Measure`, never
`Measure`↔`Region`, never two `Event`s with different ids (that needs each
event's resolved position, the deferred **P11-C5** machinery — see
`PositionOutsideRegion`'s own "Reserved" note, `effect.rs:139``:142`), never
`Musical` against `WallClock`, and **never across differing `pos`/`edge`
selectors**.
**Pin 6b — boundary consistency needs a musical DELTA, not an ordering.**
Pin 6 answers "before or after"; invariant 20's second clause needs "exactly
how far", compared against `TimeSignature::measure_duration()`
(`graph.rs:346`). Ordering does not supply that, and this was conflated in
both prior drafts.
The delta between two measure starts is computable **only** in shape c1, c2,
or c4 with **both offsets in the `Musical` clock** (or `Zero`, normalized to
`Musical(0)`), **and only when the boundary selector is identical** — same
referent id *and* same `pos`/`edge`. Then the shared reference point cancels
and the difference of the offsets is exact.
**Draft 3 subtracted offsets across differing `pos`/`edge`, which is the same
unsoundness as pin 6's ordering error.** Across a boundary the reference
points differ by the boundary's own duration, which is precisely the
unresolved quantity (`invariants.rs:400`ff). Subtracting the offsets then
omits that duration and yields a wrong delta, not merely an unordered one.
`WallClock` deltas are not musical durations and MUST NOT be compared with
`measure_duration`. **c3 supplies no delta at all**: a vector index gives
order, never distance.
Where the delta is not computable, boundary consistency **abstains** (pin 7).
**Pin 6c — the effective-grid oracle, and the instance-grid ledger the
reducer does not have.**
Every prior draft said "the active signature at the measure's start" without
defining *selection*. Over a **partially comparable** `meter_sequence` that is
not a function. Define it:
**Step 0 comes before any candidate set.** If **any** `MeterChange` in the
effective grid has an anchor **incomparable** to the measure's start (pin 6),
selection is **indeterminate** whenever a governing signature is needed — the
operation refuses (`MeasureOrderUnverifiable`), the invariant abstains (pin 7).
This holds **even when the candidate set is empty**, and draft 4 got it wrong:
it let "no candidates" win, so a sequence in which *every* change is
incomparable read as "no active signature, agreement vacuous" instead of
"cannot tell." An incomparable change is not absent; it is unplaced, and it
might have governed.
Only once step 0 passes — every change comparable — let `C` = those changes
**not after** the measure's start. Then:
1. `C` empty → **no active signature.** Agreement is vacuous; the boundary
clause abstains. This is *not* a violation. Reachable only from a
genuinely empty or wholly-later sequence, never from incomparability.
2. `C` has a unique maximum under pin 6's relation → that is the governing
change.
3. `C` has **multiple maxima mutually incomparable to each other**
indeterminate, as step 0. Two changes can each be comparable to the
measure's start yet not to one another, so this rule is not subsumed by
step 0. Never pick one by document order, id, or vector position — that
fabricates a total order pin 6 refuses to define.
**The ledger gap, which blocks base-free correctness.**
`StaffInstance.local_metric_grid` **overrides** the region default
(`graph.rs:608`, `:655`), but **the reducer retains no instance-local grid
state.** `create_staff_instance` accepts the field and seeds only the layout
advisory chain (`reduce.rs:4026`ff). So base-free, a `CreateMeasure`,
`SetMetricGrid`, or `SetTimeSignature` **cannot tell a local override from an
inherited default** — it would silently read the region default and could
refuse or permit wrongly.
This rung MUST resolve that, and there are exactly two admissible answers.
**Ratify one:**
**Disposition (A) is RATIFIED** (2026-07-29): it preserves graph-aware /
base-free parity and avoids knowingly authoring states that become invalid the
moment they are materialized.
- **(A) Add an instance-grid ledger** — `instance_grid: BTreeMap<StaffInstanceId, Option<MetricGrid>>`
with the full seven-site lifecycle (declaration, snapshot decl, init, base
seed, mint/write, snapshot, restore), seeded by `create_staff_instance` and
by `seed_from_graph`.
**The ledger alone is NOT the oracle.** It distinguishes *override* from
*inheritance*; it does not reconstruct what is inherited. When an instance
inherits, the effective grid MUST be reconstructed from the **existing**
ledgers, in canonical order:
1. `metric_grid_chain` (`reduce.rs:974`,
`BTreeMap<RegionId, WriteChain<Option<MetricGrid>>>`) — whole-grid writes
for the enclosing region;
2. `meter_change_chain` (`:993`,
`WriteChain<Option<MeterChange>>` per `(RegionId, MusicalPosition)`) —
per-key changes layered over that grid, `None` meaning explicit removal.
Both are `WriteChain`s, so the reconstruction MUST also fold in **prospective
undo restorations** (pin 9c.3) when evaluating an undo: the grid the
restoration would install, not the one currently in place.
**The same oracle runs in both reduction modes.** Graph-aware reduction must
not shortcut to `score` while base-free reduction uses the chains — that would
reintroduce the parity gap (A) exists to close, and the two paths would
disagree on exactly the cases this rung refuses.
- **(B) Ratify an explicit graph-only divergence** — the agreement and
boundary preconditions are graph-aware only, and base-free reduction
**skips them entirely** rather than consulting the region default. Cheaper,
but it means a base-free stream can author a measure that violates
invariant 20 the moment a base is supplied.
Silently reading the region default base-free is **not** an option: it is
neither (A) nor (B), and it produces wrong answers rather than absent ones.
**Pin 7 — fail-closed at the operation, abstain at the invariant.** These are
different things, and draft 1 mislabelled abstention as fail-closed. The split
is deliberate:
- **At the operation — fail closed.** `CreateMeasure` and the two grid
setters (pin 9c) *refuse* when the comparison or delta they need is not
computable. Nothing incomputable enters through this rung.
- **At the invariant — abstain, and name it.** Invariant 20 emits no
violation where agreement or delta is not computable, because base-ingested
data may predate the rule and flagging it would make the invariant useless
on real scores. This is **abstention**, stated as such in the doc comment,
**not** a safety property.
File the residue as **P13-S18**: invariant 20's agreement and boundary checks
are partial, and P11-C5 resolved positions are what would close them.
**Status: open, deliberately.**
**Pin 8 — referential preconditions.**
1. The parent `StaffInstance` must be live — **ungated**, matching
`create_staff_instance`'s region check (`reduce.rs:4031`): a mint into a
non-existent parent has nowhere to go even base-free.
2. `measure.time_signature`, when `Some(id)`, must resolve to a live
`TimeSignature` — graph-aware.
3. **`measure.start`'s referents must resolve** — `start` is a `TimeAnchor`
(`graph.rs:590`), so each non-`WallClock` variant preconditions a live
referent of the right kind. Omitted from draft 1 entirely; an unresolvable
`start` would have minted a measure anchored to nothing. Graph-aware.
**Pin 8b — three new `PreconditionFailureReason` variants, all epoch 12.**
Draft 2 had two, and used `MeasureOrderUnverifiable` for a
comparable-but-reversed start, which is untruthful — that case is perfectly
verifiable and simply wrong.
| Discriminant | Variant | Meaning |
|---|---|---|
| 16 | `MeasureMeterMismatch` | a resolving `time_signature` **disagrees** with the effective grid's active signature (distinct from the resolution failure, which is `TargetMissing`) |
| 17 | `MeasureOutOfOrder` | the carried `start` is **comparable** to the current last measure's start and is not strictly after it |
| 18 | `MeasureOrderUnverifiable` | the two starts are **not comparable** (pin 6), or the delta is not computable (pin 6b) |
All three gain `introduced_minor()` arms; none compiles without one.
**Pin 9 — `CreateMeasure` is append-only, and must check BOTH clauses
prospectively.** `StaffInstance.measures` is documented "in order"
(`graph.rs:609``:611`), so the measure is pushed at the end and the operation
refuses unless:
1. the carried `start` is **comparable** to the current last measure's start
(else `MeasureOrderUnverifiable`) and **strictly after** it (else
`MeasureOutOfOrder`);
2. agreement holds against the effective grid (else `MeasureMeterMismatch`);
3. **the delta from the previous measure's start equals that measure's
governing `measure_duration()`** (else `MeasureMeterMismatch`), or the
delta is not computable (else `MeasureOrderUnverifiable`).
**Clause 3 is the gap draft 2 left open:** a strictly-later start at the
*wrong distance* is comparable, correctly ordered, agreeing — and violates
invariant 20 the instant it lands. An operation that can create an immediate
invariant violation is not preserving the invariant.
The first measure of an instance has no predecessor: clauses 1 and 3 are
vacuous for it, and **pickup/anacrusis is deferred** — a partial first measure
MUST NOT be refused or flagged. File as **P13-S19**, status open.
**Pin 9b — invariant 20 covers agreement and boundary ONLY, and `None` is
narrower than draft 2 said.** Invariant 10 already checks that a measure's
signature reference *resolves* (`invariants.rs:1180``:1212`, test `:3596`);
invariant 20 MUST NOT duplicate it. Invariant 20 checks:
1. **agreement** — a resolving `Some(id)` equals the effective grid's active
signature at that measure's start, within pin 6's relation;
2. **boundary consistency** — consecutive starts differ by the governing
`measure_duration()`, within pin 6b's delta.
**`time_signature: None` avoids ONLY the agreement clause.** Draft 2 said
"never a violation", which is wrong: `None` means *inherit*, and the inherited
meter still governs **boundary consistency**. A `None` measure at the wrong
distance from its predecessor IS an invariant-20 violation.
**Pin 9c — the invariant must be PRESERVED by everything that can break it.**
An invariant nothing maintains is decoration. Two existing operations write
the effective grid: `SetMetricGrid` (kind 22, `payload.rs:388`) and
`SetTimeSignature` (kind 25, `:392`). This rung MUST add **prospective checks
for both invariant-20 clauses** — agreement *and* boundary consistency; draft
2 specified only agreement — to:
1. both setters' forward paths, refusing with `MeasureMeterMismatch` (or
`MeasureOrderUnverifiable` when incomputable) if the *resulting* grid would
violate invariant 20 for any **live** measure;
2. **All invariant checks MUST run BEFORE any mint.** `set_time_signature`
mints its carried signature at `reduce.rs:4518``:4522`, *before* it writes
the meter change. Appending a prospective refusal after that point **leaks a
freshly minted `TimeSignature`** from an operation that reported a
precondition no-op — and from a *non-transactional* operation there is no
undo to reclaim it. Restructure so every check precedes `mint_time_signature`,
and test that a refusal leaves **no residue in `objects`, no residue in any
carried-value map, and no residue in the graph**.
3. both setters' **undo restoration** paths, because a restoration reinstating
a prior grid breaks agreement exactly as a forward write does — and
**both undo policies**, evaluated in **aggregate**:
A single undo can collect a whole-grid restoration *and* one or more
meter-change restorations together, and **their safety is not
independent**: individually-unsafe restorations can be jointly safe (a grid
restore plus the meter-change restore that re-agrees with it), and
individually-safe ones can be jointly unsafe. Per-restoration evaluation is
therefore wrong in both directions.
- `StrictInverse` / `Cascade`: evaluate invariant 20 against the
**prospective post-undo state with the whole restoration set applied**,
and `Conflicted` if that state violates it. Never accept or reject
restorations one at a time.
- `BestEffort`: apply the **maximal safe subset**, chosen by a
**deterministic, documented rule** — the canonical-order greedy: consider
restorations in the reduction's existing canonical order, admit each one
whose addition keeps the accumulated prospective state invariant-20-clean,
skip the rest. "Maximal" here means maximal under that rule, not
set-theoretically maximum; state that plainly, because the true maximum
subset is not uniquely defined and a search for it would not be
deterministic. It must not refuse wholesale, and it must not apply a
violating set.
3. `undo_strand_block`'s **existing `TimeSignature` arm** (`reduce.rs:5476`ff),
which today consults only `meter_change_chain`, extended so that a minted
`TimeSignature` still named by a live `Measure.time_signature` also blocks.
**That hole exists today**, before G3b, and is the class Packet A closed
three instances of.
**Pin 10 — undo coverage, and TWO ledgers the rung must build.**
1. `materialize_graph_tombstones` (`reduce.rs:2768`ff) gains a
`TypedObjectId::Measure(id)` arm. Structurally harder than G3a's five
root-vector `retain`s: it must reach the owning instance across
`score.canvas.regions[*].content.staff_instances`. Pin 5's parent-carrying
map is what makes that possible without a search.
2. The `Measure` strand guard **cannot use `structures`.** That index is
**event-only by design**: the endpoint walk filters `_ => None`
(`reduce.rs:1755``:1757`) and the comment at `:1763``:1765` says so
outright — "Non-event anchorings (region, measure, wall-clock, free)
contribute no entry." A guard written against it is **born green**.
3. **For spanners:** read `cross_cutting_modify_chain` (`reduce.rs:973`),
which holds the full `CrossCuttingValue` and therefore real anchors, and be
**restoration-aware** — the inverse of Packet A's pin A6, because
`ModifyCrossCutting` rewrites those values and
`ValueRestoration::CrossCutting` (`reduce.rs:842`) restores them, so a write
chain genuinely exists. **Both directions:** a modify that *removed* a
measure anchor, undone, reinstates the reference and MUST block; one that
*added* an anchor, undone, removes it and MUST NOT block.
4. **For repeat structures, that chain is structurally unusable.**
`CrossCuttingValue` is `Tie | Slur | Beam | Spanner`
(`payload.rs:980``:985`) — **`RepeatStructure` is not a variant**, so
`cross_cutting_modify_chain` can never contain one. And
`create_repeat_structure` retains no value: it inserts liveness only
(`reduce.rs:3727`ff), pushing the value into the graph when there is one.
Draft 2's instruction to "cover RepeatStructure via that chain" was
impossible.
The rung MUST therefore add **`repeat_values: BTreeMap<RepeatStructureId, RepeatStructure>`**
— or a generalized full-anchor ledger — with pin 5's **seven** sites:
declaration, `WorkingSnapshot` declaration, initialization, base seed,
create insertion, snapshot, restore. **No delete site.**
**Draft 6 claimed an eighth, delete site on a false premise, and it is
withdrawn.** The stated justification — that retaining a deleted repeat's
value would make the strand guard "block on a corpse" — contradicts the
guard itself: pin 10.5 requires the *owning* object to be `Live`, and
`delete_repeat_structure` sets `objects` to `Tombstoned`
(`reduce.rs:3766``:3768`) rather than removing the entry. A retained
tombstoned value therefore **cannot** block. The premise was wrong, so the
site is unnecessary.
**Retention is also the ratified project discipline, and this rung must not
diverge from it.** Packet A's pin A7 ruled that value maps are *not* pruned
— safe precisely because every reader consults `self.objects` first — and
the tree bears that out: there is **no `.remove` on any value map anywhere
in the reducer** (verified: zero occurrences across `staff_values`,
`instrument_values`, `time_signature_values`, `staff_group_values`). An
eighth site would make `repeat_values` the sole exception to a uniform rule,
for no benefit.
**And retention is what makes M62's repeat observation reachable at all.**
The missing-`Live`-conjunct mutant needs a state where a tombstoned repeat's
value is still present and still names the measure. Pruning on delete would
destroy exactly that state, and M62's repeat row would be **born green**
the failure mode this contract keeps rediscovering. `measure_values` reaches
seven sites by a different route (measures are mint-only, §6 ruling 1); both
maps land on seven, and the totals are unchanged at **75**. Use the helpers that already walk every anchor kind:
`RepeatStructure::anchor_sites()` (`graph.rs:1249`) and
`anchor_object_refs` (`reduce.rs:1290`), which maps `Event`, `Measure`
**and** `Region``create_repeat_structure` already relies on exactly this
pair (`reduce.rs:3717`).
5. **The inbound surface is SEVEN surface classes.** Drafts 13 covered two
(spanner, repeat) and stopped; draft 4 said "five ledgers" and conflated
the additions with the total. The seven are: **spanner, repeat, measure,
meter change, system break, page break, tempo segment.** The five beyond
spanner and repeat sit together in the reducer's declarations at
`reduce.rs:988`ff — so the omission was visible at the site the contract
was already citing:
| Surface | Ledger | Holds |
|---|---|---|
| another measure's `start` | `measure_values` (pin 5) | `Measure.start: TimeAnchor` |
| meter changes | `meter_change_chain` (`:993`) | `Option<MeterChange>``anchor: TimeAnchor` |
| system breaks | `break_chain` (`:988`) | `(TimeAnchor, bool)` |
| page breaks | `page_break_chain` (`:989`) | `(TimeAnchor, bool)` |
| tempo segments | `tempo_segment_chain` (`:994`) | `Option<TempoSegment>`**two** anchor sites: `start: TimeAnchor` and `end: Option<TimeAnchor>` (`tempo.rs:109`) |
Each chain needs **prospective and restoration-aware** treatment — every one
is a `WriteChain`, so a restoration can reinstate a measure reference the
undo was about to strand, exactly as with `cross_cutting_modify_chain`.
**`TempoSegment` does carry anchors, at two sites** — `start` and the
optional `end` (`tempo.rs:109`). Both MUST be checked; a guard reading only
`start` misses a measure named as a segment's end.
**Owner-liveness and `targets` handling is per surface, and the tempo
surface splits by KEY — not wholesale.** Six classes are always owned
(spanner, repeat, measure, meter change, system break, page break): the
guard requires the *owning* object `Live` and not in `targets`, as Packet
A's guards do.
`tempo_segment_chain` is keyed `(Option<RegionId>, MusicalPosition)`
(`reduce.rs:995`), and the two key shapes behave differently:
- **`Some(region)` — owned.** There is a live `Region` to test, so this
entry carries the full `Live` + `!targets.contains` treatment exactly like
the other six.
- **`None` — score-level, genuinely ownerless.** No object exists to test
for liveness. The guard blocks on the reference itself, exempting only a
segment whose *write* belongs to the undone transaction.
Draft 5 excluded the **entire** tempo surface from owner treatment on the
strength of the `None` case. That was an over-correction: it would leave a
region-scoped tempo segment's `Live` conjunct unguarded.
A `Measure.start` naming another measure is the subtlest: undoing measure
*B* while live measure *C* is anchored to it strands authored state inside
the very map pin 5 introduces.
6. Test rows: removal from the owning instance, ledger tombstoning,
`TargetTombstoned` on byte-identical re-carry after undo, strand refusal
from **each of the seven surfaces**, both restoration directions,
same-transaction teardown, and a tombstoned referencer not blocking — the
last two **per surface**, since each guard has its own conjuncts to get
wrong, and the score-level tempo case has no owner conjunct at all.
**Pin 11 — the `all()` count, and a mutation that compiles.**
`[GraphInvariant; 19]``; 20`, the array gains its entry, the "All 19
invariants" doc becomes 20, and `check_invariants` (`invariants.rs:229`)
dispatches it. A hand-maintained count — the class this project keeps
rediscovering (four stale sites at Push 4a, six at G2a, the golden lock at
P13-S15).
**Reverting the count literal alone is a type error** over a 20-element array,
and a mutation that does not compile signs nothing. The signing mutation is
**deleting invariant 20's arm from `check_invariants`' dispatch**, leaving the
enum and `all()` intact — so the row must assert **behaviourally** (a score
violating only invariant 20 is flagged by `check_invariants`), since
`all().len() == 20` passes with the dispatch gone.
**Pin 12 — the golden lock is not extended.** It stays
`[(OperationKind, u8); 30]`. P13-S15 remains open by design; extending it is
its own rung, so that a golden-lock extension lands with its own mutation
evidence and nothing else in the diff.
**Pin 13 — companion 0.12.0 → 0.13.0**, `epiphany-textproj/src/lib.rs:59`,
with the negative-vector anchors at `vectors.rs:533``:534` and `:941` moved
to reject `(0 12 0)`.
**Pin 14 — the four-document ritual, plus the new guard.** An
operation-vocabulary append is a documented event in **four** spec documents:
`operation_catalog.tex` (a new `CreateMeasure` section with undo semantics
and the stale-form/deferral notes), `binary_format.tex` (kind table, tag
table, payload layout, **version 0.15.0 → 0.16.0, and a Revision History
row**), `core_spec.tex` (invariant 20 in the invariant table and its
numbering prose), and `text_projection.tex` (the companion grammar).
**And `binary_format_history.rs` must gain `("G3b", "--- Genesis tranche G3b")`
in `PRINCIPAL_MARKERS`.** The guard I landed at `6170015` will fail otherwise
— deliberately. Do not weaken the guard to accommodate the rung; add the row
and the marker.
**Pin 15 — the monotonicity evidence repair.**
`spec/PLAN_GMINOR_SCHEMA_MINOR.md:201` claims real-time monotonicity with an
evidence chain ending at G2a `7df5ca1`. Extend it with **vocabulary-introducing
events only**:
| Epoch | Event | Commit | Date |
|---|---|---|---|
| 10 | G2b | `13c3d2f` | 2026-07-29 |
| 11 | G3a | `6c5e69f` | 2026-07-29 |
| 12 | G3b | this rung | — |
**G-minor and P13-S17 (`6170015`) do NOT belong in this chain** — G-minor
built the epoch machinery and P13-S17 restored a document's history; neither
introduced an additive variant, and the chain records introducing commits, not
rungs. All four dates verified with `git show -s --format=%cs`.
**The 2026-07-29 tie is resolved by ancestry, not timestamp.** `13c3d2f` is an
ancestor of `6c5e69f` (verified with `git merge-base --is-ancestor`), so G2b
precedes G3a. Record the tie-break method, exactly as the existing prose does
for the two events sharing 2026-07-07.
---
## §2. Touch table
**Draft 1 omitted mandatory integration files; drafts 23 miscounted them.**
The authority is G3a's own footprint — `git show --name-only 6c5e69f`, 35
files — not memory. Rows 614 are **nine** files, not ten; the tenth omitted
G3a-footprint file is **`core/DECISIONS.md`, carried in row 35**. Row 14a
(`QUICKSTART.md`) and row 12a (`decode.rs`) are further surfaces found in later
review rounds. Every one of them fails **silently**, none at compile time.
**Row 12a is a pre-existing live bug, not just this rung's plumbing.**
`decode.rs`'s `precondition_reason` (`:205`) ends at **13** and rejects
everything higher, so `AcousticRealizationPinned` (14) and
`TranspositionOutOfRange` (15) **encode but cannot decode today** — a
materialized effect carrying either fails canonical round-trip. Nothing caught
it because the testkit's generator draws `rng.below(14)` (`generators.rs:417`)
under a doc comment claiming "every core and registered variant." This rung
repairs the decoder through **18** and the generator through **18**, and files
the pre-existing half as **P13-S20**.
| # | File | Change |
|---|---|---|
| 1 | `ops/src/payload.rs` | `CreateMeasureOp`; `OperationKind::CreateMeasure`; `discriminant()` 39; `introduced_minor()` `Some(12)`; tag `39 => "create-measure" @ Some(12)`; `CanonicalEncode` (parent then value); **no `schema_major` arm** |
| 2 | `ops/src/effect.rs` | `MeasureMeterMismatch = 16`, `MeasureOutOfOrder = 17`, `MeasureOrderUnverifiable = 18`; three `introduced_minor()` arms → `Some(12)` |
| 3 | `core/src/codec.rs` | `Measure` in `canonical_value!` |
| 4 | `core/src/invariants.rs` | invariant 20; `number()` arm; `all()` `19`→`20` + entry + doc; `check_invariants` dispatch; the check body (pins 6, 6b, 9b); doc-comment abstention + pickup deferrals |
| 5 | `ops/src/reduce.rs` | `create_measure` (pins 8, 9); parent-carrying **`measure_values` ×7 sites**; **`repeat_values` ×7 sites** (no delete site — pin 10.4); **`instance_grid` ×7 sites** and the **shared effective-grid oracle** reconstructing inheritance from `metric_grid_chain` + `meter_change_chain` in **canonical write order**, folding in prospective restorations, run in **both** reduction modes (pin 6c, M30aM30c, M31a); `Measure` graph-removal arm; the `Measure` strand guard across **all seven surfaces**`cross_cutting_modify_chain` (spanner), `repeat_values`, `measure_values`, `meter_change_chain`, `break_chain`, `page_break_chain`, `tempo_segment_chain` (both anchor sites) — restoration-aware on the five `WriteChain` surfaces; **`SetMetricGrid`/`SetTimeSignature` preservation, both clauses, both undo policies, all checks before any mint; `TimeSignature` strand extension (pin 9c)** |
| **6** | `ops/src/envdecode.rs` | envelope decode arm for kind 39 |
| **7** | `ops/src/migrate.rs` | migrate-on-read handling |
| **8** | `ops/src/v0.rs` | v0 wire path |
| **9** | `ops/src/vectors.rs` | operation wire vector for kind 39 |
| **10** | `ops/src/valuegen.rs` | a `measure(...)` fixture — see §3's length-distinguishability requirement |
| **11** | `ops/src/fuzz.rs` | fuzz arm |
| **12** | `ops/src/textproj_kind.rs` | kind ↔ projection mapping — **this is where the operation-kind production lives**, not `textproj/project.rs` |
| **12a** | `ops/src/decode.rs` | `precondition_reason` (`:205`) must decode **16, 17, 18** — and **14, 15**, which it already omits. See below |
| **13** | `spec/vectors/decode_vectors.txt` | frozen cross-impl decode vectors — **append only** |
| **14** | `spec/vectors/textproj_document_vectors.txt` | frozen projection vectors — **append only** |
| **14a** | `spec/QUICKSTART.md:56` | "the 19 graph invariants" → 20. A stale surface absent from every prior draft; it is prose, so it fails silently forever |
| 15 | `ops/src/lib.rs` | re-exports |
| 16 | `core/src/graph.rs` | `Measure` / `measures` docs: append-only ordering, the abstention, `None`-inherits-boundaries |
| 17 | `textproj/src/lib.rs:59` | `COMPANION_VERSION``(0, 13, 0)` |
| 18 | `textproj/src/parse.rs` | **companion-version fixture only.** `project.rs` has **zero** operation-kind productions (verified: 0 matches for the G3a kinds, vs 4 in `ops/src/textproj_kind.rs`), so drafts 13 named the wrong file |
| 19 | `textproj/src/vectors.rs` | positive vector; negative anchors → `(0 12 0)` |
| 20 | `editor-core/src/barriers.rs` | one `subjects_of` arm — **the only authorized editor-crate change** |
| 21 | `layout-ir/src/barrier.rs:1179` | `tag: 39``40` |
| 22 | `testkit/tests/text_projection_grammar.rs:316``:317` | `40`, `"39 payload-free kinds…"` |
| 23 | `testkit/src/generators.rs` | draw arm; comment `30..=39`; `saw_create_measure` + assertion; **`precondition_failure_reason`'s `rng.below(14)` (`:417`) → `below(19)` with arms 1418** — its doc comment claims "every core and registered variant" and is false today |
| 24 | `testkit/src/layout_stub.rs:1375` | comment `30..=39` |
| 25 | `testkit/src/roundtrip.rs` | **unconditional**: verify at ratification whether the kind list is enumerated there, and record the finding either way. Draft 2's "if the kind list is enumerated there" made a touch row depend on the implementer's reading |
| 26 | `testkit/tests/binary_format_history.rs` | `("G3b", "--- Genesis tranche G3b")` in `PRINCIPAL_MARKERS` |
| 27 | `spec/operation_catalog.tex` | `CreateMeasure` section incl. undo semantics; **the three new precondition reasons 1618**; **document version bump + changelog entry** |
| 28 | `spec/binary_format.tex` | kind table, tag table, payload layout, **the three new `PreconditionFailureReason` discriminants 1618**, **0.15.0 → 0.16.0**, Revision History row |
| 29 | `spec/core_spec.tex` | invariant 20 + the numbering prose, **and the normative `OperationKind` / `OperationKindTag` listings, which must gain `CreateMeasure`** — drafts 13 named only the invariant |
| 30 | `spec/text_projection.tex` | companion grammar, **plus the document title/version bump and a changelog entry** — not merely the grammar |
| 31 | four `.pdf` | regenerated |
| 32 | `spec/PLAN_GMINOR_SCHEMA_MINOR.md` | epoch 12 row; **pin 15's evidence repair** |
| 33 | `spec/PLAN_GENESIS_OPS.md` | ladder CLOSED |
| 34 | `spec/PASS13_CANDIDATES.md` | **P13-S18** (pin 7 abstention residue, **open**), **P13-S19** (pin 9 pickup deferral, **open**), **P13-S20** (the reasons-14/15 decoder hole, **RESOLVED in this rung**) |
| 35 | `core/DECISIONS.md`, `ops/DECISIONS.md` | the rung's record |
| 36 | this contract | DRAFT → RATIFIED |
**No `epiphany-bundle` change** (pin 2: no accept-set move). No editor-crate
change beyond row 20.
---
## §3. Mutation plan — seventy-five mutations, ratified
Draft 2 named outcomes without edits for most rows. Every mutation below is a
**specific edit to a specific site**, and each must be **observed** red.
Reasoning that a mutation "would fail" signs nothing.
### Numbering rule
**One M = one edit.** Draft 3 claimed 52 while M48 and M50 each specified two
distinct edits, so the stated total was not a count of anything. Where one edit
is observed across several rows, that is stated and it remains **one** M.
### Vocabulary — both spaces, all five epoch sites (M1M10)
Draft 2's single "t1" collapsed all of this into one tag-epoch mutation. There
are **two** discriminant spaces and **two** epoch functions for the kind
(`OperationKind::introduced_minor()` at `payload.rs:431`, the tag vocabulary's
at `:723`), plus three reasons with a discriminant and an epoch each.
| M | Edit |
|---|---|
| M1 | `OperationKind::discriminant()` `CreateMeasure` 39 → 40 |
| M2 | tag vocabulary `CreateMeasure` 39 → 40 |
| M3 | `OperationKind::introduced_minor()``Some(11)` |
| M4 | tag vocabulary `@ Some(12)``Some(11)` |
| M5, M6, M7 | each new reason's discriminant → 19 (unused, so it compiles) |
| M8, M9, M10 | each new reason's `introduced_minor()``Some(11)` |
### Wire (M11M14)
| M | Edit | Row |
|---|---|---|
| M11 | add `CreateMeasure(_) => 3` to `schema_major()` | a block containing **only** a `CreateMeasure` envelope stamps major **0**; `OperationEnvelopeBlock` stays 3. The block composition is load-bearing: a mixed block would stamp from another kind and pass with M11 applied |
| M12 | reorder `struct_codec!(Measure)`'s field list | frozen literal bytes |
| M13 | swap parent/value order in `CreateMeasureOp::encode_canonical` | frozen literal bytes. Signs pin 4 |
| M14 | **weaken the strict re-encode rejection** — make `decode_canonical` return the decoded value without comparing the re-encoding | strict-canonicality row |
**M14 is not "remove `Measure` from `canonical_value!`".** That does not
compile: the new envelope decode arm calls `value::<Measure>`, whose bound is
`T: CanonicalValue` (`envdecode.rs:224`), so dropping the macro entry breaks the
build and signs nothing.
**And the row must use a structurally decodable but NONCANONICAL encoding.** A
field-swapped byte string is likely rejected by ordinary structural decoding,
which proves nothing about the macro's re-encode check.
**The fixture is concrete and verified — the row is signable, with no
conditional.** Encode `Measure.start` as a `Musical` offset whose
`RationalTime` is spelled **unreduced, `2/4`**. That decodes cleanly:
`RationalTime`'s `decode_canonical` ends in
`Ok(RationalTime::from_big(BigRational::new(numer, denom)))`
(`time.rs:352`ff), and `BigRational::new` **reduces on construction**, so the
value becomes `1/2`. Re-encoding then emits `1/2`'s magnitudes, which differ
from the `2/4` bytes fed in — so the strict per-value re-encode comparison is
the *only* thing standing between the input and acceptance, which is exactly
what M14 must prove.
Draft 5's "if none exists, mark the row unsignable" branch is **removed**: it
contradicted §3's no-deviation rule, and the encoding demonstrably exists.
### Mint (M15M22)
| M | Edit | Row |
|---|---|---|
| M15 | skip the `measure_values` base seed | **base**-recarry only — it cannot reach the from-empty row |
| M16 | make the value comparison always report `identical` | differing re-carry observes `AlreadyApplied` |
| M17 | drop `StaffInstanceId` from the comparison | parent-mismatch re-carry. Signs pin 5 |
| M18 | remove the parent-liveness check | dead parent, graph-aware **and** base-free legs (ungated) |
| M19 | remove the `time_signature` resolution check | unresolvable signature |
| M20 | remove the `start`-referent resolution check | three observations — `Event`, `Measure`, `Region`. Signs pin 8.3 |
| M21 | append unconditionally | ordering precondition |
| M22 | report `MeasureOrderUnverifiable` for a comparable-but-reversed start | must observe the wrong reason. Signs pin 8b |
### Comparability (M23M26)
| M | Edit | Row |
|---|---|---|
| M23 | allow `Musical` vs `WallClock` offsets | cross-clock must be `MeasureOrderUnverifiable` |
| M24 | widen c3 to admit `pos: End` or nonzero offsets | a nonzero-offset measure anchor must be **not comparable** |
| M25 | **order across differing `pos`/`edge`** (restore draft 3's `Start < End`) | `Region{Start, Musical(100)}` vs `Region{End, Zero}` must be **unverifiable**. Signs pin 6's identical-selector rule |
| M26 | **subtract offsets across differing `pos`/`edge`** in the delta | the delta must be **not computable**, not a wrong number. Signs pin 6b |
### Grid oracle and the instance-grid ledger (M27M31)
| M | Edit | Row |
|---|---|---|
| M27 | on multiple mutually-incomparable maxima, pick by document order | must refuse (`MeasureOrderUnverifiable`), never fabricate a total order |
| M28 | drop **step 0**, letting an empty candidate set win | a sequence whose changes are **all incomparable** must be indeterminate, **not** vacuous. Signs pin 6c step 0 |
| M29 | treat an empty candidate set as a violation | a **genuinely empty** `meter_sequence` — no active signature is **vacuous**, not a violation |
| M30 | skip the `instance_grid` base seed | base-free must still see a local override |
| M30a | reconstruct the inherited grid from `metric_grid_chain` **only**, ignoring `meter_change_chain` | layered per-key changes must reach the oracle |
| M30b | in graph-aware mode, read the effective grid from `score` instead of the shared oracle | both modes must run the **same** oracle |
| M30c | overlay **current** per-key `meter_change_chain` values onto the **current** whole grid, ignoring write order | the two chains must interleave in **canonical order**. Tests must cover **both** interleavings: a `SetTimeSignature` **before** a later `SetMetricGrid` (the whole-grid write supersedes the earlier per-key change) and **after** it (the per-key change overlays). M30a/M30b/M31a are all satisfiable by an order-ignoring oracle, so this is a distinct obligation |
| M31 | read the region default while a local override exists | the override must win |
| M31a | omit prospective undo restorations from the reconstruction | the grid a restoration **would** install must govern the check |
**Disposition (A) is ratified**, so M30, M30a, M30b, M31 and M31a all apply.
**The total is 75.** The arithmetic has moved twice, each time because a
review round added a real obligation rather than because the count drifted:
- **71 → 74.** (A) keeps M30/M31 as two rather than collapsing them, and the
confirmed `TempoSegment` double-site keeps M58 — but disposition (A)'s oracle
carries three obligations draft 4 had no mutations for: reconstruct
inheritance from **both** chains (M30a), fold in **prospective restorations**
(M31a), and run in **both** reduction modes (M30b).
- **74 → 75.** M30a/M30b/M31a are each satisfiable by an oracle that ignores
write order, so **canonical interleaving was required but unsigned**;
**M30c** signs it.
Counted mechanically: **71 rows, two of which carry three ids each → 75
distinct single-edit mutations.**
### Create-side clauses (M32M33)
| M | Edit | Row |
|---|---|---|
| M32 | remove `create_measure`'s agreement check | disagreeing signature |
| M33 | remove `create_measure`'s **delta** check | a strictly-later start at the **wrong distance** must be refused — the immediate-violation gap |
### Invariant 20 (M34M40)
| M | Edit | Row |
|---|---|---|
| M34 | remove the agreement clause | flags disagreement |
| M35 | remove the boundary clause | flags wrong distance |
| M36 | skip the boundary clause when `time_signature` is `None` | `None` inherits and is still bound |
| M37 | flag the incomparable case | abstention |
| M38 | flag a pickup first measure | deferral |
| M39 | add a resolution check to invariant 20 | an unresolvable reference is an invariant-**10** violation and **not** a 20 |
| M40 | delete invariant 20's arm from `check_invariants`' dispatch | asserted **behaviourally**; reverting `all()`'s count is a type error and signs nothing |
### Preservation (M41M47)
| M | Edit | Row |
|---|---|---|
| M41 | remove `SetMetricGrid`'s agreement precondition | |
| M42 | remove `SetMetricGrid`'s boundary precondition | draft 2 checked agreement only |
| M43 | remove `SetTimeSignature`'s agreement precondition | |
| M44 | remove `SetTimeSignature`'s boundary precondition | |
| M45 | **move the checks back after `mint_time_signature`** (`reduce.rs:4518`) | a refused `SetTimeSignature` must leave **no residue** in `objects`, in any carried-value map, or in the graph. Signs pin 9c.2 |
| M46 | evaluate restorations **individually** under `StrictInverse` | a jointly-safe grid + meter-change set must be **applied**, and a jointly-unsafe individually-safe set must **conflict** — both observed |
| M47 | replace `BestEffort`'s canonical-order greedy with a naive per-restoration filter | the safe subset must be the documented deterministic one |
### Undo — seven surface classes (M48M62)
| M | Edit | Row |
|---|---|---|
| M48 | remove the `TimeSignature`↔`Measure` strand extension | closes a hole live **today** |
| M49 | remove the `Measure` graph-removal arm | removal from the owning instance |
| M50 | rewrite the `Measure` strand guard against `structures` | **must go red**, proving the event-only index cannot see measure anchors (`reduce.rs:1755``:1765`) |
| M51 | remove the spanner surface | |
| M52 | remove the repeat surface | signs pin 10.4 — `CrossCuttingValue` has no `RepeatStructure` variant |
| M53 | skip the `repeat_values` base seed | |
| M54 | remove the **`measure_values`** surface | another `Measure.start` anchored to the undone measure |
| M55 | remove the **`meter_change_chain`** surface | |
| M56 | remove the **`break_chain`** surface | |
| M57 | remove the **`page_break_chain`** surface | |
| M58 | remove the **`tempo_segment_chain`** surface | **confirmed applicable**: `TempoSegment` carries `start: TimeAnchor` **and** `end: Option<TimeAnchor>` (`tempo.rs:109`). Must be observed at **both** sites — a `start`-only guard misses a measure named as a segment's end |
| M59 | drop restoration-awareness on the shared prospective-value path, **reinstated-reference** direction (one edit) | must block. **Observed on all five restoration-capable surfaces**: spanner (`cross_cutting_modify_chain`), meter change, system break, page break, and tempo segment — the last at **both** anchor sites. Six observations, one mutant |
| M60 | drop the **added-anchor** direction on the same shared path (one edit) | must **not** block. Same five surfaces, same six observations |
| M61 | drop `!targets.contains(…)` from every surface guard (one edit) | observed on **all seven** surface rows — same-transaction teardown |
| M62 | drop the `Live` conjunct from every owned-surface guard (one edit) | observed on **seven** rows — the six always-owned classes **plus region-scoped (`Some(region)`) tempo segments**, which have a live `Region` owner. Only **score-level (`None`) tempo** is N/A. M58 cannot substitute here: it removes tempo handling wholesale and so cannot detect a *missing `Live` conjunct* in tempo handling that is present |
**Restoration is N/A for exactly two surfaces.** `repeat_values` and
`measure_values` are **immutable value maps**, not `WriteChain`s — nothing
rewrites a repeat's or a measure's anchors in place, so there is no prospective
post-undo value that could differ from the stored one. Drafts 45 left M59/M60
as single generic observations, which signed the *mechanism* but not its
*coverage*: a prospective-value path wired up for spanners only would have
passed both. One mutant each is still correct — the path is shared — but **every
applicable row must observe it**, and the two N/A surfaces must be named as N/A
rather than silently absent.
### Post-undo re-create ordering (M63)
| M | Edit | Row |
|---|---|---|
| M63 | make `create_measure`'s `Tombstoned` arm fall through to the value-map identity check | a **byte-identical** re-carry after undo must observe `TargetTombstoned`, not `AlreadyApplied`. Draft 3 had regression coverage here and **no killing mutation** |
### Precondition-reason decoding (M64M66) — and a live bug
`decode.rs`'s `precondition_reason` (`:205`) ends at **13**. Reasons **14 and
15 already encode but cannot decode**, so a materialized effect carrying either
fails canonical round-trip **today**. Nothing caught it because
`generators.rs:417` draws `rng.below(14)` under a doc comment claiming "every
core and registered variant."
| M | Edit | Row |
|---|---|---|
| M64 | revert the decoder to end at 13 | reasons 1618 round-trip |
| M65 | remove decoder arms **14/15** | the pre-existing hole, now regression-locked (**P13-S20**) |
| M66 | revert `generators.rs` `below(19)``below(14)` | the generator must actually reach every variant its doc claims |
### Sentinels and bookkeeping (M67M71)
| M | Edit | Row |
|---|---|---|
| M67 | `barrier.rs` `tag: 40``39` | invalid-tag sentinel |
| M68 | revert the grammar count | grammar sentinel |
| M69 | revert the `saw_create_measure` assertion | generator sentinel |
| M70 | revert `COMPANION_VERSION` | companion accepts 0.13.0, rejects `(0 12 0)` |
| M71 | delete the G3b Revision History row | the `binary_format_history.rs` guard |
**Ratified total: 75 mutations, one edit each.** Every former conditional is
resolved — disposition (A) is ratified, `TempoSegment` carries two anchor sites,
and M14's fixture is concrete — so **no deviation clause remains anywhere in
this contract**. Report the observed count; any deviation from 75 is a finding.
Prose-only sentinel sites (`layout_stub.rs:1375`, `QUICKSTART.md:56`) have no
mutation; they are covered by the §4 grep gate, and the report must say so
rather than implying mutation coverage.
### Not mutations — gates
Draft 2 listed these among the mutations, which overstated its own coverage:
- **Frozen-corpus byte stability**: `spec/vectors/*.txt` gain entries and **no
existing vector's bytes move** — a corpus **diff gate**, verified by
inspecting the diff, not by mutating anything.
- **PDF title pages** read the bumped versions.
### The `valuegen` fixture trap, from G3a
G3a's `analysis_layer` fixture happened to encode a name at exactly 16 bytes —
the same width as the id — making a field-swap mutation **byte-invisible**.
`Measure` has five fields including two `Option`s and a `TimeAnchor`. Choose
fixture values whose encodings are **mutually distinguishable in length**, and
state in the report that this was checked. M12/M13's frozen literals are what
make it matter.
### Anti-traps
A mutation that does not compile signs nothing. A mutation in an operation that
runs *before* the asserted state cannot reach it. A symmetric encode/decode
change is invisible to round-trip assertions. A grep guard must be sliced so it
cannot match its own citation, and *name-presence* is not enough when the name
already appears in neighbouring prose. **A guard written against an index that
structurally cannot hold the referent is born green.** A row asserted but not
observed red is unsigned.
---
## §4. Gate
- `cargo test --workspace` — count compared to the **1454** baseline at
`6170015`, delta explained.
- `cargo clippy --workspace --all-targets` — zero warnings.
- `cargo fmt --check` — clean.
- All **75** mutations observed red and restored by hand-editing back.
- The two corpus diff gates and the four PDF title pages.
- A grep gate over the prose-only sentinels (`layout_stub.rs:1375`,
`QUICKSTART.md:56`) confirming no stale count survives.
- Whitespace per §5.
## §5. Whitespace and staging
1. Stage the touch-table files explicitly. **Never `git add -A`.**
2. **`git diff --cached --check`** — catches staged and formerly-untracked
files. `git diff --check` alone is blind to both, which is how this
session's own contract sat untracked past a "clean" gate.
3. Commit.
4. `git diff --check <parent>..HEAD -- crates/ spec/` — path-scoped, so the
pre-existing `spikes/` failure cannot mask a real one.
**A concurrent session commits to this repository.** Re-check `HEAD` before
committing, commit with an explicit pathspec, and never run `git reset`,
`git restore --staged`, `git checkout`, or `git stash` against the shared
index.
## §6. Boundary — unchanged and absolute
MUST NOT be read, written, or staged: `spec/PLAN_EDITOR_APP.md`,
`spec/CONTRACT_EDITOR_*.md`, `spec/ANALYSIS_GENESIS_PERSISTENCE.md`,
`spec/ANALYSIS_TEXT_RUN_PRIMITIVES.md`, `spec/DRAFT_T4_FIXTURE_RECIPE.md`,
`crates/epiphany-editor-gui/goldens/*.png`, `crates/epiphany-render-svg/**`,
`crates/epiphany-glyphs/**`,
`crates/epiphany-testkit/benches/editor_pipeline.rs`, the entire `spikes/`
tree, the root `Cargo.toml` change, `.claude/worktrees/`.
**Touch-table row 20** (`barriers.rs`, one `subjects_of` arm) is the **only**
authorized editor-crate change, and it does not generalize beyond this packet.
## §7. Report requirements
Baseline and final counts; **all 75 mutations** with their **observed** failing
test names; the four PDF title pages; anything the contract did not anticipate.
Confirm explicitly:
- `schema_major()` gained **no** arm, and the wire row used a
**`CreateMeasure`-only** block;
- the golden lock still reads `; 30]`;
- the evidence chain lists **only** the three vocabulary-introducing events;
- **all nine** integration files in rows 614 were touched, plus row 12a
(`decode.rs`), row 14a (`QUICKSTART.md`), and `core/DECISIONS.md` in row 35 —
which is the tenth omitted G3a-footprint file;
- that disposition **(A)** was implemented — the instance ledger **plus** the
shared oracle reconstructing inheritance from `metric_grid_chain` and
`meter_change_chain` in canonical order, folding in prospective restorations,
and running in **both** reduction modes (M30a, M30b, M31a);
- that M58 was observed at **both** `TempoSegment` anchor sites;
- that M14 used the ratified unreduced-`RationalTime` (`2/4`) fixture and
observed the strict re-encode rejection;
- that **no mint precedes any invariant check** in either setter, and that a
refusal leaves no residue in `objects`, the value maps, or the graph;
- that restoration safety was evaluated in **aggregate**, with `BestEffort`'s
subset chosen by the documented canonical-order greedy;
- that **all seven** surface classes have guards, with the per-surface owner
rule stated — M62 observed on **seven** rows (six always-owned plus
region-scoped tempo), with only score-level (`None`) tempo N/A;
- that M30c observed **both** grid-chain interleavings;
- that M59 and M60 were each observed on **all five** restoration-capable
surfaces — six observations apiece, tempo at both anchor sites — and that
`repeat_values` and `measure_values` were recorded as **N/A** for restoration
rather than silently omitted;
- the physical site counts actually implemented: `measure_values` **7**,
`instance_grid` **7**, `repeat_values` **7** — **no delete site on any of
them**, and tombstoned values deliberately retained (pin 10.4);
- that M62's repeat observation used a state where a **tombstoned repeat's
value is retained** and still names the measure;
- that `decode.rs` decodes reasons **14 through 18** and `generators.rs` draws
all of them — **P13-S20** filed as RESOLVED;
- whether `roundtrip.rs` enumerates kinds (row 25) — **report the finding
either way**;
- **no existing vector's bytes moved** in either frozen corpus;
- the `valuegen` fixture's field encodings are length-distinguishable;
- M50 was observed red — the strand guard genuinely cannot be written against
the event-only `structures` index;
- **P13-S18** and **P13-S19** were filed with status **open**.
If a pin is wrong or unsatisfiable, **stop and say so.** Pins 2, 9b, and 12
are prohibitions. Pin 6 forbids inventing a comparable relation beyond its five
shapes, and pin 6b forbids treating an ordering as a delta — the correct
response to an incomputable case is to refuse at the operation and abstain at
the invariant. Pin 9c widens this rung into two pre-existing operations, both
undo policies, and one pre-existing undo hole; **if that scope is wrong, say so
before writing code** rather than delivering an invariant nothing maintains.