diff --git a/spec/ANALYSIS_GENESIS_PERSISTENCE.md b/spec/ANALYSIS_GENESIS_PERSISTENCE.md new file mode 100644 index 0000000..d5d581e --- /dev/null +++ b/spec/ANALYSIS_GENESIS_PERSISTENCE.md @@ -0,0 +1,238 @@ +# Analysis: canonical graph-state persistence across genesis and pruning + +**Status: analysis, not a ruling.** This is the field-by-field `Score` table +that `spec/PLAN_EDITOR_APP.md` §Ruling B blocker (i) requires *before* the +blocker can be resolved, and that the T1b runway names as step (1). It +decides nothing. It establishes what is true, states the three findings that +follow, and lays out the disposition options with their real costs so the +choice can be made on evidence. + +Written alongside T4-pre W1 (`CONTRACT_EDITOR_T4PRE_IR.md`). Analysis only: +no code, no `.tex`, no `epiphany-core` edit — no collision with the Push-4b +track, which owns those surfaces. + +All citations verified against the working tree at `85d8af6`. + +--- + +## 1. The mechanism, as built + +**Reduction has two modes, and only one of them produces a graph.** +`Reducer::new(op_set)` reduces bookkeeping alone; `new_onto(op_set, base: +&Score)` clones a base `Score` and reduces onto the clone +(`ops/src/reduce.rs:1302-1306`). Graph-aware preconditions are *skipped* +in the base-free mode — the code says so outright: "base-free reduction has +no instrument/group universe to check against" (`reduce.rs:3822`), "base-free +reduction has no staff universe to check" (`reduce.rs:3721`). So a reduction +with no base produces a `MaterializedState` and no `Score` at all. + +**What the canonical document is.** `req:format:canonical-document-reduction` +(`core_spec.tex:11549-11592`): with `canonical_base = None`, the canonical +document is the deterministic reduction of the union of all +`operation_roots` envelopes; with `Some(base)`, it is the base snapshot's +**materialized state** plus the reduction of the envelopes its +`covers_causal_frontier` does not cover. Acceleration snapshots "**MUST be +ignored** for the purposes of canonical state" and must be rebuilt or +discarded on disagreement. + +**What the canonical base actually carries.** `MaterializedState` +(`reduce.rs:503-522`) is: effects, conflicts, anomalies, object +existence (`objects`), spellings, system breaks, page breaks, pending. That +is **reducer bookkeeping**. It contains no `Region`, no `Staff`, no `Event`, +no `Instrument`, no `Part` — no graph values whatsoever. `GraphMaterialization` +pairs it with a `Score`, and says so plainly: the score "is derived state, +never the source of truth" (`reduce.rs:526-534`). + +**What pruning does.** `req:format:pruning-state-preservation` +(`core_spec.tex:11596-11621`): materialize a snapshot covering a chosen DVV +frontier, replace `canonical_base`, **remove operation-envelope blocks +entirely covered by the new frontier**, commit atomically — and "pruning +MUSTNOT alter the canonical document state." The manifest carries +`canonical_base: Option` and a separate, explicitly +non-canonical `acceleration_snapshots: Vec` +(`bundle/src/manifest.rs:384-387`). + +Put together: the values that make a score a score live **only** in the +envelopes and in whatever base was handed to the reducer. Pruning is licensed +to delete those envelopes, and the thing it puts in their place carries no +values. + +--- + +## 2. The table + +Every field of `Score` (`core/src/graph.rs:1710-1735`), with the operations +that write it under graph-aware reduction. "Op-covered" means *some* +operation in `OperationKind` (`ops/src/payload.rs:116-198`) can create or +change it; the writer citations are the actual mutation sites in +`reduce.rs`, distinguished from the base-seed reads at `reduce.rs:1313-1362`, +which are how a base graph's objects enter the reducer's existence index +without a minting operation. + +| # | Field | Op-covered | Writing operations | Verified at | +|---|---|---|---|---| +| 1 | `metadata` | ✅ | `SetMetadata` (LWW) | `reduce.rs:2713`, `:5249` | +| 2 | `canvas.regions` | ✅ | `CreateRegion` / `DeleteRegion`, `ChangeRegionTimeModel`, `SetMetricGrid`, `SetTimeSignature`, `SetUserSystemBreak` / `SetUserPageBreak` | 29 sites incl. `:2656` | +| 3 | `canvas.layout_defaults` | ❌ **none** | — page size and margins | zero hits in `epiphany-ops` | +| 4 | `instruments` | ❌ **none** | — no `CreateInstrument` exists | read-only at `:1318` | +| 5 | `staves` | ✅ | `CreateStaff` (mint), tombstone removal | `:3850`, `:2560` | +| 6 | `staff_groups` | ❌ **none** | — | read-only at `:1329` | +| 7 | `parts` | ❌ **none** | — | read-only at `:1333` | +| 8 | `cross_cutting` | ✅ | `CreateCrossCutting` / `Delete` / `Modify`, `CreateRepeatStructure` / `DeleteRepeatStructure` | 38 sites | +| 9 | `time_signatures` | ✅ | `SetTimeSignature` (set-union mint) | `:3891`, `:2563` | +| 10 | `tuning_context` | ❌ **none** | — pitch space, tuning system, reference | zero hits in `epiphany-ops` | +| 11 | `tempo_map` | ✅ | `SetTempoSegment` | `:4142` | +| 12 | `events` | ✅ | `InsertEvent`, `DeleteEvent`, `ModifyEvent`, `Insert`/`Delete`/`ModifyIdentifiedPitch`, `Transpose`, `TransposeInterval` | 28 sites | +| 13 | `spelling_attachments` | ✅ | `RespellPitch` | 15 sites | +| 14 | `decomposition_attachments` | ⚠️ **removal only** | reduction only *retains*; the creator is the prepass | `:2342`; `core/src/prepass.rs:382` | +| 15 | `spelling_precedence` | ❌ **none** | — | zero hits in `epiphany-ops` | +| 16 | `analysis_layers` | ❌ **none** | — | read-only at `:1345` | +| 17 | `views` | ❌ **none** | — | read-only at `:1349` | +| 18 | `identity` | ❌ **none** | — the `IdentityContext` itself | zero hits | +| 19 | `tombstoned_pitches` | ✅ derived | delete/tombstone paths | `:2526` | +| 20 | `tombstoned_events` | ✅ derived | delete/tombstone paths | 3 sites | + +**Eight fields have no operation that can produce them** (3, 4, 6, 7, 10, 15, +16, 17, plus `identity` at 18), and one more (14) can only be pruned back, +never authored. Of the covered ones, several are covered *only in the +graph-aware mode* — they write through `if let Some(score) = self.graph`. + +--- + +## 3. The three findings + +### Finding 1 — the genesis root is unreachable from an empty base + +The creation chain is instrument → staff → staff instance → voice → event, +and each link is enforced under graph-aware reduction: + +* `CreateStaff` refuses unless `TypedObjectId::Instrument(op.staff.instrument)` + is live (`reduce.rs:3824-3833`) — and **no operation creates an + `Instrument`**. Genesis is outside the operation set by ratified decision + ("there is no `CreateCanvas`/`CreateInstrument`", + `binary_format.tex:2420`; Pass-12 K8). +* `CreateStaffInstance` refuses unless the referenced global `Staff` is live + (`reduce.rs:3723-3734`). + +So reduction onto `Score::empty(identity)` can never reach a note. The escape +hatch that exists today is not a design — it is the base-free mode, which +skips the preconditions precisely *because* it has no graph to check, and +produces no graph either. + +### Finding 2 — the canonical base cannot carry what the base-only fields hold + +The eight uncovered fields can enter a `Score` only by being in the base +handed to the reducer. Pruning replaces that base with a `MaterializedState` +snapshot, which carries none of them, and deletes the covered envelopes — +which never carried them either. **After a prune, the uncovered fields are +gone with no canonical way to recover them**, and the acceleration snapshot +that does hold full-`Score` bytes is normatively forbidden as a source of +canonical state. + +This is not only an editor concern. Field 3 is the printed page geometry; +field 10 is the tuning context the entire Push-4b resolver consumes; field 7 +is the part definitions that `req:graph:part-content-projection` calls +normative. + +### Finding 3 — there is no canonical wire path from bundle bytes to a `Score` + +`canonical_base` holds `MaterializedState`; a base stamped above schema major +0 forces read-only (`bundle.rs:857`); full-`Score` bytes exist on the wire +only in the acceleration-snapshot role, which MUST be ignored. Consequently +the editor cannot open a document today — and Fact 3 of the plan records the +symptom: the GUI opens a hard-coded testkit fixture, and neither editor crate +even depends on `epiphany-bundle`. + +The three findings are one problem seen from three sides: **the canonical +document format has no representation for graph state that no operation +authors.** + +--- + +## 4. Dispositions + +Four coherent options. They are not mutually exclusive — A and D compose, and +C subsumes much of A. + +**A. A canonical genesis block.** A new chunk role, canonical and never +pruned, carrying exactly the uncovered fields: canvas layout defaults, +instruments, staff groups, parts, tuning context, spelling precedence, +analysis layers, views, identity. Reduction takes it as the base. This keeps +the ratified "genesis outside the operation set" decision intact — genesis +becomes *persisted canonical input* rather than operations — and it is the +smallest change that makes documents openable. Cost: one format addition and +a manifest field; the hard question it must answer is what happens when two +replicas' genesis blocks disagree, since a non-op payload has no CRDT merge +rule. Concurrent editing of genesis fields would remain unsupported, which +is honest for v1 but is a real product ceiling (adding an instrument to a +shared score is a normal edit). + +**B. Close the op-coverage gap.** Add `CreateInstrument`, `CreateStaffGroup`, +`CreatePart`, `SetCanvasLayoutDefaults`, `SetTuningContext`, +`SetSpellingPrecedence`, and analysis/view CRUD, making the op log +self-sufficient. This is the only option under which genesis state converges +concurrently like everything else, and it makes the grow-only log the whole +truth. Cost: it reverses a ratified Pass-12 decision, and each operation +needs reduction semantics, conflict behaviour, undo behaviour, and catalog +text — a schema major spent deliberately, in the Push-4b mold, not a +tranche this track can absorb. Note it does **not** by itself fix pruning: +the envelopes carrying these ops become prunable like any other. + +**C. Promote the canonical base to carry graph values.** Make the +canonical-base snapshot *checkpointed reducer state plus graph state* rather +than bookkeeping alone. This is the same shape T4b needs for incremental +materialization ("checkpointed-reducer-state-plus-tail"), so the two tracks +would pay for one mechanism. It also repairs pruning for **every** field at +once, covered or not. Cost: the largest format change of the four; the base +role's schema-major-0 pin and the read-only-above-0 rule +(`bundle.rs:857`) must be revisited; and the `SnapshotId` derivation is +today an acknowledged test-harness stand-in with no normative derivation +(`binary_format.tex:698`), which would have to become real before snapshots +are load-bearing. + +**D. Scope-limit T1b.** Ship the document layer for documents whose genesis +is empty, metadata-only, or region-only, and refuse the rest cleanly — +the plan's existing escape hatch. Cost: an editor that cannot open a score +with an instrument in it is not the editor; this is a way to land the +bundle/session/lease machinery (Ruling D's unforgeable lease, single-writer +enforcement, the save protocol) against a real format while the genesis +decision is made properly. It buys sequencing, not a solution. + +**The recommendation I would defend:** **D now, C as the destination, A only +if C proves too large to sequence.** D unblocks the Ruling-D ownership API +and the save/dirty protocol — the parts of T1b that the editor track actually +needs next and that are independent of this question. C is where the format +wants to end up, because it is the only option that makes pruning safe for +every field rather than for an enumerated list, and because T4b needs the +same checkpoint mechanism regardless. B is the right long-run answer for +*concurrent* genesis editing, but it is a Push-4b-class spec tranche and +should be sequenced by product priority, not by this blocker. + +Whatever is chosen inherits one hard constraint: pruning MUSTNOT alter the +canonical document state (`core_spec.tex:11612-11616`). Any option that +enumerates fields must be re-audited against this table every time a field is +added to `Score` — which argues, again, for C. + +--- + +## 5. Open questions for the ruling + +1. **Genesis divergence.** Under A, what is the merge rule when two replicas + present different genesis blocks for the same document id? (Under B this + question dissolves; under C it becomes snapshot-disagreement handling, + which the spec already answers for acceleration snapshots.) +2. **Is `identity` document state or session state?** It is on `Score` today + with no op coverage. If a document has one `IdentityContext` and each + session mints under its own replica id, the field's role needs stating. +3. **`decomposition_attachments` and `spelling_precedence`** are consumed by + the prepass. Are they derived state that should be rebuilt rather than + persisted — in which case they leave this table — or authored state? + Field 14's removal-only reduction path suggests the former. +4. **Does the canvas's page geometry belong to the document or to a view?** + §3.2's document ≠ session ≠ view split may relocate field 3 entirely, + and part-specific page geometry is a real product requirement. + +--- + +*Related: `spec/PLAN_EDITOR_APP.md` §Ruling B, §3.1, §3.2, §3.7; +`spec/CONTRACT_EDITOR_T4PRE_IR.md`.* diff --git a/spec/CONTRACT_EDITOR_T4PRE_IR.md b/spec/CONTRACT_EDITOR_T4PRE_IR.md new file mode 100644 index 0000000..416e77c --- /dev/null +++ b/spec/CONTRACT_EDITOR_T4PRE_IR.md @@ -0,0 +1,245 @@ +# Contract: Editor T4-pre — the layout-IR readiness tranche + +Repo root `/home/jeans/Repos/active/epiphany`. Governed by +`spec/PLAN_EDITOR_APP.md` §3.7 ("the layout-IR readiness tranche (T4 +prerequisite)") and named as a prerequisite by Ruling A: *"IR per-system +primitive ownership; the shared typed glyph-asset seam; the text-run +primitive decision. These are IR/render tranches this ruling depends on, not +work it smuggles in."* T3 is complete at `85d8af6`. + +Execution model as T1a/T2/T3: Sonnet subagents per packet, coordinator +line-level review with independent mutation re-runs, user deep-dives at +contract sign-off, any new golden baseline, and the final report. Mutation +discipline throughout: anchor-assert before substituting, restore by +reversing, never `git checkout`. + +**Parallel safety.** The Push-4b track owns `crates/epiphany-core/**`, its +`DECISIONS.md`, and all `.tex` (`CONTRACT_PUSH4B_RESOLVER.md` blast radius); +it is live in the working tree. This tranche touches **no `epiphany-core` +file, no `.tex`, and adds no requirement label** — counts stay at whatever +the parallel track has them at; report observed actuals, never assert stale +numbers. + +**The literal census, both structs.** W1 adds a field to `ResolvedSystem` as +well as to `ResolvedLayoutIR`, and Rust struct literals are exhaustive, so +every literal of both must be updated or the workspace does not compile. +Verified: + +* `ResolvedLayoutIR {` — `layout-ir/src/{resolved.rs:405, solver.rs:518}`, + `engrave/src/lib.rs:407`, `render-svg/src/svg.rs:1049`, + `testkit/src/layout_stub.rs:436`. +* `ResolvedSystem {` — `layout-ir/src/solver.rs:501`, + `engrave/src/casting.rs:1950`, `testkit/src/layout_stub.rs:461`, **and + `editor-core/src/lib.rs:4718`** — the two-system hit-test fixture in + editor-core's test module, a full four-field literal with no `..` spread, + and `ResolvedSystem` cannot implement `Default` (it carries a + `Provenance`). + +So this tranche **does** reach one editor-crate file: that test literal, a +**mechanical field-add only**. `epiphany-editor-core` otherwise borrows +`&ResolvedLayoutIR` and mutates `.pages`; it **must not consume ownership** — +adoption stays at T4 per pin 8. Parallel safety is unaffected either way: +Push-4b owns `epiphany-core`, not the editor crate. + +--- + +## W1 — per-system primitive ownership (dispatchable now) + +### The verified starting point + +Read these before designing; they change the shape of the work: + +* **The partition already exists and is discarded.** Casting-off computes + `system_of_slot`, `stroke_system`, `curve_system`, and `region_of_system` + and returns them on `CastLayout` (`engrave/src/casting.rs:186-206`); + `engrave/src/lib.rs:386-391` folds `cast.glyphs/strokes/curves/pages` into + `ResolvedLayoutIR` and **drops every ownership vector on the floor**. W1 is + "stop discarding it", not "infer it". +* **Glyph ownership is derivable exactly, and one consumer already derives + it**: the quality census maps a glyph to a system through + `system_of_slot[constrained_glyph.horizontal_slot]` + (`engrave/src/quality.rs:232`), skipping slots no region claimed. +* **Cross-system primitives are already resolved at this stage.** A + system-spanning stroke is replaced by its first segment plus synthesized + continuation segments, and a system-spanning curve is split by de Casteljau + subdivision, the continuations carrying `SYSTEM_CONTINUATION_SYNTHESIS` + provenance (strokes `casting.rs:1051-1075`, curves + `casting.rs:1128-1165`). So **every resolved primitive belongs + to at most one system already**. Fact 2's warning that "cross-system curves + and boundary-straddling primitives make inference ambiguous" is an argument + against *spatial inference by the consumer*, not against publishing what + casting-off knows. +* **`SystemId(pub u128)` (`layout-ir/src/cache.rs:10`) has no production + constructor** — the single construction in the tree is testkit's random + cache generator (`layout_stub.rs:630`, property-test fodder for the cache + codec). Nothing maps a real system into + `LayoutCache.resolved: BTreeMap`; it is inert scaffolding for + T4b. W1 does **not** wire it; see the identity pin. + +### Design pins + +1. **Ownership lives on the system, not in a parallel table.** + `ResolvedSystem` gains one field holding index lists into the layout's + flat `glyphs`/`strokes`/`curves` arrays (`u32` indices — no layout nears + 4 G primitives, and it matches the crate's existing count width), and + `ResolvedLayoutIR` gains one **unowned** bucket of the same shape. + Structural ownership + removes the "table order must match page order" invariant a top-level + parallel table would create, and each system already carries its own + identity in `provenance`. +2. **No primitive is split, merged, reordered, or renumbered.** The flat + arrays are exactly what they are today, in exactly today's order. W1 + publishes indices into them and nothing else. +3. **Unowned is a first-class bucket, never coerced.** `None` — "claimed by + no region" — goes to the unowned bucket. It has a real producer: the stub + solver (`layout-ir/src/solver.rs:490-520`) builds a degenerate page tree + of default-rect systems and resolves no per-system geometry, so **it + publishes every primitive as unowned**. Fabricating an attribution there + would be a lie about what that path computed. +4. **The partition is total and disjoint.** For each of the three arrays, the + union of every system's indices plus the unowned bucket is exactly + `0..len`, each index appearing exactly once. This is the load-bearing + invariant; it is a test, not a comment. +5. **Identity is available, position is primary.** A consumer keys a system + by its `provenance.stable_id` (`LayoutObjectId`); systems remain addressed + positionally in page order. **Verified caution to record in DECISIONS.md, + because it will bite T4b:** a region's *first* system reuses the region's + own provenance verbatim (`casting.rs:1871`), and page 1 reuses the first + region's provenance too (`casting.rs:1192`) — so a system's `stable_id` + can equal a page's and a region's. On the **stub solver path the aliasing + is total, not just first-system**: every stub system reuses its region's + provenance (`solver.rs:501-502`), so the rule below has a producer on both + solver paths. Uniqueness *among systems* is structural, not incidental — + synthesized ids are domain-tagged hashes over (source, kind discriminant, + namespaced instance key), and `KEY_NS_SYSTEM` and `KEY_NS_PAGE` occupy + distinct namespaces — which is all this packet needs; **any future + cross-kind map keyed on raw `LayoutObjectId`/`SystemId` u128s must + disambiguate by kind.** +6. **Byte-neutral: ownership is NOT canonically encoded.** Precedent is in + the same module: `vertical_band` is carried through but excluded, because + "band ownership tells a vertical solver which staff owns a primitive; it + draws nothing, so two layouts differing only in it are the same rendered + layout and hash alike" (`resolved.rs:110-118`). System ownership draws + nothing either. `encode_canonical` is therefore **unchanged**, and the + module's canonical-serialization note gains ownership to its stated + exclusions. If a future normative requirement wants ownership pinned on + the wire, that is a spec-side schema-major decision — not this packet's + budget to spend. Two consequences the **DECISIONS.md entry must state + outright**, because they are what stop a later reader from "fixing" this: + encoding ownership would make the fingerprint *more fragile than the + rendering it fingerprints* — a casting-off refactor that re-partitions + without moving a pixel would become a byte-level break and force a schema + major for something no renderer and no conformance claim can observe; and + because two conformant implementations may legitimately partition + differently, **any future cross-implementation test of incremental + relayout compares final bytes, never intermediate partitions.** +7. **One derivation, not two.** Casting-off publishes `glyph_system` on + `CastLayout` (computed once, from `system_of_slot`) and the quality census + **consumes it** instead of re-deriving. Two copies of an attribution rule + drift; this is the packet that makes that impossible. +8. **One accessor.** `ResolvedLayoutIR` gains a `systems()` walk in page + order (pages → systems), which the partition tests use. Editor-core + hand-rolls this same flatten today (`editor-core/src/lib.rs:989`); it + adopts the accessor at T4, not here. + +### Tests + minimum mutations + +Value-asserting throughout; the multi-system fixtures already exist +(`ten_measure_single_staff` casts to 2 systems at the demo's scale; +`ten_measure_with_slurs` is the casting fixture behind golden G4). + +* **(m1) totality/disjointness** — the partition test of pin 4 on a real + multi-system engrave. *Mutation:* drop the last glyph index from a system's + list → dies. +* **(m2) no coercion** — the stub-solver path publishes everything unowned. + *Mutation:* map `None` to system 0 → dies. +* **(m3) attribution correctness** — assert the *actual* per-system glyph / + stroke / curve counts of a two-system fixture (real numbers, reported in + the packet report). *Mutation:* off-by-one on the system index → dies. +* **(m4) the census really consumes the published vector** — *mutation:* + publish `Some(0)` for every glyph while leaving `system_of_slot` correct → + the existing quality-metric assertions die. If they survive, the census is + still deriving its own answer and pin 7 is not done. +* **(m5) continuation segments follow the later system** — a slur or stroke + crossing a system break: its continuation is owned by the system it was + split *into*. *Mutation:* attribute continuations to the source segment's + system → dies. +* **(m6) byte-neutrality is locked** — build a layout, clone it, perturb + **only** the ownership lists, assert `canonical_bytes()` equal while + `PartialEq` differs. *Mutation:* encode the ownership lists in + `encode_canonical` → dies. + +### Blast radius + +`crates/epiphany-layout-ir/src/{resolved.rs, solver.rs, lib.rs}`, +`crates/epiphany-engrave/src/{casting.rs, lib.rs, quality.rs}`, the +test-only literals at `render-svg/src/svg.rs:1049`, +`testkit/src/layout_stub.rs:{436, 461}` and +`editor-core/src/lib.rs:4718` (mechanical field-add only — editor-core +consumes no ownership in this packet), and the `DECISIONS.md` of `layout-ir` +and `engrave`. Nothing else. No new crate, no new dependency, no public API +removed or renamed. + +--- + +## W2 — the shared typed glyph-asset seam (contracted after W1 review) + +Named now so the tranche's shape is visible; **not dispatched with W1**. + +The Bravura table is private to `render-svg` (`outline()` is `pub(crate)`, +`outline.rs:7`) and stores SVG path `d` strings generated by +`tools/extract_bravura_outlines.py` (`outlines_generated.rs:1-20`). A canvas +tessellator needs typed vector paths from a crate both the renderer and the +app can depend on. + +**The pin that makes it safe**, to be detailed when W1 lands: the generator +emits a **typed path representation alongside the existing `d` string**, and +`render-svg` keeps emitting the `d` string byte-for-byte — so every SVG +golden and every conformance byte stays identical while the canvas gets +typed geometry. The open decision W2's contract resolves is the home (a new +`epiphany-glyphs` crate vs. a `layout-ir` module), judged on dependency +weight and the MSRV/CI job structure, plus whether the typed form is +generated or parsed once at load. + +## W3 — the text-run primitive decision (analysis packet, after W2) + +Ruling A's criterion 3 makes the text pipeline a hard spike criterion: +shaping, fallback, bidi, and metrics consistent across canvas, SVG/PDF +export, hit testing, and the accessibility tree. W3 is a **decision +document**, not an implementation — what a text-run primitive is in the +resolved IR, and which of shaping/metrics the IR owns versus the renderer — +because getting this wrong forecloses the toolkit choice T4's spike is +supposed to make freely. + +## Coordinator deliverable, alongside W1 + +`spec/ANALYSIS_GENESIS_PERSISTENCE.md` — the **field-by-field `Score` table** +Ruling B blocker (i) demands before it can be resolved, per the plan's T1b +runway. Analysis only: no code, no `.tex`, no core edits, therefore no +collision with the parallel track. Fable writes it while W1 runs; the user +deep-dives it as a coordination point. + +--- + +## Gate (every packet, actual output) + +The standard six; conformance **9/9 with `--features golden-gate`, 8/8 +without** (both run); requirement labels reported as observed. Plus two locks +specific to this tranche, run as before/after comparisons and reported with +their actual digests: + +* **Layout canonical bytes are byte-identical.** Capture + `ResolvedLayoutIR::canonical_bytes()` for the reference-suite fixtures at + the base commit, then after the change; they must be equal. +* **All five GUI goldens are byte-identical** (`ten_measure_open.png` 53638, + `ten_measure_insert.png` 54590, `ten_measure_slurs_castoff.png` 57891, + `ten_measure_caret_entry.png` 57379, plus G3's reuse of G1). No golden is + re-blessed in this tranche — nothing here may change a pixel. + +Blast radius per packet as stated. Do not commit. + +## Report + +Per packet, as every tranche: files + summary, exact asserted values +(including the real per-system primitive counts), every mutation with kill +evidence, gate output, deviations flagged explicitly.