13 KiB
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:
- This contract's sign-off (precedes any dispatch).
- 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.
- 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 existingrasterizehelper (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 thepngdev-dependency fallback proves necessary (§prohibitions)..github/workflows/ci.yml— exactly one additive step in the existingeditor-guijob (ci.yml:109): uploadtarget/golden-failures/**withif: 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 (
pngis already inCargo.lockvia that tree). If the feature is not exposed through resvg's re-export, the fallback is apngdev-dependency (the version already in the lock) — neverimage, 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 --workspaceclean); whoever lands second rebases.
What to build
1. The comparator and bless machinery (src/goldens.rs)
- Rasterize through the binary's existing
rasterizefn (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, anddiff.png(per-pixel highlight) totarget/golden-failures/<test_name>/and name all three paths in the panic message. - Bless:
EPIPHANY_BLESS_GOLDENS=1writes/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 togoldens/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), theninsert_note_at, re-render, compare togoldens/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 togoldens/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:
- 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.rsscaffolding; 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.ymldiff; 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:
- Comparator → compare encoded PNG bytes instead of decoded RGBA: the re-encoding unit test must die.
- Comparator → skip the dimension check: the dimension-mismatch test must die.
- Comparator → suppress artifact writing on mismatch: the artifacts-produced test must die.
- G2 → skip the
insert_note_atcall (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). - G3 → skip the
undo()call: G3 must die. - G4 → substitute
ten_measure_single_staff(0)for the slurs fixture: G4 must die (on the system-count value assertion or pixels — report which). - 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:
cargo fmt --all --checkcargo clippy --workspace --all-targets→ 0 warningscargo test --workspace→ 0 failedRUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps→ 0cargo run -q -p epiphany-testkit --example conformance_suite→ 8/8cargo 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 anyDECISIONS.mdbut the newepiphany-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
imageor 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.