diff --git a/spec/CONTRACT_EDITOR_T1A_GOLDENS.md b/spec/CONTRACT_EDITOR_T1A_GOLDENS.md new file mode 100644 index 0000000..bfb14f4 --- /dev/null +++ b/spec/CONTRACT_EDITOR_T1A_GOLDENS.md @@ -0,0 +1,262 @@ +# Contract: Editor T1a — the visual golden harness + +Repo root `/home/jeans/Repos/active/epiphany`. The plan is +`spec/PLAN_EDITOR_APP.md`; read its §Ruling C (granted, as amended), §Ruling A +(granted as amended 2026-07-23), and §5 in full before editing anything. +Ruling C independently authorizes this tranche. Ruling B remains blocked and +Ruling D remains conditional — **nothing in this contract touches a document, +a bundle, or `editor-core`**. + +This tranche closes the repo's last unverified surface: the GUI's rendered +output has never been seen by any gate (plan Fact 1). It adds **pixel-level +golden baselines of the resvg-rasterized score** — the exact surface the demo +GUI displays — as ordinary `#[test]`s in `epiphany-editor-gui`, compared as +**decoded pixels, never encoded files**, with reviewable failure artifacts in +CI. The resolver tranche (`CONTRACT_PUSH4B_RESOLVER.md`) is in flight; plan +§5's parallel-safety rules are binding on every commit. + +## Execution model — who does what + +Implementation is dispatched to **subagents (Sonnet-tier), one per work +packet** (§Work packets), each with a file-scoped brief quoting this +contract's relevant section verbatim. The **coordinator (Fable)** reviews +every packet at line level before anything lands: re-runs the gate itself, +verifies each mutation's evidence (a worker's own report is never sufficient), +checks that every cited file:line says what the code claims, and rejects any +packet that drifts outside its blast radius. The **user's deep-dive points**, +named in advance: + +1. **This contract's sign-off** (precedes any dispatch). +2. **Baseline review.** The four PNGs are the first visual record of this + editor's output in the project's history, and a golden locks whatever it + sees — **bugs included**. Before the blessing commit, the user visually + inspects each baseline: clef and staff lines present, ledger lines correct, + noteheads on the intended positions, slur curves plausible, system breaks + where casting-off put them. An unreviewed baseline is an unverified claim + wearing a checkmark. +3. **The final report** (§Report). + +## Blast radius + +* `crates/epiphany-editor-gui/src/main.rs` — minimal: a `#[cfg(test)] mod + goldens;` declaration and, if needed, `pub(crate)` visibility on the + existing `rasterize` helper (`main.rs:160`). No behavior change to the + running binary. +* `crates/epiphany-editor-gui/src/goldens.rs` — NEW, `#[cfg(test)]`-only: the + comparator, bless machinery, and the golden tests. +* `crates/epiphany-editor-gui/goldens/*.png` — NEW baseline images. +* `crates/epiphany-editor-gui/DECISIONS.md` — NEW (this crate's first): + records this tranche's calls (§What to build, item 4). +* `crates/epiphany-editor-gui/Cargo.toml` + `Cargo.lock` — ONLY if the `png` + dev-dependency fallback proves necessary (§prohibitions). +* `.github/workflows/ci.yml` — exactly **one additive step** in the existing + `editor-gui` job (`ci.yml:109`): upload `target/golden-failures/**` with + `if: failure()`. Nothing else in the file moves. + +You touch no other crate and no other file. In particular +`crates/epiphany-core/**` is being worked in parallel by the resolver tranche +— do not go near it — and `epiphany-testkit` is frozen for this tranche +(gate promotion is T2, deliberately). + +## Central prohibitions + +* **Counts do not move.** Requirement labels stay **212 / 282 / 282** + (`requirement_labels.rs:12-14`); the conformance suite stays **8/8** + (`conformance_suite.rs:202`). If either moves, stop and report. +* **No existing golden or fuzz digest is re-blessed.** This tranche *adds* + new PNG baselines; it modifies nothing that exists. The resolver's + zero-churn tripwire must stay unambiguous. +* **Comparison is decoded pixels only** — dimensions + raw RGBA. Comparing + encoded PNG bytes is the exact defect Ruling C's amendment exists to + prevent (it locks encoder behavior and churns while pixels are identical). +* **No new runtime dependencies.** PNG encode/decode goes through the + resvg-bundled tiny-skia (`png` is already in `Cargo.lock` via that tree). + If the feature is not exposed through resvg's re-export, the fallback is a + `png` **dev-dependency** (the version already in the lock) — never `image`, + never a runtime dep. +* **Never bless to make a test pass.** A diff is a finding: it is reviewed + (user deep-dive point 2 for new baselines; ordinary review for changes) + before any re-bless. The bless mechanism exists for *deliberate* baseline + updates only. +* Land only green (`cargo test --workspace` clean); whoever lands second + rebases. + +## What to build + +### 1. The comparator and bless machinery (`src/goldens.rs`) + +* **Rasterize** through the binary's existing `rasterize` fn (`main.rs:160`) + — the same code path the GUI displays, `GlyphMode::PathOutline` (the + default; no fonts), `RenderOptions { px_per_staff_space: 12.0, .. }` (the + demo's default). +* **Compare**: decode the committed baseline PNG; assert dimensions equal, + then raw RGBA bytes equal. On mismatch, write `actual.png`, + `expected.png`, and `diff.png` (per-pixel highlight) to + `target/golden-failures//` and name all three paths in the + panic message. +* **Bless**: `EPIPHANY_BLESS_GOLDENS=1` writes/overwrites the baseline + instead of comparing. Policy stated in a comment at the definition: a + bless is a reviewed decision, never a fix. +* **The comparator's own unit tests** (value-asserting): + * identical pixmaps pass; + * a single altered pixel fails AND produces all three artifacts at the + named paths; + * **re-encoding the baseline PNG with different encoder settings still + passes** — the decoded-pixel contract made executable; + * mismatched dimensions fail before any pixel comparison. + +### 2. The four golden states + +Each test builds its session headlessly — `EditorSession::open(fixture, +Box::new(Engraver::default()))`, exactly the demo's open path (`main.rs:197`) +— asserts the *session-level values* first, then locks the pixels. Each +state renders **twice**, asserting the two SVG strings and the two pixmaps +are identical (the determinism double) before comparing to the baseline. + +* **G1 — as opened**: `ten_measure_single_staff(0)`, rasterized, compared to + `goldens/ten_measure_open.png`. +* **G2 — after a scripted pencil insert**: the target is derived through the + session's own inverses, never a magic constant — pick the world point via + `staff_pitch_at` / `position_at` / `default_grid_at`, **assert the resolved + pitch and snapped beat values** (e.g., the intended nominal/octave and + position as exact rationals), then `insert_note_at`, re-render, compare to + `goldens/ten_measure_insert.png`. +* **G3 — after undo of that insert**: **no third baseline.** Assert the + post-undo raster equals **G1's baseline** byte-for-byte in decoded RGBA — + undo returns the pixels, not just the model. This is the strongest test in + the tranche; it must reuse G1's file, not a copy. +* **G4 — casting-off**: `ten_measure_with_slurs(0)` + (`fixtures.rs:777`) — multi-system layout **and** the + cross-system slur-split path (`casting.rs:2262`), the layout path real + documents take. Compared to `goldens/ten_measure_slurs_castoff.png`. The + test additionally asserts the resolved layout actually produced **more + than one system** (value assertion — if casting-off stops triggering, this + must fail as a value error, not as a mysterious pixel diff). + +### 3. The CI step + +One additive step in the `editor-gui` job, after the test step: + +```yaml +- name: Upload golden failure artifacts + if: failure() + uses: actions/upload-artifact@v4 + with: + name: golden-failures + path: target/golden-failures/ + if-no-files-found: ignore +``` + +Nothing else in `ci.yml` changes. (Match the `actions/upload-artifact` +major version already used elsewhere in the file, if any; otherwise v4.) + +### 4. `DECISIONS.md` (the crate's first) + +Record: the decoded-pixel comparison contract and why (Ruling C amendment); +the bless mechanism and its policy; the G3-reuses-G1 design; the casting-off +fixture choice (`ten_measure_with_slurs` for the slur-split coverage); the +determinism basis (PathOutline = no fonts; pure-Rust resvg/tiny-skia; Linux +CI and dev) — and the standing consequence: **baselines pin the raster +stack**, so any `Cargo.lock` movement of `resvg`/`tiny-skia`/`png` is a +golden-review event, never a silent re-bless. + +## Work packets (subagent dispatch) + +Sequential where stated; each packet's brief includes this contract's +matching section verbatim, the blast-radius list, and the prohibitions. + +* **W1 — comparator + bless + its unit tests** (`goldens.rs` scaffolding; + no baselines yet). Sonnet. Exit: comparator tests green, mutations + verified (§Verification), no other file touched. +* **W2 — the four golden states + determinism doubles** (depends on W1). + Sonnet. Exit: tests written and value-asserting; baselines generated via + the bless mechanism but **committed only after user review** (deep-dive + point 2); mutations verified. +* **W3 — CI step + DECISIONS.md** (after W2's baselines are blessed). + Sonnet. Exit: the single-step `ci.yml` diff; DECISIONS.md complete. + +The coordinator reviews each packet's diff line-by-line, independently +re-runs its tests and mutations, and only then stages it. Baselines go to +the user between W2 and W3. + +## Verification + +**Mutation-verify every new test.** Assert the anchor text is present before +substituting (a no-match substitution is a no-op mutation indistinguishable +from a passing test); restore by reversing the exact substitution — **never +`git checkout`**. Report each mutation and the test that died. The named +mutations, minimum set: + +1. Comparator → compare encoded PNG bytes instead of decoded RGBA: the + re-encoding unit test must die. +2. Comparator → skip the dimension check: the dimension-mismatch test must + die. +3. Comparator → suppress artifact writing on mismatch: the + artifacts-produced test must die. +4. G2 → skip the `insert_note_at` call (render the unedited session): G2 + must die on pixels AND its pitch/beat value assertions must be shown to + still pass (proving the pixel lock catches what value assertions cannot). +5. G3 → skip the `undo()` call: G3 must die. +6. G4 → substitute `ten_measure_single_staff(0)` for the slurs fixture: G4 + must die (on the system-count value assertion or pixels — report which). +7. Determinism double → perturb the second render (a different + `px_per_staff_space`, or append a byte to its SVG string before + rasterizing): the double's equality assertions must die. (An earlier + draft said "make the second render reuse the first's output" — that + mutation passes vacuously and proves nothing; corrected at W2 dispatch.) + +Then the full gate, reporting actual commands and actual output: + +1. `cargo fmt --all --check` +2. `cargo clippy --workspace --all-targets` → 0 warnings +3. `cargo test --workspace` → 0 failed +4. `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps` → 0 +5. `cargo run -q -p epiphany-testkit --example conformance_suite` → 8/8 +6. `cargo test -p epiphany-testkit --test requirement_labels` → counts + unchanged at 212 / 282 / 282. + +**Expected churn: three NEW PNG files and nothing else** (four golden states, +but G3 reuses G1's baseline by design). If any existing +golden, digest, count, or gate number moves, stop and report rather than +re-blessing. + +CI pins clippy at 1.95.0 and the MSRV at 1.85; `epiphany-editor-gui` is +excluded from the MSRV jobs and checked on pinned stable (`ci.yml:99,109`) — +this tranche keeps that structure untouched. + +## Citations + +This is product-side work; it adds no requirement labels and should cite no +`req:*` string. If a doc comment nonetheless cites one, read the requirement +first and confirm it says what the sentence claims (the P13-S9 rule). Plan +citations (`PLAN_EDITOR_APP.md` §Ruling C, §5, Fact 1, Fact 7) are the +governing references. + +## Do not + +* Touch `epiphany-core`, `epiphany-testkit`, any `.tex`, any other crate, or + any `DECISIONS.md` but the new `epiphany-editor-gui/DECISIONS.md`. +* Promote a conformance gate, move 8/8 or 212/282/282, or re-bless anything + existing. +* Compare encoded PNG bytes; add `image` or any runtime dependency. +* Commit a baseline the user has not visually reviewed. +* Change the demo binary's behavior, its egui pin, or its rendering path — + this tranche observes; it does not refactor. +* Restore a mutation with `git checkout`. Reverse the substitution. + +## Report + +State: the comparator contract and its unit tests; each golden state's value +assertions and file; the G3=G1 equality design; every mutation and the test +that died (including mutation 4's two-part evidence); the baseline images' +human-review record; the exact `ci.yml` diff; whether the `png` dev-dep +fallback was needed; actual gate output; confirmation that no existing +golden, digest, or count moved; and anything you chose not to do and why. + +## Next: T2 promotion, and what this tranche feeds + +Promotion of this harness to a numbered `[9/9]` conformance gate is **T2**, +after the resolver lands (the 8/8 count is frozen until then). The baselines +also become the Ruling-A cross-check for the T4 canvas — as geometry/scene +equivalence plus a bounded visual differential, NOT pixel equality (plan +§Ruling A). T1b remains blocked per the plan; nothing here may anticipate it. diff --git a/spec/PLAN_EDITOR_APP.md b/spec/PLAN_EDITOR_APP.md new file mode 100644 index 0000000..3c841c6 --- /dev/null +++ b/spec/PLAN_EDITOR_APP.md @@ -0,0 +1,889 @@ +# Editor track — from seam probe to product editor: scope and plan + +Status: **T1 is split.** **T1a (visual golden harness) dispatches now, +resolver-parallel; T1b (document layer) sequences after the resolver +tranche**, behind two named blockers (§Ruling B). **Ruling C granted** +(casting-off sub-decision, decoded-pixel amendment, CI-artifact clarification +— 2026-07-23). **Ruling D granted conditionally** (condition: the T1b +contract specifies the document/session ownership API — over an unforgeable +document-bound session type or lease token — before code). **Ruling A +granted as amended (explicit, 2026-07-23). Ruling B redrafted twice and +re-scoped — not grantable until the graph-state-persistence and +versioned-decode blockers resolve.** T1a is dispatched under +`CONTRACT_EDITOR_T1A_GOLDENS.md`. +Prepared against `main` @ `2cf2dae`; revised three times on 2026-07-23 +against `b0acacb` after three source-level reviews (14 + 11 + 9 findings; +every one dispositioned in §6). Every claim below was checked against the source; where +a probe was run the file and line are given. The companion executable +documents are `spec/CONTRACT_EDITOR_T_.md`, one per tranche, in the +house style of `CONTRACT_PUSH4B_RESOLVER.md`. + +This plan has two jobs. The near job is dispatching T1a safely in parallel +with the Push-4b resolver tranche, and sequencing T1b honestly behind its +blockers. The far job is §3, the **architecture-risk map**: the selected +risks that shape the early rulings, each with its **foreclosure guard** — +what the early tranches must not do, so that no later track finds its door +welded shut by an early convenience. §3 is deliberately *not* a complete +product roadmap (see its closing note). + +--- + +## 1. The facts that shape this track + +**The editor's model is already built; only its shell is a demo.** The editing +architecture is layered exactly right for the ambition, and the layers have +sharply different maturity: + +| layer | where | state | +|---|---|---| +| headless session (selection, minting, atomic apply, CRDT-correct undo) | `epiphany-editor-core` (6.7k lines, 93 tests, conformance gate `[7c]`) | **solid** | +| render→hit-test provenance contract | `epiphany-layout-ir/src/hittest.rs` | **solid** | +| SVG renderer (Bravura outlines, golden-locked) | `epiphany-render-svg` | **solid** | +| GUI shell | `epiphany-editor-gui` — 598 lines, one `main.rs`, self-described "demo binary" (`main.rs:21`), version 0.0.0 | **disposable by design** | + +MuseScore's engraving engine and UI are entangled; Epiphany's are separated by +a tested seam. That separation is the asset this track must not squander: +**every editor capability lands headless in `editor-core` first, proven at the +IR boundary, before any GUI wiring** — the discipline that has already worked +through six intents, undo/redo, and the pencil. + +**Fact 1 — the GUI is the only unverified surface in the repo.** The rendering +and interaction have never been visually verified (no display in the dev +environment); only the `ViewMap` click-plane math is unit-tested (3 tests, +`main.rs:530`). Everything below it is golden-locked or conformance-gated. But +verification does **not** need a display: the crate already rasterizes +headlessly via `resvg` (`main.rs:160`), so image snapshots of the rendered +score are ordinary `#[test]`s. + +**Fact 2 — the interactive rendering path is a dead end; the data under it is +not.** Every edit re-renders the whole score to an SVG string, parses it with +`usvg`, rasterizes with `resvg`, and uploads one texture (`main.rs:247`). That +is O(score) per keystroke, and a single texture cannot survive real documents +(GPU max-texture dimensions). But the Bravura outlines the renderer draws from +are **staff-space, y-up path data independent of SVG** +(`outlines_generated.rs:1`), and casting-off multi-system layout has landed +(Phase 3 tranche 1, `0316160`). One structural caveat, corrected from this +plan's first draft: `ResolvedLayoutIR` carries pages → systems → staves → +measures as *structure*, but its drawable primitives — `glyphs`, `strokes`, +`curves` — are **flat top-level arrays** (`resolved.rs:86-100`), not owned by +systems. Per-system damage therefore requires explicit primitive ownership +added to the IR before T4 (§3.7), not inferred spatially (cross-system curves +and boundary-straddling primitives make inference ambiguous). And the glyph +assets are not yet a shared seam: the outline table is private to +`render-svg` and stores SVG `d` strings (`outline.rs:5`), so a typed-path / +shared-assets extraction is likewise T4-prerequisite work (§3.7). + +**Fact 3 — the editor has no document.** The GUI opens a hard-coded testkit +fixture (`main.rs:197`). Neither editor crate depends on `epiphany-bundle` +(probed: no reference in either `Cargo.toml`). Meanwhile the bundle's own model +says what a saved document *is*: "A block of operation envelopes (**the +canonical document**)" (`chunk.rs:19-20`), and `Bundle::create/open/commit` +plus operation-block read/write are public API +(`bundle.rs:184,265,606,488`). The session side is ready to be wired: ops are +minted under a replica identity (`editor-core/src/lib.rs:305-333`), +`with_identity` guards on the authored history (`lib.rs:392-398`), and the +applied/authored split is explicit (`lib.rs:443`). + +**Fact 4 — selection and input are one-object, click-only.** The session's +selection is `Option` (`lib.rs:304`) — no range, no list, no caret. +These are seam gaps, not GUI gaps, and they gate copy/paste, batch edits, and +MuseScore-style note entry. They are named tranches below, not part of T1. + +**Fact 5 — a parallel tranche is in flight and freezes specific surfaces.** +`CONTRACT_PUSH4B_RESOLVER.md` reserves `crates/epiphany-editor-gui/**` for this +track by name (line 27) and claims `epiphany-core` + its `DECISIONS.md` for +itself; as of `b0acacb` that tranche is actively in progress in the working +tree. Its gates pin, until it lands: + +* requirement counts at **212 / 282 / 282** (`requirement_labels.rs:12-14`) — + so this track adds **no `.tex` edits and no requirement labels**; +* the conformance suite at **8/8** (`conformance_suite.rs:202`) — so **no new + conformance gate** is promoted mid-flight (ordinary `#[test]`s are fine; + their gate is "0 failed", not a count); +* **zero golden churn** — their tripwire for wire leakage; this track moves no + golden while they fly. + +Probed decoupling: `ScoreTuningContext` is referenced nowhere outside +`epiphany-core`, so their field addition cannot break the editor crates. +`epiphany-bundle` and `epiphany-ops` were never in the resolver's blast +radius, but T1b's work in them waits regardless (§Ruling B blockers) — only +T1a runs during the flight. + +**Fact 6 — the resolver tranche is building the editor's playback input.** Its +deliverable is (pitch-space position, tuning system, reference) → **Hz**. That +is exactly what a playback tranche consumes. Playback is therefore sequenced +*after* Push 4b rather than blocked on a tuning engine of its own; the seam is +named here so it is a plan, not a rediscovery. + +**Fact 7 — CI already has the shape this track needs.** The MSRV-pinned +workspace jobs exclude `epiphany-editor-gui`; a separate pinned-stable +`editor-gui` job tests it (`ci.yml:99,109,131`). T1a's goldens run inside +that existing job with **one additive CI change**: an `if: failure()` +artifact-upload step so failing goldens' actual/expected/diff images are +reviewable after the run (Ruling C) — paths printed in assertions refer to +the ephemeral runner and are local-reproduction aids only. No effect on the +pinned counts or resolver safety. A future app crate mirrors the same +exclusion + job pattern. + +**Fact 8 — every edit today re-materializes the world, and that is the +deepest technical risk on the whole road.** `EditorSession::apply` reduces the +**entire accumulated log onto the pristine open-time base** on every edit +(`lib.rs:1033`) and re-engraves the whole score. This is correct — it *is* the +canonical reduction, which is why the session renders exactly what a peer +would — and at probe scale it is fine. At orchestral scale it is the +difference between an editor and a slideshow. The hard constraint on any fix: +an incremental result MUST be **byte-identical to the from-scratch canonical +reduction** — incrementality is an optimization of the determinism contract, +never an alternative to it. The intended mechanism — a checkpoint plus the +envelope tail — exists in outline only: today's canonical-base payload +carries reducer bookkeeping, not graph values (Fact 10), so the checkpoint +form is itself blocker-i work (§Ruling B). Incremental *engraving* (re-solving only damaged +systems) has the same shape and the same determinism obligation. This is a +named track (§3.1) with a named ladder position (T4b): performance budgets are +**staged** — reduce, engrave, scene-build, paint measured separately — because +an end-to-end latency number that mixes them cannot attribute blame (§Ruling +A), and a renderer verdict is uninformative while reduction dominates. + +**Fact 9 — the specified model already anticipates the product; what is +missing is engines and seam coverage, not vision.** Probed: `PartDefinition` +with per-part layout overrides, visibility overrides, and auto-cue sources is +specified, and **"Parts Are Projections, Not Storage"** is normative — +`req:graph:part-content-projection` (`core_spec.tex:6380-6410`). `LyricLine` +is a modeled graph object with a typed id and codec discriminant +(`core/src/lib.rs:360,515,551`). `SetUserSystemBreak` already exists as an +operation — authored layout decisions are ops, in the canonical document, like +everything else. The gap between Epiphany and a MuseScore-class product is not +a missing data model; it is engines, editor intents over the modeled objects, +and product surface (§3.7 is the verified registry). Model-completeness work +is *coordinated spec+core tranches* in the Push-4b mold — consumed by this +track, never invented inside it. + +**Fact 10 — the canonical model constrains persistence more than this plan's +first two drafts assumed.** Verified across both reviews: + +* The replicated operation set is a **grow-only CRDT** + (`req:semops:grow-only-operation-set`, `core_spec.tex:6924`); an envelope + leaves canonical state only through pruning whose replacement frontier + **strictly dominates** the old one (`core_spec.tex:11536`). +* **Genesis is outside the operation set, deliberately** (ratified Pass-12 + decision): "there is no `CreateCanvas`/`CreateInstrument`" + (`binary_format.tex:2420`), and `CreateStaff`'s graph-aware reduction + **preconditions a live instrument** (`reduce.rs:3792`). Reduction onto an + empty score therefore cannot reach the staff/instance/voice chain notes + require — the §Ruling-B genesis blocker. +* The `canonical_base` role MUST stay schema-major 0 (`MaterializedState`); + a higher-stamped base forces read-only (`bundle.rs:857`); the major-2 + full-`Score` bytes are the **acceleration-snapshot** role + (`roundtrip.rs:339`), whose `SnapshotId` is an acknowledged test-harness + stand-in with no normative derivation (`binary_format.tex:698`). And + `MaterializedState` serializes reducer bookkeeping — effects, existence, + spellings, breaks, conflicts, pending — **not** the + `Region`/`Staff`/`Event`/`Instrument`/`Part` graph values (`reduce.rs:499`, + `binary_format.tex:1695`); pruning may then remove the covered operation + blocks that carried those values (`core_spec.tex:11553`), and the + acceleration snapshot cannot repair the loss (it is discardable and MUST + be ignored or rebuilt on disagreement). **After pruning, today's canonical + roots cannot reconstruct an editor `Score`.** +* The format requires **migrate-on-read** for older canonical blocks + (`binary_format.tex:2453,2532`), but `decode_envelope` takes no + schema-major and decodes embedded values at current layouts + (`envdecode.rs:622`); writes must stamp blocks via + `operation_block_versioned` with the block's max payload major + (`bundle.rs:111`) — the §Ruling-B versioned-decode blocker. **Minimal + stamping** means a current writer *legitimately* emits major-0, -1, and -2 + blocks according to their contained values (`payload.rs:200`), and the + manifest carries no writer provenance — "written by the current writer" is + unprovable from the file. +* Manifest operation roots are **deduplicated, deterministically ordered + sets** (`manifest.rs:465`) — committed history is a set, not a recoverable + chronological prefix. +* Extension edit-barrier injection is the *bundle opener's* job + (`lib.rs:453`); the durable unsafe-edit tombstone encoding is deferred at + the format level (`core_spec.tex:11962` area); `Bundle::commit` + auto-restores omitted unknown-extension declarations (`bundle.rs:678`). +* `FileStore::create` **truncates at open** (`store.rs:152`) and tracks file + length in memory (`store.rs:141`) — the atomic-commit protocol is + crash-safe for one cooperating writer, not a concurrent-writer protocol, + and a lock acquired after a truncating open is too late. +* A failure at the commit-point flush is **indeterminate**: the bundle + poisons itself read-only and requires reopening (`bundle.rs:601,744`) — + neither a save success nor an ordinary failure. And after reopening, a + *readable* generation is not thereby *durable*: the failed barrier was + `sync_all` itself (`store.rs:194`), so only a fresh successful durability + barrier settles the outcome. + +Ruling B is drafted against these constraints, not around them. + +--- + +## 2. The tranche ladder + +Named in order. Each tranche gets its own contract, mutation-verified tests, +and the full gate. Nothing here edits `.tex` — if a future tranche wants +normative editor requirements, that is a separate spec-side decision made +after the resolver tranche lands. + +* **T1a — the visual golden harness (resolver-parallel; contract next).** + Ruling C, in `editor-gui` plus one additive CI artifact-upload step. + Independent of every Ruling-B question; dispatches now. +* **T1b — the document layer (post-resolver; contract after its blockers + resolve).** Rulings B and D: `EditorDocument` + single-writer enforcement + in `epiphany-bundle`. Its runway, in order: (1) the **graph-state + persistence decision** — how canonical graph state is persisted across + *both* genesis *and* pruning, driven by the field-by-field `Score` table + (§Ruling B, blocker i); a coordinated spec/core/ops/format decision, or an + explicit T1b scope limit to empty/metadata/region documents; (2) the + **versioned-decode disposition** — the migrate-on-read API in + `epiphany-ops`, preferred and eventually mandatory, or the enforceable + current-layout restriction (§Ruling B, blocker ii); (3) the **Ruling-D + ownership API**, specified in the contract before code. +* **T2 — selection model v2 (post-resolver).** Range and list selection in + `editor-core` (`Selection` becomes plural with an anchor), `within(rect)` + wired to the GUI as rubber-band select; the batch intents selection makes + meaningful (delete-range, transpose-range); **copy/paste, whose clipboard + format is a T2 ruling — a versioned fragment projection, not the Text + Projection itself**: the TP grammar defines only complete documents + (`text_projection.tex:957`; `textproj/src/lib.rs:24`), so the fragment + form must define closure over referenced objects, external references vs + included dependencies, fresh-id remapping on paste, destination placement + (region/voice/time), causal-context treatment, and partial-structure + policy (ties, tuplets, beams, spanners). It may reuse TP leaf/value + productions; paste mints fresh destination operations rather than + importing source envelopes. Fragments are **untrusted input**: the ruling + also specifies byte/count/depth limits and unknown-version rejection. + Also: promotion of T1a's golden harness to a numbered conformance gate in + testkit (the count moves here, deliberately). +* **T3 — note-entry caret.** The input cursor as headless session state + (position, advance-by-duration), the MuseScore-style entry loop — with the + entry seam **input-method-agnostic**: a caret intent takes (pitch, duration) + and does not care whether they came from typed letters, an on-screen + keyboard, or **step-time MIDI**. Live MIDI entry is the primary input method + for most working musicians, so the seam is designed and tested for it from + the first commit; the device wiring itself (a `midir`-style listener minting + caret intents) lands app-side, T3 if cheap, T4 otherwise. Real-time + (played-against-a-click) entry, and the metronome it requires, are + playback-era work (T5), not T3 — audio dependencies do not enter the + workspace before T5's architecture exists. GUI wiring after the seam is + proven. +* **T4 — the rendering tranche.** The Ruling-A architecture built as the new + app crate (Ruling D): direct vector canvas over `ResolvedLayoutIR`, + viewport-culled, per-system damage against the IR's (by-then) explicit + primitive ownership (§3.7); a **command registry** as the app's action + architecture from day one (§3.5); the toolkit spike (§Ruling A) opens this + tranche. Prerequisites from §3.7 land first: IR primitive ownership, the + shared typed glyph-asset seam, and the text-run primitive decision. +* **T4b — incremental materialization + engraving (the app's exit + criterion).** **Checkpointed-reducer-state-plus-tail** reduction (the + "snapshot-plus-tail" name is retired: today's canonical-base payload + cannot rehydrate graph content — §Ruling B blocker i) and per-system + re-solve, each gated by **byte-equality against the from-scratch result** + (§3.1). Sequenced + *before* the app claims parity and retires the demo, and *before* T5 — + T4's staged budgets (Fact 8) tell us how much is needed; the app does not + ship "at parity" while paying O(score) per keystroke. +* **T5 — playback seam.** Consumes the Push-4b resolver's Hz (Fact 6); + transport + audio sit app-side per the Chapter 1 core/product boundary. + Scoped honestly as its own plan when reached; two scope decisions are + already visible and named now: what `SoundConfiguration`'s opaque bytes + *mean* (patch references? synth state? — the answer shapes §3.5's plugin + runtime), and MTS export (§3.3) so microtonal scores sound right outside + Epiphany. + +--- + +## 3. The architecture-risk map + +The selected risks that shape the rulings — each with what gates it and its +**foreclosure guard**. This is *not* a complete MuseScore-class roadmap: major +product areas remain deliberately unmapped here — full notation-command +coverage, score/instrument setup and templates, lyrics/text engraving, +style/inspector systems, autosave/recovery, printing, packaging/distribution, +plugin security & versioning. They are named so their absence is a decision; +each gets mapped when its gating work exists. + +### 3.1 Incremental materialization + incremental engraving + +The Fact-8 risk. Two coupled halves: reduce from a checkpoint + envelope +tail instead of from genesis — which needs a **reducer-from-state-plus- +frontier API in `epiphany-ops`** *and* a checkpoint payload that can +rehydrate both CRDT bookkeeping and graph content (today's +`MaterializedState` cannot, Fact 10 — the checkpoint form is §Ruling-B +blocker-i work), each its own tranche — and re-engrave only damaged systems. Both carry the byte-identity obligation — the conformance +suite's role here is an equivalence gate (incremental vs from-scratch, +byte-compared) before any incremental path is trusted. **Fast open belongs to +this track too**: acceleration snapshots with a trusted validation story, a +selection/retention policy (frontier coverage, reduction version, profile), +and a normative `SnapshotId` — T1b deliberately writes none (Ruling B), so +"fast open" is never claimed while cold-open still replays the log. Ladder +position: T4b. +**Foreclosure guard:** Ruling B keeps every saved document's full envelope log +canonical and intact (grow-only), which is precisely what makes any future +incremental result *checkable* against from-scratch truth. T4's damage unit is +the *system* — the same unit incremental engraving will re-solve — so paint +damage and engrave damage share a boundary instead of fighting over one. + +### 3.2 Parts and multi-view (document ≠ session ≠ view) + +Specified and normative (Fact 9): parts are projections with layout/visibility +overrides. Architecturally this forces the split Ruling D now makes concrete: +one **document** (bundle + canonical operation state), one **session** per +editing replica (undo, selection, caret), many **views** (score + each part, +page vs continuous — each its own engraving of the same graph, so solver + +resolved layout ultimately belong at the view level). Gated on: a +parts-materialization engine (spec+core tranche), and T4's view architecture. +**Foreclosure guard:** T1b introduces `EditorDocument` and keeps save/bundle +ownership OFF `EditorSession`'s public shape (Ruling D condition); T4's canvas +takes a `ResolvedLayoutIR` it is *given* rather than owning the pipeline, so +N views are N canvases over one document. + +### 3.3 Interchange: MusicXML, MIDI, MEI, and adoption + +Nobody migrates to a notation editor that cannot read their existing scores. +MusicXML import/export (and MIDI in/out) is adoption-critical for any +"next MuseScore" claim, and it is absent from every current plan. It is its +own future track — importers/exporters as separate crates behind seams, +exactly the `render-svg` pattern, with import producing *operations* (a +MusicXML file becomes a minted op log over a genesis, so an imported score is +a first-class document with provenance, not a foreign object). Alongside +MusicXML and MIDI: **MEI**, the academic/archival standard — a spec-driven, +deterministic, microtonal platform's natural early adopters are exactly MEI's +community — and **MTS export** (MIDI Tuning Standard), which the spec already +names on the import side (`core_spec.tex:3333`). + +A sequencing fact the tranche ladder hides: **interchange is headless and +parallelizable.** An importer consumes `epiphany-ops`/`-core`/`-textproj` and +never touches the editor ladder — a MusicXML→op-log converter can be built as +its own parallel track well before T4/T5, gated on model coverage (§3.7) and +the same genesis-persistence decision as T1b, not on the editor. And a +*partial* importer (notes, rhythms, meters, keys) is already the evaluation +gateway prospective users need; import completeness then tracks model +completeness, honestly reported per element. +**Foreclosure guard:** Ruling B's op-log-is-the-document semantics is +precisely what makes import-as-minting possible. Named here so the product +roadmap never treats it as an afterthought. + +### 3.4 Collaboration — the differentiator MuseScore cannot retrofit + +Stated precisely (this section's first draft overclaimed): **the operation-set +convergence substrate exists** — canonical concurrent reduction converges +byte-identically in any delivery order, and that substrate is what no +incumbent can bolt on. What does *not* yet exist is the editing layer above +it: the session's commit gate **rejects any non-clean reduction** +(`lib.rs:1033`, `reduce.rs:604`) — a collaboration-hostile simplification, +since real collaboration requires conflict-bearing documents to remain open +and editable where safe; remote envelopes must apply **without entering local +undo history**; canonical-base/frontier reconciliation, equivocation and +unresolved-operation surfacing, and durable sync checkpoints with idempotent +retry all need design. Undo across a sync boundary is the known hard edge: a +committed op is never retracted by prefix-dropping (Fact 10, grow-only) — +that is the **undo-as-operation** design (compensating operations), gated on +this track. +**Foreclosure guard:** Ruling B writes **only applied units** into documents +— an envelope never reaches canonical state while its author considers it +undone — and every session mints under a fresh replica with counters that can +never collide (Ruling B identity rule). T1b's save semantics are the +async-collab substrate; that is why they get a full ruling rather than an +implementation detail. Ruling B's open-policy matrix keeps conflict-bearing +documents *openable* (read-only, status surfaced) rather than rejected. + +### 3.5 Command architecture, scripting, and Chapter-11 extensions + +MuseScore has hundreds of commands, keybindable and scriptable; Chapter 11 +already declares extension points and keeps the plugin runtime product-side. +The seam's intent methods (`delete_selection`, `insert_note_at`, …) are the +right primitives, but the app must not hardcode toolbar→method calls the way +the demo does: T4 builds a **command registry** — every action a named, +introspectable, keybindable command over seam intents — which is also the +future scripting/plugin surface and the macro/repeat substrate. When the +plugin runtime arrives, the anticipated substrate is **WASM** — +language-agnostic, sandboxable, content-addressable like everything else in +the platform — and the permission/isolation model ships **with the first +plugin, not after it**: edit barriers already exist in the editor +(`editor-core/src/barriers.rs`), and the runtime must honor them from day one. +**Foreclosure guard:** T4 starts with the registry; nothing in T1–T3 needs to +change, because headless intents are exactly what commands wrap. + +### 3.6 Accessibility, internationalization, and performance budgets + +MuseScore 4 made screen-reader accessibility a headline feature; a successor +cannot regress it, and it cannot be retrofitted onto a toolkit that lacks an +accessibility tree — nor onto a canvas that has a tree but exposes nothing +meaningful through it (§Ruling A criterion 4). Likewise the spec's Chapter 10 +performance budgets exist as types with the reference suite deferred — +editing latency should eventually be gated, not vibes-checked, and gated +**per stage** (Fact 8: reduce / engrave / scene-build / paint), because an +end-to-end number cannot attribute blame. Text-heavy surfaces assume +translation from the start. And one differentiator worth naming: a +**Music Braille translator** — no major notation tool has first-class +Braille score output. Its input is the **materialized `Score`** (or a +dedicated semantic projection of it), NOT the Text Projection: the TP +serializes document identity and envelope history +(`text_projection.tex:957`), and two different histories can materialize the +same score — Braille output must not vary with edit history. The TP can be a +transport; it is not the semantic source. Named as an accessibility-track +deliverable, unscheduled. +**Foreclosure guard:** accessibility semantics, text shaping, and staged +latency are **hard T4 spike criteria** in Ruling A, not tiebreakers. + +### 3.7 Model, engraving, and IR completeness — the verified registry + +Fact 9's posture made concrete. Verified against source this pass; these are +**coordinated spec+core/IR tranches in the Push-4b mold** — the editor ladder +consumes them and never blocks on them — and the sequencing currency is the +**schema-major budget** ("a major is a budget to spend deliberately", +`PLAN_PUSH4B_TUNING.md`), not editor tranche numbers: + +* **Genesis persistence (T1b blocker i, §Ruling B):** how genesis-only graph + data — instruments, canvas — is canonically persisted and travels with a + document. The one registry item that *does* gate an editor tranche. +* **Articulations, dynamics, ornaments are empty wire types.** + `ArticulationMark` / `DynamicMark` / `OrnamentMark` are unit structs + (`event.rs:49-57`) that already encode — giving them real payloads is a + **schema major**, to be batched deliberately together with their engraving + semantics (placement, collision, spacing impact), not dribbled out. +* **`NoteheadShape` is cited but defined nowhere** — the spec's + `NoteDecisions` carries the field (`core_spec.tex:9603`) but no enum + definition exists in spec or Rust. Gates percussion (X, diamond), early + music (void, rhombus), and contemporary heads. +* **Percussion is modeled but unmapped.** `UnpitchedEvent` exists with + `UnpitchedMemberId(pub u32)` opaque (`event.rs:45,127`); the member → + staff-position → notehead-glyph → sound mapping that makes drums editable + does not. Pairs naturally with the `NoteheadShape` tranche. +* **Grace notes: modeled, never engraved.** `GraceKind` is carried by both + pitched and unpitched events (`event.rs:91,114,137`); `epiphany-engrave` + contains no reference to grace at all. Spacing/slur/stem consequences make + this an engraving-track item. +* **Figured bass and tablature are absent from the spec entirely** (verified: + zero mentions). Chapter-scale model decisions, sequenced by product + priority — named so their absence is a decision, not an oversight. +* **The version-aware envelope decoder (T1b blocker ii, §Ruling B):** the + migrate-on-read API in `epiphany-ops` the format already requires + (Fact 10) — also a prerequisite for any importer/exporter that reads real + documents (§3.3). +* **The layout-IR readiness tranche (T4 prerequisite):** explicit per-system + **primitive ownership** (or deterministic per-system primitive ranges) for + the flat `glyphs`/`strokes`/`curves` arrays (Fact 2); a **shared typed + glyph-asset seam** (the Bravura outline table is private to `render-svg` + and stores SVG `d` strings — a canvas needs typed vector paths from a + shared crate); and **text-run primitives** in the resolved IR — titles, + lyrics, chord symbols, rehearsal marks, instrument names — with shaping, + fallback, and metrics consistent across canvas, SVG/PDF export, hit + testing, and accessibility (Ruling A criterion). +* **The engraving-quality track:** the Standard-tier solver and real + quality-metric computation (Chapter 9's nine axes) — repeatedly deferred, + and the actual gate on "professional engraving" claims. +* **Graphic/aleatoric notation is NOT on this list** — the model is already + real (`GraphicEvent`, `IndeterminateEvent` implemented and encoded, + `codec.rs:44`; `EventOrderingDAG` + time-brackets specified, + `core_spec.tex:2672,2719`). What it lacks is product surface: drawing tools + (a Ruling-A spike criterion) and editor intents. It is a differentiator + waiting on the canvas, not on the spec. + +--- + +## 4. Rulings + +### Ruling A — interactive rendering architecture — **GRANTED 2026-07-23 (as amended)** + +**The product canvas paints vectors directly from `ResolvedLayoutIR`; the SVG +string path is demoted to export and goldens.** Concretely: tessellate typed +glyph paths and the IR's strokes/curves straight to the GPU surface; cull by +viewport; re-tessellate only damaged **systems** — the same unit incremental +engraving will later re-solve (§3.1), so paint damage and engrave damage share +a boundary. The interactive path never serializes to an SVG string. The canvas +consumes a `ResolvedLayoutIR` it is *given* (§3.2's guard), so part views and +split views are more canvases, not more pipelines. + +**Prerequisites (before or at T4 open, from §3.7):** 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. + +What this ruling does **not** pin: the toolkit/tessellation stack. The T4 +spike decides it, bounded by these recorded criteria: + +1. **Fill correctness:** `epaint`'s filled-path tessellator does not implement + even-odd/nonzero fill for paths with holes — and glyphs have holes — so + "just egui shapes" is likely out; the candidates are lyon-tessellated + meshes inside egui, or a vector renderer (e.g. Vello) behind a window + shell. +2. **Staged latency, not end-to-end latency:** reduce / engrave / scene-build + / paint measured **separately** on large multi-system fixtures against + Chapter-10-style budgets (Fact 8). A toolkit verdict from an end-to-end + number is uninformative while reduction or solving dominates; the spike + measures the stages the toolkit actually owns. +3. **Text pipeline (hard criterion):** shaping, font fallback, bidi/complex + scripts, and metrics consistent between interactive canvas, SVG/PDF + export, hit testing, and the accessibility tree. A stack with no credible + text story is disqualified regardless of vector performance. +4. **Accessibility semantics, not toolkit support (hard criterion):** an + accessibility tree (e.g., AccessKit) is necessary but not sufficient — the + spike must demonstrate **one score fragment exposing meaningful semantics + through it**: labeled notes/rests/measures, focus movement, navigation, + selection state, and command activation. A toolkit can carry a tree while + the custom canvas remains unusable to a screen reader; that outcome fails + this criterion. +5. **Vision-critical interactions prototyped, not assumed:** an overlay layer + suitable for collaborative presence cursors (§3.4); freehand/shape input + for graphic-region editing (§3.7); touch. These are what a wrong toolkit + forecloses. +6. The demo's egui pin is 0.29 (0.35 redesigned the `App` trait); whether the + app crate starts on modern egui, iced, or a Vello surface is the spike's + call under criteria 1–5. + +SVG (`render-svg`) remains the export format, the golden format, and a +correctness cross-check for the T4 canvas — as **defined geometry/scene +equivalence plus a bounded visual differential under a controlled backend**, +NOT pixel equality: a GPU tessellator legitimately differs from `resvg` in +antialiasing and curve flattening while being geometrically correct. +Pixel-exact comparison is reserved for Ruling C, where both sides are the +same SVG/resvg pipeline. + +### Ruling B — the document layer and persistence semantics — **REDRAFTED ×2 — NOT grantable yet** + +*(First draft withdrawn for violating the grow-only operation set. Second +draft amended by the 2026-07-23 second review. Two blockers stand between +this ruling and grant; both are outside the resolver-parallel blast radius, +which is why T1b sequences after the resolver.)* + +**Blocker (i) — canonical graph-state persistence, across genesis AND +pruning.** Two halves of one question. *Genesis:* "every piece of content +enters as operations" is false for more than instruments — genesis is +outside the operation set by ratified design (`binary_format.tex:2420`), +`CreateStaff` preconditions a live instrument (`reduce.rs:3792`), and the +`Score` root also carries staff groups, parts, tuning context, tempo map, +analysis layers, and views (`graph.rs:1693`), for which the operation +vocabulary (`payload.rs:120`) has incomplete or no construction coverage — +resolving instruments alone could still leave parts, staff groups, custom +tuning, analysis layers, and views unsavable. *Pruning:* the canonical-base +payload is `MaterializedState` — reducer bookkeeping without graph values — +while pruning may remove the covered blocks that carried those values +(Fact 10): after pruning, canonical roots alone cannot reconstruct an editor +`Score`, which also undermines any "base-plus-tail" reading of §3.1/T4b +until the checkpoint payload can rehydrate graph content. + +Before choosing "persist genesis" vs "add operations", the resolution +requires a **field-by-field table over the `Score` root**: field; canonical +default; existing create/modify/delete coverage; whether arbitrary imported +values are representable; and whether the field belongs in genesis, the +operation log, or derived state. Resolution, one of: **(a)** a coordinated +spec/core/ops/format decision on canonical graph-state persistence (the +recommended path — it also unblocks import, §3.3, and T4b's checkpoint); or +**(b)** an explicitly scope-limited T1b (empty / metadata / region +persistence only), with the full table named as the prerequisite for +*useful* documents. The T1b contract opens with this disposition. + +**Blocker (ii) — versioned decode.** Reopening any document whose blocks +carry older schema majors requires migrate-on-read +(`binary_format.tex:2453,2532`); `decode_envelope` is current-layout-only +(`envdecode.rs:622`). A "written-by-the-current-writer" restriction is +**unenforceable**: minimal stamping means a current writer legitimately +emits major-0, -1, and -2 blocks by content (`payload.rs:200`), and the +manifest carries no writer provenance — rejecting lower majors would reject +T1b's own files. Resolution, preferred: the **version-aware +decoder/migration API in `epiphany-ops`** (reads driven by each block's +`schema_version`; its own tranche, §3.7), with mixed-major reopen tests, +**mandatory before general `EditorDocument::open`**. The only honest interim +restriction, if T1b proceeds first: *T1b supports only envelope bytes +decodable under current layouts; recognized historical layouts receive a +distinct `UnsupportedHistoricalEncoding` error* — enforceable, limited, and +stated in the contract. Writes in every case stamp blocks via +`operation_block_versioned` with the maximum payload major in the block +(`bundle.rs:111`). + +**The ruling (as it stands, pending the blockers):** + +* **Ownership and the save protocol (the Ruling D condition):** T1b + introduces `EditorDocument`, owning the bundle handle, the committed + operation set, the **committed generation**, read-only status, identity + allocation, and extension + anomaly state. T1b grants **one writable + session lease per document, and the lease is unforgeable in the type + system** — a bare `save(&mut EditorSession)` is rejected as a design, + because a probe-mode session, a session leased from another document, or a + session based on a different committed generation could otherwise be + persisted into the wrong bundle as a validly encoded but semantically + unrelated document. The contract picks one concrete shape — + `WritableEditorSession<'doc>`, a `DocumentSession` guard owning + `&mut EditorDocument`, or a private lease token carrying document identity + + committed generation that `save` validates — and specifies **how the + session materializes committed + local operations after promotion** (an + immutable shared reference to the committed set, or a coordinator; the + document exclusively owns the mutable committed state either way). Save + commits the leased session's staged units and, **only after a successful + commit and durability barrier, atomically promotes exactly those units** + from the session's undoable state into the immutable committed partition + (`lib.rs:312` ties every active envelope to an undo unit — promotion is a + partition move, never a clear-while-active). **Dirty state lives with the + session/lease**, not the document. The redo stack's fate at the save + boundary is specified and tested in the contract (redo units reference + unsaved envelopes by construction; the contract proves whether they remain + valid or are cleared). A multi-session coordinator is §3.2's future, not + T1b's. +* **Documents arise from genesis** (as constrained by blocker i). The + canonical document is the operation log reduced over the document's + genesis; `EditorDocument::create` mints all post-genesis content as + operations. Opening an arbitrary in-memory `Score` (the demo fixture path) + remains supported as a **probe mode and is not savable**: a score whose + construction is in neither genesis nor the log cannot be a canonical + document. Any further construction gap discovered en route fails closed + and is filed, not papered over. +* **Save appends; a successful save is an undo boundary.** Save commits + exactly the units staged at save time as new operation blocks. The + post-save invariant (committed history is a *set*, `manifest.rs:465` — a + positional-prefix claim was withdrawn): **committed_after = + committed_before ∪ local_applied_at_save**, and every promoted local unit + leaves local undo without ever leaving canonical reachability + (authored−applied is a non-contiguous *subsequence* after forks, + `lib.rs:326`). Undo never crosses a save: undoing a committed envelope + requires a *compensating operation* (undo-as-operation, §3.4), never + removal (Fact 10). No manifest re-referencing, no block repacking. +* **Commit outcomes are three-way, and reconciliation must prove + durability, not visibility.** Failure *before* the commit point: units + remain unsaved and undoable. Success: units are promoted. + **Indeterminate commit-point flush** (the bundle poisons itself read-only, + `bundle.rs:601,744`): the failed barrier was `sync_all` itself + (`store.rs:194`), so a generation readable after reopen may be kernel + cache, not durable state. Reconciliation therefore: (1) reopen and + identify the selected generation; (2) confirm whether the exact staged + envelope set is reachable; (3) execute a **fresh durability barrier** and + require it to succeed *before* reporting saved and promoting units; (4) if + that barrier fails, remain in an indeterminate / recovery-required state — + never declare saved *or* unsaved. The injected-fault tests distinguish + "visible after reopen" from "confirmed durable". +* **No snapshots in T1b — of either role.** The `canonical_base` role is + written only by pruning (none in T1b). Acceleration snapshots are + **omitted entirely**: as reviewed, validating one against full re-reduction + on load delivers no fast open (replay stays on the cold path), using one + before validation displays untrusted state, selection/retention among + multiple snapshots is unspecified, and the `SnapshotId` stand-in must not + be productized as a derivation. Reopen is full replay; fast open joins + §3.1 when a real validation strategy exists. +* **Reopen preserves causal history.** Opening a document loads the stored + envelopes as an immutable **committed partition** of the session's + operation set — a *logical* partition, not a chronological prefix + (manifest roots are deduplicated sets, `manifest.rs:465`). Materialization + reduces committed + session ops together, and the first new edit's causal + context **extends the stored frontier** (an empty context would author an + edit causally concurrent with the very state on screen; a claimed frontier + without predecessors present would strand the edit pending — + `lib.rs:909,1021`). **No frontier is stored** (T1b writes no base and no + snapshots), so the frontier is **derived exactly from committed + membership**, honoring the DVV model (`causal.rs:1`): the vector floor + covers only the *contiguous* counter prefix per replica; operations beyond + a gap become individual dots. Required frontier-builder test: committed + {(r,0), (r,2)} → floor `r:0` + dot `(r,2)`, never floor `r:2` — + undo/forking makes counter gaps routine. Undo/redo operate strictly on + session units. Full-log + re-reduction cost at scale is Fact 8's known debt; the + reducer-from-base-plus-frontier API is §3.1's tranche, not T1b's. +* **The open-policy matrix (every `Bundle::is_read_only()` cause + propagated), with two distinct read-only outcomes:** a + **`PreservedDocument`** (metadata/bundle access only, NO materialized + editor session — the UI must never imply the score was faithfully + displayed) versus a **read-only `EditorDocument`** (canonical state + understood and rendered; editing prohibited). Hard byte corruption → open + error; unsupported canonical operation major → **`PreservedDocument`** + (its operations cannot be decoded into a score); bundle / profile anomaly + → preserved read-only at the strongest level honestly claimable; semantic + conflict, pending dependency, or equivocation in reduction → **read-only + `EditorDocument` with surfaced status** until collaboration support lands + (§3.4) — never a rejection of a valid document; identical duplicate + envelopes across blocks → accepted as set duplicates, not corruption. +* **Identity: fresh by default.** Every session mints under a **fresh random + replica** (the v0 `getrandom` convention). Resuming a prior replica is + **not offered in T1b**: a max-counter scan cannot see orphaned blocks, + older manifests, backups, or a concurrently open session on the same + replica — durable resumption needs high-water storage with atomic + counter-range reservation, covering operation *and* entity ids, deferred. + Entity-id minting scans the committed partition + the session's authored + log, preserving the never-re-mint guarantee (`lib.rs:326`) across reopen. +* **Single writer: lock before anything destructive.** `FileStore::create` + truncates at open (`store.rs:152`) — so creation uses `create_new`, and + overwrite is open-without-truncate → **acquire the exclusive advisory + lock** → validate overwrite authorization → truncate. Lock denied is a + defined result (read-only open or error, per caller intent). Generation + + file-UUID are revalidated immediately before commit, with a defined + **external-modification error** (surfaced, never silently overwritten or + retried) — scoped honestly to **cooperating writers of the same file**: an + atomic path replacement is invisible to a held inode and is out of scope + unless the contract adds path-identity revalidation. Tests drive two + independently opened handles and prove the second cannot truncate or + mutate the first's document. This is `epiphany-bundle` work. +* **Extensions fail closed.** Barrier injection is the opener's job + (`lib.rs:453`) and the durable tombstone encoding is deferred at the format + level (Fact 10). T1b opens any bundle carrying extension declarations + **read-only**; unsafe edits are refused on bundle-backed documents. + Writable extension-bearing documents wait for the format tranche that + supplies the tombstone channel — silently preserving invalidated extension + data would violate a MUST. + +Deferred, named so they are plans: undo-as-operation (§3.4); durable replica +resumption; pruning (and with it any `canonical_base` writing); acceleration +snapshots + fast open (§3.1); the reducer-from-base API (§3.1); the +multi-session coordinator (§3.2). + +### Ruling C — the visual golden harness — **GRANTED 2026-07-23 (as amended and clarified)** + +**Golden images of the resvg-rasterized score, as ordinary `#[test]`s in +`epiphany-editor-gui`, running in the existing `editor-gui` CI job — compared +as decoded pixels, not encoded files.** Design points: + +* **What is locked:** the rasterized pixmap of `render()` output in + `GlyphMode::PathOutline` at a fixed `px_per_staff_space` — the exact surface + the GUI displays. `render-svg` already golden-locks SVG *bytes*; this locks + the raster step and the edit loop's visible result (fixture as opened; after + a scripted pencil insert; after undo — each state one golden). This is + verification of the **score raster layer**; egui widgets and overlays remain + outside it (below). +* **Comparison contract (amended 2026-07-23):** decode the committed PNG and + compare **dimensions plus raw RGBA bytes** exactly — never the encoded PNG + file, which would also lock encoder/compression behavior and can churn + while every pixel is identical. On failure the test writes **actual, + expected, and diff images** to the test-output directory and names their + paths in the assertion message. +* **CI artifacts (clarified 2026-07-23):** assertion-message paths refer to + the ephemeral runner and are **local-reproduction aids**; the reviewable + record is an `if: failure()` `upload-artifact` step added to the + `editor-gui` job — the one additive CI change (Fact 7). No effect on + pinned counts or resolver safety. +* **The goldens are also the render-determinism diff gate:** any solver or + renderer change that alters output pixels fails CI until the diff is + reviewed and deliberately re-blessed — the editor-level analogue of the + reduction-determinism criteria, and the review moment engraving changes + deserve. +* **[granted 2026-07-23]** the multi-system casting-off fixture **is** + goldened in T1a — it is the layout path real documents take, and the larger + re-bless surface is accepted deliberately: whenever engraving improves, the + diff there is exactly the review moment the goldens exist to create. One + casting-off golden alongside the single-staff edit-loop states. +* **Determinism basis:** `PathOutline` mode uses no fonts, and + `resvg`/`tiny-skia` are pure Rust with deterministic rasterization; CI and + dev are both Linux. The project's re-bless discipline applies (never + re-bless to make a test pass; a diff is a finding). If cross-platform drift + ever appears, the fallback is a bounded per-pixel tolerance — recorded + then, not pre-engineered now. +* **What is not covered, stated honestly:** egui widget interaction (toolbar, + overlay painting, event routing) — the selection overlay and click plane + remain unit-tested via `ViewMap`. Widget-level harnessing (`egui_kittest` + exists for newer egui; the pin is 0.29) is evaluated in T4 when the toolkit + is chosen, not before. +* **Promotion:** wiring these into the numbered conformance suite would move + the 8/8 count the resolver contract pins — so promotion to a `[9/9]` gate is + T2, explicitly. + +### Ruling D — crate strategy, CI, and ownership — **GRANTED CONDITIONALLY 2026-07-23** + +**The product GUI is a new crate (working name `epiphany-editor-app`), created +at T4, not before; the demo stays as the seam probe until the app reaches +parity, then is retired by explicit decision.** T1–T3 need no new crate: their +work lands in `editor-core` (seam + document layer), `epiphany-bundle` +(single-writer enforcement), and `editor-gui` (probe + goldens). When T4 +creates the app crate it joins the MSRV-exclusion set and gets its own +pinned-stable CI job, mirroring `ci.yml:109`'s existing pattern; it is built +around the §3.5 command registry from its first commit. The demo is never +grown into the product. + +**Grant condition (held through all three reviews):** the T1b contract +specifies the **document/session ownership API** — `EditorDocument` owning +bundle, committed state and generation, read-only status, identity, +extension + anomaly state; the leased session owning undo, dirty state, +selection/caret, solver/layout; save as a document method over an +**unforgeable document-bound session type or lease token** (§Ruling B — a +bare `&mut EditorSession` does not satisfy the condition), with the atomic +post-commit-and-durability promotion protocol — *before* any implementation. +The ruling is granted contingent on that API appearing in the contract and +surviving its review. + +--- + +## 5. Parallel-safety contract with the resolver tranche + +While `CONTRACT_PUSH4B_RESOLVER.md` is in flight (it is, as of `b0acacb` — +uncommitted resolver work sits in `epiphany-core` in this working tree), +**only T1a runs**, and it: + +* touches only `crates/epiphany-editor-gui/**`, `.github/workflows/ci.yml` + (the single additive `if: failure()` artifact-upload step in the + `editor-gui` job), and `spec/*.md` additions — no `epiphany-core`, no + `.tex`, no `DECISIONS.md` but this track's own future files; +* adds no requirement labels (counts stay 212 / 282 / 282), promotes no + conformance gate (suite stays 8/8); **adds new `editor-gui` PNG baselines + but modifies/re-blesses no existing golden and changes no fuzz digest** — + keeping the resolver's zero-churn tripwire unambiguous; +* lands only green (`cargo test --workspace` clean at every landing) — the + resolver's gate runs workspace-wide and must never be reddened from our + side; +* whoever lands second rebases; both contracts state the same frozen numbers, + so a spurious count movement is detected by either side. + +T1b's surfaces (`editor-core`, `epiphany-bundle`, `epiphany-ops` for the +versioned decoder) wait for the resolver to land regardless of file overlap — +its blockers, not just courtesy, set that order. + +--- + +## 6. Review ledger + +House-ratification style: one line per finding, disposition and location. + +### First review — 2026-07-23, 14 findings, all accepted + +| # | finding (short) | disposition | +|---|---|---| +| 1 | Saved envelopes cannot be re-referenced away (grow-only, `:6924`; pruning dominance, `:11536`) | **Ruling B redrafted**: save = undo boundary; undo-as-op stays §3.4 | +| 2 | Snapshot payload/role wrong; genesis gap (major-0 base, `bundle.rs:857`; accel role, `roundtrip.rs:339`) | **Ruling B redrafted**: roles corrected; genesis question later became blocker i (second review) | +| 3 | Empty logs on reopen lose causal history (`lib.rs:909,1021`) | **Ruling B redrafted**: committed partition; contexts extend stored frontier | +| 4 | Replica-resume can reuse ids | **Ruling B redrafted**: fresh random replica default; resumption deferred behind durable reservation | +| 5 | Extension barriers/tombstones unhonorable in T1 (`lib.rs:453`, `bundle.rs:678`) | **Ruling B**: extension-bearing bundles open read-only; fail closed | +| 6 | Active-prefix rewrite ignored forks + block granularity (`lib.rs:326`, `block.rs:98`) | Dissolved by finding-1 redraft; saved-set-is-a-prefix invariant recorded | +| 7 | FileStore is not a concurrent-writer protocol (`store.rs:141`) | **Ruling B**: lock + revalidation + two-handle tests; tightened again by second review #7 | +| 8 | IR primitives are flat, not system-owned (`resolved.rs:86-100`); glyph seam private | **Fact 2 corrected**; IR-readiness tranche in §3.7; Ruling A prerequisite | +| 9 | Rendering ruling omitted general text | **Ruling A criterion 3** (hard); text-run primitives in §3.7 | +| 10 | Performance sequencing circular | **Fact 8 amended** (staged budgets); **T4b added** as app exit criterion | +| 11 | Document/session/view asserted, not designed | **Ruling D grant condition**; protocol specified by second review #4 | +| 12 | Collaboration section overclaimed (`lib.rs:1033`) | **§3.4 narrowed** + itemized remaining work | +| 13 | PNG goldens must compare decoded pixels | **Ruling C amended** and granted | +| 14 | Status/roadmap wording | Header restated per-ruling; §3 reframed as risk map | + +### Second review — 2026-07-23, 11 findings, all accepted + +| # | finding (short) | disposition | +|---|---|---| +| 1 | **Blocker**: no usable document from empty genesis (no `CreateInstrument`, `binary_format.tex:2420`; live-instrument precondition, `reduce.rs:3792`) | **Ruling B blocker i**; T1 split — T1a now, T1b post-resolver, contract opens with the genesis disposition | +| 2 | **High**: reopen needs version-aware envelope decode (`envdecode.rs:622` vs `binary_format.tex:2453,2532`; `bundle.rs:111`) | **Ruling B blocker ii**; migrate-on-read tranche named in §3.7, or honest current-major restriction | +| 3 | **High**: clipboard ruling named a format that does not exist (`text_projection.tex:957`) | **T2 entry redrafted**: versioned *fragment projection* is a T2 ruling; paste mints fresh ops | +| 4 | **High**: save/dirty ownership protocol unanswered (`lib.rs:312`; roots are sets, `manifest.rs:465`) | **Ruling B + D condition**: single writable session lease; `save(&mut EditorSession)`; atomic post-commit promotion; dirty on session; "committed partition" language | +| 5 | Conflict-bearing documents need an open policy (`reduce.rs:604`) | **Ruling B open-policy matrix**; all `is_read_only()` causes propagated | +| 6 | Acceleration snapshot delivers no fast open; `SnapshotId` stand-in must not be productized (`binary_format.tex:698`) | **Ruling B**: T1b writes no snapshots; fast open joins §3.1 | +| 7 | Lock must precede destructive open (`store.rs:152`); path replacement undetectable | **Ruling B single-writer bullet tightened**: `create_new`, lock-then-truncate, scoped external-modification claim | +| 8 | Indeterminate commit-point flush (`bundle.rs:601,744`) | **Ruling B commit-outcome bullet**: three-way protocol + reopen-and-reconcile + injected-fault tests | +| 9 | T4 must not claim pixel equality with resvg goldens | **Ruling A closing amended**: geometry/scene equivalence + bounded visual differential | +| 10 | Golden diff files vanish in CI (`ci.yml:109` uploads nothing) | **Ruling C + Fact 7**: `if: failure()` artifact upload — the one additive CI change | +| 11 | Accessibility criterion must test semantics, not toolkit support | **Ruling A criterion 4 rewritten**: score fragment with labels, focus, navigation, selection, activation | + +### Third review — 2026-07-23, 9 findings, all accepted + +| # | finding (short) | disposition | +|---|---|---| +| 1 | **High**: blocker (i) scoped too narrowly — `Score` root carries staff groups, parts, tuning, analysis layers, views (`graph.rs:1693`) | **Blocker (i) widened** to canonical graph-state persistence; field-by-field table required before choosing a resolution | +| 2 | **High**: `MaterializedState` cannot restore the graph after pruning (`reduce.rs:499`, `core_spec.tex:11553`) — undermines base-plus-tail | **Blocker (i) widened** to cover pruning; **T4b renamed** to checkpointed-reducer-state-plus-tail; Fact 8/§3.1 caveated | +| 3 | **High**: "current-major-writer-only" fallback unenforceable under minimal stamping (`payload.rs:200`) | **Blocker (ii) rewritten**: version-aware decoder mandatory before general open; interim = current-layout-decodable only, `UnsupportedHistoricalEncoding` for recognized historical layouts | +| 4 | **High**: `save(&mut EditorSession)` enforces neither lease nor document binding; prefix invariant wrong | **Ruling B ownership bullet rewritten**: unforgeable document-bound session type/token; post-promotion materialization specified; invariant → committed_after = committed_before ∪ local_applied_at_save; **Ruling D condition strengthened** | +| 5 | **Medium-high**: reopen visibility ≠ durability after failed `sync_all` (`store.rs:194`) | **Commit-outcome bullet rewritten**: fresh durability barrier required before promotion; tests distinguish visible from durable | +| 6 | Unsupported-major preservation ≠ an editor session | **Open matrix split**: `PreservedDocument` vs read-only `EditorDocument` | +| 7 | No stored frontier; max-counter derivation invalid under gaps (`causal.rs:1`) | **Reopen bullet**: frontier derived exactly from committed membership — contiguous floor + dots; {(r,0),(r,2)} test required | +| 8 | §5 "moves no golden" misleading — T1a adds baselines | **§5 reworded**: adds new PNG baselines; re-blesses nothing existing | +| 9 | Braille must consume materialized semantics, not TP history; clipboard fragments are untrusted input | **§3.6 rewritten** (materialized `Score` as input); **T2 fragment ruling** gains limits + unknown-version rejection | + +--- + +## 7. Next + +**Dispatch `CONTRACT_EDITOR_T1A_GOLDENS.md` now** — Ruling C is granted, +T1a's surfaces are resolver-safe (§5), and nothing in it waits on Ruling B. +The T1b contract is drafted **after the resolver lands**, opening with, in +order: the graph-state-persistence disposition with its field-by-field +`Score` table (blocker i), the versioned-decode disposition (blocker ii), +and the Ruling-D ownership API over an unforgeable document-bound session +type — then Ruling B's grant is re-sought against that concrete contract. T2+ contracts are written +only when their tranche opens; the §3 tracks get plans of their own when +their gating work exists.