67 KiB
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<N>_<TOPIC>.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<Selection> (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.texedits 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), andCreateStaff'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_baserole MUST stay schema-major 0 (MaterializedState); a higher-stamped base forces read-only (bundle.rs:857); the major-2 full-Scorebytes are the acceleration-snapshot role (roundtrip.rs:339), whoseSnapshotIdis an acknowledged test-harness stand-in with no normative derivation (binary_format.tex:698). AndMaterializedStateserializes reducer bookkeeping — effects, existence, spellings, breaks, conflicts, pending — not theRegion/Staff/Event/Instrument/Partgraph 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 editorScore. - The format requires migrate-on-read for older canonical blocks
(
binary_format.tex:2453,2532), butdecode_envelopetakes no schema-major and decodes embedded values at current layouts (envdecode.rs:622); writes must stamp blocks viaoperation_block_versionedwith 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:11962area);Bundle::commitauto-restores omitted unknown-extension declarations (bundle.rs:678). FileStore::createtruncates 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 wassync_allitself (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-guiplus 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 inepiphany-bundle. Its runway, in order: (1) the graph-state persistence decision — RESOLVED 2026-07-24 (spec/RULING_GENESIS_PERSISTENCE.md): the operation set absorbs genesis, and pruning is parked until the canonical base carries graph values. The lease/save/single-writer machinery does not depend on that tranche, and its parallel-safety is per-rung (spec/PLAN_GENESIS_OPS.md): G1 (CreateInstrument) needs no accept-set raise and never entersepiphany-bundle, so T1b's bundle work runs beside it; G2 spends the raise inbundle.rs, where T1b's single-writer enforcement also lands, so the two must not fly together (corrected 2026-07-24); (2) the versioned-decode disposition — the migrate-on-read API inepiphany-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(Selectionbecomes 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) — RULED 2026-07-24,
tranche not yet dispatched.
spec/RULING_GENESIS_PERSISTENCE.md: the operation set absorbs genesis, so this stops being "how is genesis-only data persisted" and becomes a normal operation-vocabulary tranche — three LWW settings setters and six entity mint families, oneOperationEnvelopeBlockaccept-set raise spent once, Pass-12 K8 reversed. Blocked on theidentitydisposition. The one registry item that did gate an editor tranche; the gate is now the tranche's execution, not the decision. - Articulations, dynamics, ornaments are empty wire types.
ArticulationMark/DynamicMark/OrnamentMarkare 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. NoteheadShapeis cited but defined nowhere — the spec'sNoteDecisionscarries 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.
UnpitchedEventexists withUnpitchedMemberId(pub u32)opaque (event.rs:45,127); the member → staff-position → notehead-glyph → sound mapping that makes drums editable does not. Pairs naturally with theNoteheadShapetranche. - Grace notes: modeled, never engraved.
GraceKindis carried by both pitched and unpitched events (event.rs:91,114,137);epiphany-engravecontains 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-opsthe 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/curvesarrays (Fact 2); a shared typed glyph-asset seam (the Bravura outline table is private torender-svgand stores SVGdstrings — 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). Ownership landed W1 (dd33b34); the glyph-asset seam landed W2 (24f8c80, theepiphany-glyphscrate). The text-run decision is RULED —spec/ANALYSIS_TEXT_RUN_PRIMITIVES.md, approved 2026-07-28: a fourth resolved primitive carrying the source string and the canonical shaped result, with a content-hashed face identity. Three consequences reach outside this bullet. (a) Three of the five categories named above — lyrics, chord symbols, rehearsal marks — carry no text in the model at all, so they are blocked on a core-track schema major, not on the IR; v1 renders the text that exists (metadata, instrument/staff names,TextLineDefinition). (b) A live compliance gap: score text authored through operations is not NFC-validated, because the score-bearing payloads embed the core codec's deliberately non-folding bytes rather than the envelope's NFC-checked string reader — independent of this tranche, and core-track work. (c) The engraver gains a pinned shaper, and the.texamendment adding the primitive is a layout-fingerprint change needing no bundle/wire schema-major move (it follows strokes and curves). - 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,IndeterminateEventimplemented 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.
All three are now discharged — W1 dd33b34, W2 24f8c80, W3 ruled
2026-07-28 (ANALYSIS_TEXT_RUN_PRIMITIVES.md). Criterion 3 below is restated
by that ruling as five testable checks (§5 there): the engraver shapes, so the
spike tests whether a candidate stack can faithfully consume a shaped
result rather than whether it can shape correctly.
What this ruling does not pin: the toolkit/tessellation stack. The T4 spike decides it, bounded by these recorded criteria:
- Compound-path / inner-subpath fill correctness (amended 2026-07-28;
was "fill correctness", framed around the fill rule):
epaint'sPathShapeis a single point loop documenting "Fill is only supported for convex polygons" (epaint-0.35.0/src/shapes/path_shape.rs:14), so it cannot express compound-fill / subtractive-hole semantics — aShape::Veccan group several loops, but grouping paints them, it does not subtract a counter from its enclosing contour. Glyphs are multi-contour with bounded counters, so "just egui shapes" is out; the candidates are lyon-tessellated meshes inside egui, or a vector renderer (e.g. Vello) behind a window shell. The fill rule is not the load-bearing property, and the original wording implied it was. Bravura's contours are correctly oppositely wound (measured signed ring areas, e.g.gClef[8.702, −0.691, −1.803, −0.509]), so even-odd and nonzero agree on every bundled hole. What a candidate must preserve is every filled contour and every bounded counter: a tessellator that keeps only the outer contour paints counters solid, and one that keeps only the largest drops disjoint components such asfClef's two dots. The T4 spike's Round 1 tests both properties separately (spec/CONTRACT_EDITOR_T4_SPIKE.md). - 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.
MEASURED 2026-07-28 —
epiphany-testkit/benches/editor_pipeline.rs, gating the core's portion (envelope construction + reduction + layout, all three the requirement names) againstreq:perf:single-system-edit-latency(16.7 ms), on a session-shaped log — counters from 0 and each envelope carryingactive_prior_context(), because a context-free log measures a different algorithm and understated reduce by ~3× in this bench's first version. At depth 100 / 1,000 / 3,000 / 5,000 / 10,000 on the ten-measure fixture: reduce 223 µs / 2.30 ms / 8.79 ms / 17.53 ms / 54.07 ms, engrave 268 / 314 / 327 / 317 / 260 µs, scene-build 133 / 165 / 152 / 151 / 123 µs, paint 2.21 / 2.86 / 2.80 / 2.78 / 1.36 ms (depth 4,000 measured clean but ungated at 12.99 ms core — aPassrow at 78% of budget flaps under load). Five consequences, all of which sharpen this criterion rather than satisfy it: (a) reduce is the only depth-scaling stage and is superlinear (~n^1.4; 10× the log costs ~23.5× the time), breaking the frame between 3,000 and 5,000 edits — 9.11 ms then 17.84 ms core, so the wall is ~4,500; (b) engrave is flat and small (260–327 µs) and at depth 100 is the larger half of the core's portion, so "uninformative while reduction dominates" holds only past roughly depth 500; (c) paint dominates early and is overtaken by ~1,000 edits — 4.5× the core at depth 100, level at 1,000, far behind after; (d) scene-build is 3–5 µs of IR work plus ~130 µs of SVG serialization, so a direct-IR canvas avoids 2.34 ms at depth 100 = 83% of the full per-edit pipeline, 99.8% of the render path alone (both denominators stated; an unqualified "98%" was supported by neither); (e) the measured depth is document-lifetime, not per session — Ruling B (below) makes reopen full replay: stored envelopes load as a committed partition and materialization reduces committed + session operations together. Nothing resets the depth until the checkpoint/pruning machinery assigned to T4b can write a newcanonical_base. There is no session-reset mitigation to lean on, and note entry mints one operation per note. Sequencing: T4 before T4b still stands — the canvas removes what dominates a document's first ~1,000 edits and is the architecture later tranches build on — but the two are no longer comfortably separated, and T4b's trigger is ~4,500 edits of accumulated history rather than the ~10,000 this bench's first, context-free version reported. The bench watches for it as anXfailrow at depth 5,000. - 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.
- 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.
- 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.
- The demo's egui pin is 0.29 (0.35 redesigned the
Apptrait); 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 E — the clipboard fragment projection — GRANTED 2026-07-23
A versioned s-expression fragment format, values-only, paste-as-minting. The T2 ruling the ladder reserved, now drafted:
- Header and versioning:
(epiphany-fragment (major minor patch)), starting(0 1 0). An unrecognized major is rejected, never partially parsed. The fragment format is application-level and versioned — it is NOT canonical wire format, opens no schema major, and may evolve. - Values, never identities. A fragment reuses the Text Projection's
value/leaf productions (pitches, durations, spellings — the ratified
textual forms) but NOT its document grammar: no document id, no envelopes,
no causal contexts, no object ids. Content is per-event values plus a
rational onset relative to the fragment origin, in per-voice lanes
keyed by ordinal (not
VoiceId). Paste mints fresh operations (InsertEvent+RespellPitchtransactions, fresh ids from the session's minters) — the §3.3 import-as-minting principle at clipboard scale. - Closure, v1 (fail closed, report dropped): notes/rests with their per-event attachments copy; a slur copies iff both endpoints are inside the range, else it is dropped and reported; a partially-selected tuplet refuses the copy (the reducer's own refusal discipline); a tie cut by the range boundary is dropped and reported; derived state (decomposition, spellings that are merely inferred) is never copied — it re-derives.
- Placement:
paste_at(point, &grid)(pencil-style, viaposition_at) andpaste_over_selection()(at the anchor member's onset, in its voice) — both with make-room overwrite semantics reusingmake_room, and both atomic transactions (a refused member rolls back the whole paste). - Untrusted input: hard caps on bytes, event count, and nesting depth, each a named constant with a value-asserting rejection test; unknown version → clean error. Fragments arrive from the OS clipboard; they are input, not trusted state.
Granting this ruling unblocks T2's W4 packet (copy/paste); W1–W3 do not depend on it.
Ruling B — the document layer and persistence semantics — REDRAFTED ×2 — blocker (i) RESOLVED 2026-07-24; blocker (ii) still open
(First draft withdrawn for violating the grow-only operation set. Second draft amended by the 2026-07-23 second review. Two blockers stood between this ruling and grant; both are outside the resolver-parallel blast radius, which is why T1b sequences after the resolver.)
Blocker (i) is RESOLVED —
spec/RULING_GENESIS_PERSISTENCE.md, ratified 2026-07-24. The field-by-field table this blocker demanded was produced asspec/ANALYSIS_GENESIS_PERSISTENCE.md(nine uncovered surfaces, not the handful this text guessed at:canvas.layout_defaults,instruments,staff_groups,parts,tuning_context,spelling_precedence,analysis_layers,views, andStaffInstance.measures, plusidentity). The ruling takes neither (a)-as-written nor (b): the operation set absorbs genesis — Pass-12 K8 is reversed, there is no genesis block, and a document isScore::empty(identity)plus its envelope log. Execution is a Push-4b-class coordinated tranche; the editor track consumes it. Two constraints carry forward into T1b: pruning may not be implemented until disposition C lands (aMaterializedStatebase cannot rebuild a score), and theidentitycursor is ruled in that document's §3 (it stays onScore, byte-equality claims confine toMaterializedState, and from-empty reduction derives the counter). Execution is laddered inspec/PLAN_GENESIS_OPS.md; T1b's bundle work is parallel-safe with G1 but not with G2. The text below is kept as the record of what the blocker was.
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 baresave(&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>, aDocumentSessionguard owning&mut EditorDocument, or a private lease token carrying document identity- committed generation that
savevalidates — 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:312ties 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.
- committed generation that
- Documents arise from genesis (as constrained by blocker i). The
canonical document is the operation log reduced over the document's
genesis;
EditorDocument::createmints all post-genesis content as operations. Opening an arbitrary in-memoryScore(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 wassync_allitself (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_baserole 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 theSnapshotIdstand-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)} → floorr:0+ dot(r,2), never floorr: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: aPreservedDocument(metadata/bundle access only, NO materialized editor session — the UI must never imply the score was faithfully displayed) versus a read-onlyEditorDocument(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-onlyEditorDocumentwith 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
getrandomconvention). 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::createtruncates at open (store.rs:152) — so creation usescreate_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 isepiphany-bundlework. - 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 inGlyphMode::PathOutlineat a fixedpx_per_staff_space— the exact surface the GUI displays.render-svgalready 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-artifactstep added to theeditor-guijob — 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:
PathOutlinemode uses no fonts, andresvg/tiny-skiaare 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_kittestexists 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 additiveif: failure()artifact-upload step in theeditor-guijob), andspec/*.mdadditions — noepiphany-core, no.tex, noDECISIONS.mdbut 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-guiPNG 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 --workspaceclean 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.