263 lines
13 KiB
Markdown
263 lines
13 KiB
Markdown
# 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/<test_name>/` 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.
|