# 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.