epiphany/spec/CONTRACT_GENESIS_G3A_UNDO_R...

533 lines
26 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: the G3a undo repair, and the Binary Format chronology restoration
**Status:** **RATIFIED 2026-07-29.** Two packets, two commits. Pins A3
(ungated ledger/map guards), A6 (no restoration lookup while those fields
have no write chain — revisit only if a modifying operation is introduced),
and A7 (retaining the maps is safe given A4's live-state filter and
object-first recreate refusal) were ratified explicitly. Mutation budget
ratified at twenty-two for Packet A and three for Packet B.
**Governs:** the defect that blocks G3a sign-off (Packet A), and P13-S17
(Packet B).
**Predecessor:** `spec/CONTRACT_GENESIS_G3A_ENTITIES.md`, landed at `6c5e69f`.
---
## §0. What was actually verified
Every claim below was read out of the code, not relayed.
**The tombstone branch is reachable.** `mint_container`
(`crates/epiphany-ops/src/reduce.rs:3922`) calls `note_minted`
(`:7848`), which pushes the object into `tx_minted` whenever
`self.current_tx` is set. `undo_transaction` (`:5288`) reads
`tx_minted` and `tombstone_undo_targets` (`:5387`) writes
`ObjectState::Tombstoned` into `objects` for every target. All four G3a
reducers reach `mint_container`: `create_staff_group` `:4283`,
`create_part_definition` `:4340`, `create_analysis_layer` `:4384`,
`create_view` `:4437`. `create_instrument` `:4214` and `create_staff`
`:4165` do the same.
Therefore `crates/epiphany-ops/DECISIONS.md:1986` — "`ObjectState::Tombstoned`
is unreachable for any of them through the public operation API today" — is
**false**, and was false for `CreateStaff` and `CreateInstrument` before this
rung. It is a pre-existing wrong claim that G3a inherited and repeated.
**The graph keeps ghosts.** `materialize_graph_tombstones`
(`:2737`) walks `targets` at `:2768` with arms for `Pitch`, `Voice`,
`Slur`, `Tie`, `Beam`, `Spanner`, `RepeatStructure`, `Staff`,
`TimeSignature`, then `_ => {}` at `:2810`. There is no arm for
`StaffGroup`, `PartDefinition`, `AnalysisLayer`, `View`, or `Instrument`.
The ledger tombstones; `Score.staff_groups` / `.parts` / `.analysis_layers`
/ `.views` / `.instruments` keep the value. Ledger and graph disagree —
the exact failure the `Spanner` arm's own comment at `:2793` records as
having been fixed once already.
**The strand guard is silent on the new families.** `undo_strand_block`
(`:5458`) matches `Staff` and `TimeSignature`, then `_ => None` at
`:5500`. Undoing a `StaffGroup` mint while a live `Staff.group`
(`crates/epiphany-core/src/graph.rs:825`) names it strands the reference;
likewise `AnalysisLayer``ViewDefinition.active_layers` (`graph.rs:1652`)
and `Instrument``Staff.instrument` (`graph.rs:817`).
`PartDefinition` and `View` need no guard: nothing in the graph references
them. Their own fields (`PartDefinition.staves` `graph.rs:1636`,
`ViewDefinition.active_layers`) are **outbound**, and removing the holder
strands nothing.
**Count check.** `spec/PLAN_GMINOR_SCHEMA_MINOR.md:198` says "all ten
kind/tag pairs." The epoch table at `:170``:180` lists 2427 (4), 2829
(2), 30 (1), 31 (1), 3233 (2), 34 (1), 3538 (4) = **fifteen**.
**Binary Format chronology.** `spec/binary_format.tex` history rows: G2a
0.12.0 (`:3599`), G-minor 0.13.0 (`:3628`), G3a 0.14.0 (`:3643`). The
ladder order is G1 → G2a → G-minor → G2b → G3a, so **G2b is missing**
between 0.13.0 and G3a's row, and the accept-set raise
`OperationEnvelopeBlock` 2→3 that G2b performed is recorded nowhere in the
revision history — G3a's row at `:3657` merely observes the block "stays at
3 where genesis tranche G2b left it."
**G1 has no standalone history row, by design.** It is recorded
retroactively *inside* the G2a row: the principal marker is at `:3599`
(`0.12.0 --- Genesis tranche G2a`), and G1 appears nested at `:3603` as
"(`CreateInstrument`, genesis tranche G1 --- landed at `3b09595` with no
matching entry here)". The row is explicit that G1 never got its own entry.
Pin B6 is scoped accordingly.
---
# PACKET A — the undo repair
## §A1. Pins
**Pin A1 — five graph-removal arms.** In `materialize_graph_tombstones`
(`reduce.rs:2768`), before the `_ => {}` catch-all, add:
| Target | Removal |
|---|---|
| `TypedObjectId::StaffGroup(id)` | `score.staff_groups.retain(\|v\| v.id != *id)` |
| `TypedObjectId::PartDefinition(id)` | `score.parts.retain(\|v\| v.id != *id)` |
| `TypedObjectId::AnalysisLayer(id)` | `score.analysis_layers.retain(\|v\| v.id != *id)` |
| `TypedObjectId::View(id)` | `score.views.retain(\|v\| v.id != *id)` |
| `TypedObjectId::Instrument(id)` | `score.instruments.retain(\|v\| v.id != *id)` |
Each mirrors the existing `Staff` (`:2804`) and `TimeSignature` (`:2807`)
arms exactly. **No new `RepairRecord`**: `tombstone_undo_targets` already
pushes one `CascadeDeleted` per target at `:5427` before calling this
function, and none of the existing arms push more.
**Pin A2 — three inbound-reference guards.** In `undo_strand_block`
(`:5458`), before `_ => None`:
| Target | Blocked by | Read from |
|---|---|---|
| `StaffGroup(g)` | a live `Staff` whose `group == Some(g)` | `self.staff_values` |
| `AnalysisLayer(l)` | a live `ViewDefinition` whose `active_layers` contains `l` | `self.view_values` |
| `Instrument(i)` | a live `Staff` whose `instrument == i` | `self.staff_values` |
Return `Some((*target, referencer_obj))` on the first match in map order.
**Pin A3 — the guards are ledger-based and ungated.** They read the
carried-value maps (`staff_values` `:1001`, `view_values` `:1018`), **not**
`self.graph`, and are **not** wrapped in `if self.graph.is_some()`.
Rationale, and it is a real decision: the *create*-side referential
preconditions **are** graph-gated (`create_staff:4137`,
`create_staff_group:4266`, `create_view:4420`) because base-free reduction
has no universe to resolve against. The *undo* side is different — the
carried-value maps are populated by mints regardless of graph presence and
are seeded from base when there is one, so under base-free reduction the
ledger still knows that a live staff names this group. Gating the guard
would let base-free undo strand a reference the ledger can plainly see.
The two existing guards (`instance_staff`, `meter_change_chain`) are
likewise ungated. **Stronger than the create side, and strictly safer.**
**Pin A4 — liveness is read from `objects`, not from map presence.** The
value maps are insert-only; `tombstone_undo_targets` never removes from
them. A guard that treated map presence as liveness would let an
already-tombstoned staff block an unrelated undo forever. Each guard MUST
require `matches!(self.objects.get(&referencer_obj), Some(ObjectState::Live))`.
**Pin A5 — same-transaction referencers do not block.** Each guard MUST
also require `!targets.contains(&referencer_obj)`, matching the `Staff`
guard's `!targets.contains(&iobj)` at `:5471` and the doc comment at
`:5456`. A transaction that mints a group and a staff in it must be
undoable whole.
**Pin A6 — `restorations` is not consulted by the three new guards.** The
`TimeSignature` guard consults it (`:5483`) because `MeterChange` has a
write chain that undo may restore to a prior value. `Staff.group`,
`Staff.instrument` and `ViewDefinition.active_layers` have **no modify
operation at all** — that is the standing §1.1 condition — so no
restoration can change the prospective post-undo value. The implementer
MUST NOT cargo-cult the restoration lookup; its presence would be dead
code asserting a write chain that does not exist.
**Pin A7 — the value maps are not pruned on undo.** `tombstone_undo_targets`
leaves `staff_group_values` and siblings untouched, matching the existing
`staff_values` / `time_signature_values` / `instrument_values` precedent.
This is safe **because every create reducer consults `self.objects` before
its value map**. The `match self.objects.get(..)` lines, not the function
declarations: `create_staff_group` `:4241`, `create_part_definition`
`:4298`, `create_analysis_layer` `:4356`, `create_view` `:4395`,
`create_staff` `:4109`, `create_instrument` `:4186`. A re-create after undo
therefore hits `Some(ObjectState::Tombstoned)``TargetTombstoned` and
never reaches the stale value. Row family **u4** signs that ordering, and it
is the only family that does — a re-create assertion bolted onto u1 would be
unsigned regression coverage, since u1's arm-deletion mutation cannot reach
it (§A3).
**Pin A8 — Instrument is in scope as collateral.** It is the same root
cause, it already has normative undo semantics, and leaving it would mean
knowingly shipping a fifth instance of a defect this packet exists to fix.
**Pin A9 — no new `OperationKind`, tag, epoch, discriminant, wire layout,
accept-set, or companion version.** This packet changes reducer behaviour
only. `spec/binary_format.tex` is **not** touched by Packet A. If any of
those surfaces appears in the diff, the packet is wrong.
**Pin A10 — DECISIONS.md correction of record.** Replace the false
paragraph at `crates/epiphany-ops/DECISIONS.md:1986` with a correction that
states plainly: the branch **is** reachable via `UndoTransaction` over a
transaction containing the mint; it was reachable for `CreateStaff` and
`CreateInstrument` before G3a; the claim as written was wrong; and the
guards and removal arms this packet adds are what make it correct. Do not
silently delete it — the wrong claim was signed off and the record must say
so.
## §A2. Touch table (Packet A)
| # | File | Change |
|---|---|---|
| 1 | `crates/epiphany-ops/src/reduce.rs` | five arms in `materialize_graph_tombstones` (pin A1) |
| 2 | `crates/epiphany-ops/src/reduce.rs` | three arms in `undo_strand_block` (pins A2A6) |
| 3 | `crates/epiphany-ops/src/reduce.rs` | doc comment on `undo_strand_block` `:5452` naming the three new blocks |
| 4 | `crates/epiphany-ops/src/reduce.rs` | test rows u1au1e, u2au2c, u2bf-ac, u2tomb-ac, u3au3c, u4au4e |
| 5 | `crates/epiphany-ops/DECISIONS.md` | pin A10 correction; record the five arms, three guards, and pin A3's ruling |
| 6 | `spec/PLAN_GENESIS_OPS.md:25` | "commit pending" → `6c5e69f`; note the undo repair rides after it |
| 7 | `spec/PLAN_GMINOR_SCHEMA_MINOR.md:190` | "introducing commit pending" → `6c5e69f` |
| 8 | `spec/PLAN_GMINOR_SCHEMA_MINOR.md:198` | "all ten kind/tag pairs" → "all fifteen kind/tag pairs" |
| 9 | `spec/CONTRACT_GENESIS_G3A_UNDO_REPAIR.md` | **this file** — status line DRAFT → RATIFIED, dated, naming the ratifying decision on pins A3/A6/A7 |
**Row 9 exists because the contract governing a commit must land with it.**
The file is currently untracked; without this row it would sit outside both
packets while §A2 says "nothing else" and §E permits staging only
touch-table files — a rule that excluded the rule-book. It joins Packet A.
**Nothing else.** No `epiphany-bundle`, no `epiphany-core`, no spec `.tex`,
no editor-track file, no `spikes/` entry.
## §A3. Test rows
Every row names the mutation that MUST be **observed** to fail. Reasoning
that a mutation "would fail" does not sign a row — the G3a rung already
produced two such unsigned claims, both of which I had to run myself.
**Twenty-two mutations in Packet A, three in Packet B: twenty-five total.**
The six row families below are u1 (5), u2 (3), u2bf (3), u2tomb (3),
u3 (3), u4 (5).
### u1au1e — removal and tombstoning (5 mutations)
One row per family: `StaffGroup`, `PartDefinition`, `AnalysisLayer`,
`View`, `Instrument`. Each mints the object inside a declared transaction
against a non-empty base, undoes the transaction with `StrictInverse`, and
asserts:
- (i) the value is **gone** from the corresponding `Score` vector;
- (ii) `objects` reports `ObjectState::Tombstoned` for its `TypedObjectId`.
> **Killing mutation, five independent runs:** delete that family's arm from
> pin A1's table. Assertion (i) must fail. Deleting one arm must not be
> observed to kill another family's row — run them one at a time and record
> five separate observations.
**These rows do not sign pin A7.** A re-create assertion may be included as
regression coverage, but the arm-deletion mutation says nothing about it;
the ordering pin is signed by u4 below, and nowhere else.
### u2au2c — a live outside referencer blocks the undo (3 mutations)
Graph-aware (`reduce_operation_set_onto`). In each, transaction T mints the
referent; a **separate, non-transactional** op mints the referencer; undo T
under `StrictInverse` MUST be `Conflicted` and the referent MUST remain in
its `Score` vector.
| Row | T mints | Outside op mints |
|---|---|---|
| u2a | `StaffGroup g` | `Staff s` with `group: Some(g)` |
| u2b | `AnalysisLayer l` | `ViewDefinition v` with `active_layers``[l]` |
| u2c | `Instrument i` | `Staff s` with `instrument: i` |
> **Killing mutation, three independent runs:** delete that family's arm
> from pin A2. The effect becomes `Applied` and the reference strands.
### u2bf-au2bf-c — the guards hold base-free (3 mutations) — signs pin A3
The same three shapes reduced through **`reduce_operation_set`**
(`reduce.rs:674`), with no base `Score`. Assert the effect is `Conflicted`.
There is no graph to assert against — `MaterializedState` carries no score —
so these rows assert the effect and the `objects` state only.
Base-free reduction skips every create-side referential precondition
(`create_staff:4137`, `create_view:4420`), so the referencer mints
unconditionally and its value lands in `staff_values` / `view_values`
regardless. That is exactly the situation pin A3 exists to cover: the ledger
plainly knows the reference, and the undo must refuse.
> **Killing mutation, three independent runs:** wrap that family's guard arm
> in `if self.graph.is_some() { … } else { None }`. The u2bf row must go red
> **while its u2 counterpart stays green** — that divergence is the whole
> signature. Record both observations per run.
### u2tomb-au2tomb-c — a tombstoned referencer does not block (3 mutations) — signs pin A4
Graph-aware. Two transactions, undone in reverse order:
1. T1 mints the referent (`g` / `l` / `i`).
2. T2 mints the referencer naming it (`s` / `v` / `s`).
3. Undo **T2** — the referencer is tombstoned in `objects`, leaves its
`Score` vector, and by pin A7 **its value is retained** in
`staff_values` / `view_values`.
4. Undo **T1** — MUST be `Applied` (or `AppliedWithRepair`), and the
referent MUST leave its `Score` vector.
Step 3 is what creates the state pin A4 guards against: a value-map entry
present while its `objects` state is `Tombstoned`.
> **Killing mutation, three independent runs:** delete the
> `matches!(self.objects.get(&…), Some(ObjectState::Live))` conjunct from
> that family's guard. The dead referencer blocks, step 4 becomes
> `Conflicted`, and the row goes red.
### u3au3c — same-transaction teardown is allowed (3 mutations) — signs pin A5
The mirror of u2au2c: put **both** the referent and the referencer inside
T. Undo T MUST be `Applied` (or `AppliedWithRepair`), and **both** values
MUST leave their `Score` vectors.
> **Killing mutation, three independent runs:** drop the
> `!targets.contains(…)` conjunct from that family's guard. The undo becomes
> `Conflicted`.
### u4au4e — objects outranks the retained value map (5 mutations) — signs pin A7
One row per family. Take u1's state (minted in T, T undone, so the object is
`Tombstoned` while its value map still holds the value by pin A7), then
apply a **byte-identical re-carry** of the original `Create…`. It MUST yield
`NoOpReason::TargetTombstoned`**not** `AlreadyApplied`.
Byte-identical is the load-bearing choice: it is the one carried value for
which the retained map would return `identical == true` and produce
`AlreadyApplied`, so it is the only re-carry that can distinguish the two
orderings. A differing re-carry would yield a precondition no-op either way
and sign nothing.
> **Killing mutation, five independent runs:** in that family's reducer,
> make the `Some(ObjectState::Tombstoned { .. })` arm fall through to the
> value-map identity check instead of returning `TargetTombstoned` — i.e.
> reorder so the retained value overrides the tombstone. The row must
> observe `AlreadyApplied` and go red.
### Row-construction note
`create_staff` preconditions instrument liveness at `:4137` and group
liveness at `:4149`; `create_view` preconditions layer liveness at `:4420`.
All are graph-gated, so they bind in the u2 / u2tomb / u3 / u4 rows and are
skipped in u2bf.
- **u2a, u3a, u2tomb-a** need a base carrying a live `Instrument`: the
referencing `Staff` requires one, and the transaction under test mints a
`StaffGroup`, not an instrument.
- **u2c, u3c, u2tomb-c** need **no** base instrument — the instrument the
staff names is the one their own transaction mints.
- **u2b, u3b, u2tomb-b** need no base entity at all; `AnalysisLayer` and
`ViewDefinition` reference nothing outside the pair.
In every graph-aware row the referent must be minted **before** the
referencing op in accepted order. A base-ingested instrument is `Live` in
`objects` through `seed_from_graph` without any mint.
### Anti-traps
A mutation that does not compile signs nothing. A mutation in an op that
runs *before* the state under assertion cannot reach it — the t8b defect
from G3a review. A mutation that leaves the row green signs nothing, and
must be reported as such rather than reasoned around. Confirm each mutation
produces a **red test**, then restore by editing the source back, never by
`git checkout` or `git stash`.
## §A4. Gate (Packet A)
- `cargo test --workspace` — full pass, count reported and compared to the
1429 baseline at `6c5e69f`, with the delta explained by the new rows.
- `cargo clippy --workspace --all-targets` — zero warnings.
- `cargo fmt --check` — clean.
- Whitespace, per §C — **`git diff --cached --check` after staging**, then
the scoped committed-range check after the commit.
- All **twenty-two** Packet A mutations **observed** red and restored, each
reported with its actual failing test name. u2bf additionally reports its
paired u2 row staying green.
---
# PACKET B — P13-S17, the Binary Format chronology
Separate commit. The stack is unpublished, so the true chronology can be
restored rather than patched over.
## §B1. Pins
**Pin B1 — file P13-S17** in `spec/PASS13_CANDIDATES.md`: *Binary Format
revision history omitted genesis tranche G2b; the accept-set raise
`OperationEnvelopeBlock` 2→3 reached the normative tables but never the
history.* Record that G2b's own contract touch row 27 required "version,
Revision History row" and that the rung was signed off without it — the
gate did not catch a documentation MUST because nothing tests the history.
**P13-S17 lands RESOLVED, in the same commit that files it.** Its ledger
disposition is not left open: pins B2B4 restore the chronology and pins
B5B6 add the guard that makes the omission recurrence-detectable, so the
entry is filed and closed by Packet B itself. It is filed rather than merely
fixed because the candidate ledger is the record of *how the gate failed*,
and a silent repair would erase that. Contrast **P13-S15** and **P13-S16**,
which remain open by design because their fixes are sequenced to later
rungs.
**Pin B2 — restore the chronology.** Ladder order is G1 → G2a → G-minor →
G2b → G3a, so:
| Document version | Event | Where |
|---|---|---|
| 0.12.0 | G2a | `binary_format.tex:3599`, unchanged |
| 0.13.0 | G-minor | `:3628`, unchanged |
| **0.14.0** | **G2b — new row** | inserted after the G-minor row |
| **0.15.0** | G3a | `:3643`, renumbered from 0.14.0 |
The title line at `:243` moves to **0.15.0** with a description matching
G3a (it already names G3a; only the number changes).
**Pin B3 — the G2b row must state what G2b actually did.** Read
`spec/CONTRACT_GENESIS_G2B_TUNING.md` and commit `13c3d2f` and write the
row from them, not from memory. It must name at minimum: `OperationKind` /
`OperationKindTag` **34** (`SetTuningContext`), epoch **10**, the payload as
the five-field subset `epiphany_core::TuningContextSettings` rather than the
full graph type, and — the omission that motivates this packet — the
accept-set raise **`OperationEnvelopeBlock` 2→3**, the first accept-set move
since G2a explicitly recorded staying at 2.
**Pin B4 — regenerate `spec/binary_format.pdf`** from the amended source,
using the repository's existing build path.
**Pin B5 — a scoped history guard, in `epiphany-testkit`.** A new test that
reads `spec/binary_format.tex` and **slices to the
`\chapter{Revision History}` section only**. Follow the loading precedent in
`crates/epiphany-testkit/tests/requirement_labels.rs` (`std::fs` from a path
relative to the manifest dir) or `text_projection_grammar.rs`'s
`include_str!` — either, but state which.
**Pin B6 — presence of a rung *name* is not enough, and version numbers are
forbidden.** Two constraints that pull against each other, and the guard
must satisfy both.
*Why bare name-presence fails:* "G2b" already occurs **inside the Revision
History chapter** at `binary_format.tex:3657`, in the G3a row's sentence
"`OperationEnvelopeBlock` stays at 3 where genesis tranche G2b left it." A
guard asserting only that "G2b" appears in the slice stays **green after the
new row is deleted**. It would be born dead.
*Why version literals are also wrong:* encoding "0.14.0" would pin a number
this packet is itself moving, and the next chronology correction would have
to edit the guard — the stale hand-maintained parallel list failure
(`PLAN_GMINOR_SCHEMA_MINOR.md:156`).
*Which rungs have principal markers:* **G2a, G-minor, G2b, G3a — not G1.**
G1 has no standalone row and this packet does not authorize inventing one
(pin B2 lists exactly four rows, one of them new). G1 is recorded
retroactively inside the G2a row at `:3603`, and that row states outright
that G1 landed "with no matching entry here." A guard demanding a G1
principal marker would be **born red** against a document B2 leaves
correct — the guard would be wrong, not the spec.
The guard MUST therefore assert:
1. a **distinct principal marker** for each of **G2a, G-minor, G2b, G3a**,
matching the row form — the rung name immediately preceded by the row's
`---` separator, e.g. `--- Genesis tranche G2b`. Prose mentions cannot
satisfy this: `:3657` says "where genesis tranche G2b left it" and
`:3603` says "genesis tranche G1 --- landed at", where the separator
*follows* the name rather than preceding it.
2. **ordering** — marker offsets strictly increasing within the slice:
`G2a < G-minor < G2b < G3a`.
3. at least one **B3-specific content anchor** for the G2b row —
`SetTuningContext`, discriminant `34`, and the accept-set raise to
`OperationEnvelopeBlock` `3` — searched **only within the G2b row
segment**, i.e. the span from the G2b marker to the next marker (or the
slice end). Unbounded searching would let G3a's row, which names both
`OperationEnvelopeBlock` `3` and G2b, satisfy the anchor after the G2b
row is deleted — reintroducing the exact hole this pin exists to close.
**G1 is deliberately unguarded.** Record that in the test's own comment,
citing `:3603`, so a later reader does not "fix" the omission by adding a
fifth marker assertion and rediscovering this contradiction.
> **Killing mutations, three independent runs:**
> (a) delete the newly added G2b row entirely — the marker assertion goes
> red, *and* it must be confirmed that a name-only guard would have stayed
> green here, which is the finding this pin encodes;
> (b) strip the accept-set-raise clause from the G2b row, leaving its
> marker — the content anchor goes red;
> (c) move the G2b row after the G3a row — the ordering assertion goes red.
>
> All three MUST be **observed**. Packet B therefore carries **three**
> mutations, not one.
## §B2. Touch table (Packet B)
| # | File | Change |
|---|---|---|
| 1 | `spec/PASS13_CANDIDATES.md` | P13-S17 (pin B1) |
| 2 | `spec/binary_format.tex` | new G2b row; G3a 0.14.0 → 0.15.0; title `:243` → 0.15.0 |
| 3 | `spec/binary_format.pdf` | regenerated |
| 4 | `crates/epiphany-testkit/tests/` | the scoped history guard (pins B5B6) |
**Nothing else.** Packet B touches no reducer and no crate but the testkit.
## §B3. Gate (Packet B)
- Full workspace test, clippy, fmt, and the §C whitespace checks as in §A4.
- All **three** pin-B5/B6 mutations **observed** red and restored,
including the confirmation under (a) that a name-only guard would have
stayed green.
- The regenerated PDF's title page reads 0.15.0.
---
## §C. Whitespace checking — the hole this packet closes
`git diff --check -- crates/ spec/` is what the G3a rung used, and it is
**blind to untracked files**: it reported clean while this very contract sat
untracked. It is also blind to what is already staged.
Per packet, in order:
1. Stage the touch-table files explicitly. Never `git add -A`.
2. **`git diff --cached --check`** — catches whitespace in exactly what is
about to be committed, untracked-and-now-staged files included.
3. Commit.
4. **`git diff --check <parent>..HEAD -- crates/ spec/`** — a scoped
committed-range check confirming the landed commit is clean.
Step 4 is deliberately path-scoped. The unpushed range fails
`git diff --check` at `spikes/editor-toolkit/round1-oracle/ORACLE_SUMMARY.md:147`,
an editor-track file outside this packet's authorization. Scoping keeps that
pre-existing failure from masking a real one in `crates/` or `spec/`, and
keeps this packet from being tempted to "fix" a file it must not touch.
---
## §D. Report requirements (both packets)
State, per packet: the test count before and after; every mutation with the
**observed** failing test name; anything found that the contract did not
anticipate. If a pin turns out to be wrong or unsatisfiable, **stop and say
so** rather than working around it — pin A3 and pin A6 are rulings, not
suggestions, and pin A6 in particular forbids an addition that would look
like diligence.
## §E. Boundary — unchanged and absolute
These 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 unstaged root `Cargo.toml` change, and `.claude/worktrees/`.
The narrow editor authorization granted for the G3a packet
(`epiphany-editor-core/src/barriers.rs`, `epiphany-layout-ir/src/barrier.rs`)
was spent by that packet and **does not carry forward**. This packet
authorizes no editor-crate change of any kind.
Stage only the files in the two touch tables, explicitly. Never `git add -A`.