//! Drives the whole conformance suite at scale from the command line, so the //! heavy gates can run outside the unit-test timeout — the analogue of //! `epiphany-determinism`'s `fuzz_roundtrip` and `epiphany-bundle`'s //! `fuzz_crash`. Every gate is deterministic, so any failure reproduces from //! its seed. //! //! Usage: //! cargo run --release --example conformance_suite [SCALE] //! //! `SCALE` (default 1) multiplies the iteration counts. `SCALE=10` is a soak //! run; `SCALE=0` runs a fast smoke pass. Exits non-zero (via panic) on the //! first violation. use epiphany_testkit::{ bundle_harness, convergence, corpus, editloop, equivocation, fixtures, generators, gminor, layout_stub, negative, prepass_harness, roundtrip, textproj, Rng, }; /// The suite's total gate count, printed in every `[N/TOTAL_GATES]` line and /// the final summary. T2 W3 adds gate 9 (`golden-gate` feature); without the /// feature this stays at 8, so every print in this file — the final `ok` /// summary included — is byte-identical to before this tranche /// (`spec/CONTRACT_EDITOR_T2_SELECTION.md` §W3: "Without the feature the /// suite builds and prints exactly today's 8/8"). #[cfg(feature = "golden-gate")] const TOTAL_GATES: u32 = 9; #[cfg(not(feature = "golden-gate"))] const TOTAL_GATES: u32 = 8; fn main() { let scale: u64 = std::env::args() .nth(1) .map(|s| s.parse().expect("SCALE must be an integer")) .unwrap_or(1); let n = |base: u64| (base * scale).max(if base == 0 { 0 } else { 1 }); // 0. Agent A's headline hand-off gate: 1,000,000 round-trip iterations. // (Scaled; at SCALE=0 a smaller smoke count still runs.) let det_iters = if scale == 0 { 50_000 } else { 1_000_000 * scale }; eprintln!("[0/{TOTAL_GATES}] determinism round-trip gate: {det_iters} iters"); roundtrip::run_determinism_roundtrip_gate(det_iters, 0x0A11_CE5E_EDED_2024); // 1. Canonical round-trip (criterion 4): type-level corpus. let iters = (50_000 * scale).max(2_000); eprintln!("[1/{TOTAL_GATES}] canonical round-trip corpus: {iters} iters"); roundtrip::run_roundtrip_corpus(iters, 0x00C0_FFEE_1234_5678); // 1b. Bundle manifest + reducer-bookkeeping serialization + full-Score byte // round-trip (item 5's whole-score codec, via reduce_onto). eprintln!("[1b ] manifest + bookkeeping + full-Score serialization stability"); for seed in 0..n(64) { roundtrip::assert_manifest_roundtrip(&roundtrip::committed_manifest(seed)); let mut rng = Rng::new(seed.wrapping_mul(0x0100_0193).wrapping_add(17)); roundtrip::assert_manifest_roundtrip(&generators::rich_manifest(&mut rng)); let session = generators::operation_envelopes(&mut rng, 40, 3, 6, 6); roundtrip::assert_reduction_serialization_stable(&session, seed); let (score, frontier) = convergence::materialized_score(seed.wrapping_add(101)); roundtrip::assert_score_serialization_stable(&score, &frontier, seed); let envs = generators::operation_envelopes(&mut rng, 24, 3, 8, 8); roundtrip::assert_operation_block_summary_survives_storage(&envs, seed.wrapping_add(202)); } roundtrip::assert_content_mutation_changes_serialization(); // 2. Crash safety (criterion 2): the testkit driver + Agent D's gate. let crash_iters = (10_000 * scale).max(1_000); eprintln!("[2/{TOTAL_GATES}] crash recovery: {crash_iters} iters (testkit) + bundle gate"); bundle_harness::run_crash_recovery(crash_iters, 0xF00D_BEEF_1234_5678); bundle_harness::bundle_crash_recovery_fuzz(crash_iters, 0x0123_4567_89AB_CDEF); // 3. Equivocation (criterion 3): testkit driver + Agent C's gate. eprintln!("[3/{TOTAL_GATES}] equivocation order-independence"); for seed in 0..n(500) { equivocation::run_equivocation(16, seed.wrapping_mul(0x9E37_79B9).wrapping_add(1)); } equivocation::ops_equivocation_fuzz((10_000 * scale).max(1_000), 0x1234_5678); // 4. Manifest selection. eprintln!("[4/{TOTAL_GATES}] manifest selection"); for seed in 0..n(32) { bundle_harness::run_manifest_selection(seed); } // 5. Convergence (criterion 1): real-Score convergence through reduce_onto, // plus the reducer-bookkeeping projection convergence. eprintln!("[5/{TOTAL_GATES}] convergence across delivery orders (real Score + bookkeeping)"); for seed in 0..n(64) { convergence::run_graph_convergence(6, seed.wrapping_mul(0x9E37_79B9).wrapping_add(11)); } for seed in 0..n(500) { convergence::run_convergence(24, 8, seed.wrapping_mul(0x9E37_79B9)); } for seed in 0..n(32) { convergence::run_two_staff_convergence(8, seed.wrapping_mul(0x9E37_79B9).wrapping_add(7)); } // 5b. Audit regression guards (every defect the Agent C audit surfaced). eprintln!("[5b ] audit defect regression guards"); negative::run_all(); // 6. Reduction determinism (criterion 5): a large set reduced many ways, the // testkit's authoritative causal-order gate, + Agent C's own gate. let big = (1_000 * scale).max(1_000) as usize; eprintln!("[6/{TOTAL_GATES}] reduction determinism: {big}-envelope set, 10 orders"); { let mut rng = Rng::new(0x5EED_0006_0F0F_0F0F); let envelopes = generators::operation_envelopes(&mut rng, big, 3, 40, 40); convergence::assert_causal_order_respected(&envelopes); convergence::assert_reduction_determinism(&envelopes, 10, &mut rng); } // Authoritative: causal-order correctness over many conformant sets. convergence::run_authoritative_reduction_gate( (10_000 * scale).max(1_000) as usize, 3, 0x0CA0_05A1, ); // Supplementary: Agent C's own hand-off gate (permutation invariance). convergence::ops_reduction_determinism_fuzz((10_000 * scale).max(1_000), 0x00C0_FFEE); // 7. Layout round-trip (criterion 6). eprintln!("[7/{TOTAL_GATES}] layout round-trip"); for seed in 0..n(128) { layout_stub::round_trip(&fixtures::ten_measure_single_staff(seed)); layout_stub::round_trip(&generators::graph::valid_score_rich(seed)); } // 7b. Track A — Agent H pre-pass merge gate (spelling + decomposition): // the representative-corpus eligibility taxonomy (F3) and the H harness // (F4) — determinism, spelling correctness, decomposition reconstruction, // RespellPitch precedence, and the non-vacuity tripwire — at scale. eprintln!("[7b ] Agent H pre-pass gate: taxonomy coverage + merge gate"); corpus::run_all(); prepass_harness::run_all(scale.max(1)); // 7c. Track A — the UI seam: the editing-loop vertical slice (hit-test → score // object → operation → reduce → re-layout → re-render → re-resolve // selection). Every fixture must drive a click→sharpen→re-render cycle whose // selection survives the relayout — the contract a GUI's correctness rests // on. (The harness `epiphany-editor-core` packages as a callable API.) eprintln!("[7c ] UI-seam gate: editing loop over the corpus"); for seed in 0..n(48) { for score in [ fixtures::ten_measure_single_staff(seed), generators::graph::valid_score_rich(seed), ] { let report = editloop::run_edit_loop(&score).unwrap_or_else(|| { panic!("seed {seed}: no clickable notehead to drive the editing loop") }); assert!( report.graph_changed, "edit-loop seed {seed}: graph unchanged" ); assert!( report.selection_preserved, "edit-loop seed {seed}: selection lost across relayout" ); assert!( report.render_changed, "edit-loop seed {seed}: edit not visible" ); } } eprintln!("[7d ] cross-implementation decode conformance vectors"); { use epiphany_testkit::vectors; assert_eq!( vectors::COMMITTED, vectors::render(), "{} is stale; regenerate with `cargo run -q -p epiphany-testkit \ --example generate_vectors`", vectors::PATH ); match vectors::verify(vectors::COMMITTED) { Ok(n) => eprintln!(" {n} vectors, every verdict agreed"), Err(failures) => panic!( "{} decode-vector disagreement(s):\n{}", failures.len(), failures.join("\n") ), } } // 7e. Whole-document Text Projection vectors. The strong equation is over // text bytes, never bundle identity; the generated-operation pass also // checks the weaker semantic equation by reducing both sides. eprintln!("[7e ] Text Projection whole-document conformance vectors"); for seed in 0..n(16) { textproj::assert_semantics_preserved(seed); } { use epiphany_textproj::vectors; assert_eq!( vectors::COMMITTED, vectors::render(), "{} is stale; regenerate with `cargo run -q -p epiphany-testkit \ --example generate_vectors`", vectors::PATH ); match vectors::verify(vectors::COMMITTED) { Ok(n) => eprintln!(" {n} vectors, every verdict agreed"), Err(failures) => panic!( "{} Text Projection vector disagreement(s):\n{}", failures.len(), failures.join("\n") ), } } // 7f. G-minor (spec/PLAN_GMINOR_SCHEMA_MINOR.md §4, pin 11): the // independent oracle over the manifest's carried schema minor, built // from known, decodable, in-tree fixtures — never routed through // `TOTAL_GATES`, the same convention 7b-7e already use (this is a // sub-gate of gate 7, not a new top-level numbered gate). It is NOT // evidence that `epiphany-textproj` validates arbitrary hand edits // (pin 11.4): that layer stays a preserving producer by ruled design // (a), carrying whatever SchemaVersion a document declares verbatim. eprintln!("[7f ] G-minor: manifest schema-minor oracle over in-tree fixtures"); { let (checked, not_checkable) = gminor::run_gate(); eprintln!( " {checked} fixture(s) checked (equality oracle), {not_checkable} \ reported not-checkable (never counted as a pass)" ); } // 8. T2 W3 — the [9/9] golden conformance gate (`golden-gate` feature // only; absent without it, and the line above prints "8/8" unchanged). // Re-derives the three T1a golden states headlessly and rasterizes // them exactly as `epiphany-editor-gui`'s demo binary does, comparing // decoded RGBA against the baselines committed there. See // `spec/CONTRACT_EDITOR_T2_SELECTION.md` §W3 and this crate's // `DECISIONS.md` (T2 W3). #[cfg(feature = "golden-gate")] { eprintln!("[8/{TOTAL_GATES}] golden conformance: T1a baselines rasterized headlessly"); golden_gate::run(); } eprintln!("[{TOTAL_GATES}/{TOTAL_GATES}] ok: full conformance suite passed (scale {scale})"); } /// T2 W3: the `[9/9]` golden conformance gate. Promotes the T1a visual /// goldens (`spec/CONTRACT_EDITOR_T1A_GOLDENS.md`, /// `crates/epiphany-editor-gui/src/goldens.rs`) to a numbered gate in this /// suite, entirely behind the `golden-gate` feature so the raster stack /// (`resvg`, pulled in only via the optional dependencies in `Cargo.toml`) /// never enters the MSRV build's dependency closure /// (`spec/CONTRACT_EDITOR_T2_SELECTION.md` §W3). /// /// **Compare-only.** Unlike `epiphany-editor-gui`'s own comparator (which can /// bless and writes failure artifacts), this gate only ever compares: no /// `EPIPHANY_BLESS_GOLDENS` path, no artifact writing. A mismatch panics with /// a message pointing at `cargo test -p epiphany-editor-gui goldens` — the /// crate that owns the baselines, the bless mechanism, and the reviewable /// `actual.png`/`expected.png`/`diff.png` artifacts — as the diagnostic /// surface. /// /// **Cross-crate baseline coupling.** The three committed PNGs live in /// `epiphany-editor-gui/goldens/`, not in this crate — this crate compares /// against them without owning them, resolving the path from *this* crate's /// `CARGO_MANIFEST_DIR` (the only directory `env!` can see at compile time) /// up to the sibling crate directory. Documented here and in `DECISIONS.md` /// (T2 W3) and `epiphany-editor-gui/DECISIONS.md` (T1a): a `git mv` of either /// crate, or a rename of `epiphany-editor-gui/goldens/`, breaks this path /// silently unless both DECISIONS files are updated together. #[cfg(feature = "golden-gate")] mod golden_gate { use std::fs; use std::path::PathBuf; use epiphany_core::TypedObjectId; use epiphany_editor_core::EditorSession; use epiphany_engrave::Engraver; use epiphany_layout_ir::{HitShape, Point}; use epiphany_render_svg::{render, RenderOptions}; use epiphany_testkit::fixtures; use resvg::tiny_skia::Pixmap; /// Runs the three T1a golden-state comparisons. Panics on the first /// mismatch or missing baseline (never a silent skip — the `fs::read` /// failure branch below panics directly, exactly like /// `epiphany-editor-gui/src/goldens.rs::compare`'s own missing-baseline /// path). pub fn run() { // G1 — as opened: `ten_measure_single_staff(0)`, exactly the demo's // open path (`epiphany-editor-gui/src/main.rs:210`, // `EditorApp::new`). let score = fixtures::ten_measure_single_staff(0); let session = EditorSession::open(score, Box::new(Engraver::default())) .expect("the ten-measure fixture renders under the real engraver"); assert_matches_baseline("ten_measure_open", &render_pixmap(&session)); // G2 — after the scripted pencil insert. The click point is // re-derived through the session's own inverses, replicating // `goldens.rs::scripted_insert_target` faithfully: the last system // by lowest `bounding_box.origin.y`, the rightmost Pitch-sourced // notehead within that system's vertical band, offset +0.5 staff // spaces right / +2.0 staff spaces up from that notehead's // right-center. let score = fixtures::ten_measure_single_staff(0); let mut session = EditorSession::open(score, Box::new(Engraver::default())) .expect("the ten-measure fixture renders under the real engraver"); let target = scripted_insert_target(&session); let grid = session .default_grid_at(target) .expect("the target point sits over a metric region"); let outcome = session .insert_note_at(target, &grid) .expect("the target is a clean, unoccupied insert slot"); assert!( outcome.graph_changed, "gate 9: the scripted insert must change the score graph" ); assert_matches_baseline("ten_measure_insert", &render_pixmap(&session)); // G4 — casting-off: `ten_measure_with_slurs(0)` // (`fixtures.rs:777`), the multi-system layout and cross-system // slur-split path the T1a baseline locks. let score = fixtures::ten_measure_with_slurs(0); let session = EditorSession::open(score, Box::new(Engraver::default())) .expect("the slurred ten-measure fixture renders under the real engraver"); assert_matches_baseline("ten_measure_slurs_castoff", &render_pixmap(&session)); } /// Renders `session`'s resolved layout at `px_per_staff_space: 12.0` (the /// demo's default; `RenderOptions`'s other fields stay at their default, /// `GlyphMode::PathOutline` — no fonts) and rasterizes it, matching /// `render_pixmap` in `epiphany-editor-gui/src/goldens.rs`. fn render_pixmap(session: &EditorSession) -> Pixmap { let options = RenderOptions { px_per_staff_space: 12.0, ..Default::default() }; let output = render(session.resolved(), &options); rasterize(&output.svg) } /// Rasterizes a rendered SVG string to a `tiny_skia` pixmap, replicating /// `epiphany-editor-gui/src/main.rs::rasterize_pixmap` exactly: usvg /// parse, dimensions rounded up to whole pixels, an opaque white /// background (so premultiplied alpha stays fully opaque), then /// `resvg::render` over that background. This crate cannot call the /// original directly (it is private to a different crate's binary), so /// the algorithm is copied rather than shared — see the module doc for /// why that coupling is acceptable and documented rather than factored /// out. fn rasterize(svg: &str) -> Pixmap { let tree = resvg::usvg::Tree::from_str(svg, &resvg::usvg::Options::default()) .expect("a rendered score's SVG parses"); let size = tree.size(); let width = size.width().ceil().max(1.0) as u32; let height = size.height().ceil().max(1.0) as u32; let mut pixmap = Pixmap::new(width, height).expect("nonzero rasterized dimensions"); pixmap.fill(resvg::tiny_skia::Color::WHITE); resvg::render( &tree, resvg::tiny_skia::Transform::identity(), &mut pixmap.as_mut(), ); pixmap } /// The click point G2 scripts — derived from `session`'s own rendered /// geometry, never a magic screen constant. Faithfully replicates /// `epiphany-editor-gui/src/goldens.rs::scripted_insert_target`; see that /// function's doc comment for the full derivation rationale (why the /// *last* system is the one with the lowest `bounding_box.origin.y`, and /// why the target is the rightmost Pitch-sourced notehead within that /// system's vertical band rather than the page-wide rightmost notehead). fn scripted_insert_target(session: &EditorSession) -> Point { let last_system = session .resolved() .pages .iter() .flat_map(|page| &page.systems) .min_by(|a, b| { a.bounding_box .origin .y .0 .total_cmp(&b.bounding_box.origin.y.0) }) .expect("casting-off produced at least one system"); let sys_bottom = last_system.bounding_box.origin.y.0; let sys_top = sys_bottom + last_system.bounding_box.size.height.0; let last_notehead = session .hit_test() .regions .iter() .filter_map(|r| match (&r.source, r.shape) { (TypedObjectId::Pitch(_), HitShape::Box(b)) => Some(b), _ => None, }) .filter(|b| { let mid_y = (b.bottom.0 + b.top.0) / 2.0; (sys_bottom..=sys_top).contains(&mid_y) }) .max_by(|a, b| a.right.0.total_cmp(&b.right.0)) .expect("the last system renders at least one notehead"); Point::new( last_notehead.right.0 + 0.5, (last_notehead.bottom.0 + last_notehead.top.0) / 2.0 + 2.0, ) } /// Compares `actual` against the committed baseline /// `epiphany-editor-gui/goldens/{name}.png`: decoded RGBA, dimensions /// first (the Ruling-C contract) — never encoded PNG bytes. Compare-only: /// no bless, no artifact writing (see the module doc). A missing or /// undecodable baseline panics directly, the same "loud failure, never a /// silent skip" contract as `goldens.rs::compare`. fn assert_matches_baseline(name: &str, actual: &Pixmap) { let path = baseline_path(name); let bytes = fs::read(&path).unwrap_or_else(|err| { panic!( "gate 9: no golden baseline at {} ({err}) — `epiphany-editor-gui/goldens/` must \ contain the committed T1a baselines for this gate to compare against; \ diagnose with `cargo test -p epiphany-editor-gui goldens`", path.display() ) }); let expected = Pixmap::decode_png(&bytes).unwrap_or_else(|err| { panic!( "gate 9: baseline at {} is not a decodable PNG: {err}", path.display() ) }); assert_eq!( (actual.width(), actual.height()), (expected.width(), expected.height()), "gate 9: {name} dimensions differ from the committed baseline at {} (actual {}x{} \ vs expected {}x{}); diagnose with `cargo test -p epiphany-editor-gui goldens`", path.display(), actual.width(), actual.height(), expected.width(), expected.height(), ); // A plain `assert_eq!` here would dump both full byte slices (millions // of bytes for a real score raster) into the panic message; a // differing-byte count is the useful signal, and `cargo test -p // epiphany-editor-gui goldens` is where the actual/expected/diff PNG // artifacts to inspect visually already live. if actual.data() != expected.data() { let differing = actual .data() .iter() .zip(expected.data()) .filter(|(a, b)| a != b) .count(); panic!( "gate 9: {name} decoded RGBA differs from the committed baseline at {} \ ({differing} of {} bytes differ); diagnose with `cargo test -p \ epiphany-editor-gui goldens`", path.display(), actual.data().len(), ); } } /// The committed baseline path for a named golden: /// `{CARGO_MANIFEST_DIR}/../epiphany-editor-gui/goldens/{name}.png` — this /// crate's `CARGO_MANIFEST_DIR` is `crates/epiphany-testkit`, and the /// baselines live in the sibling crate `crates/epiphany-editor-gui`. See /// the module doc's "Cross-crate baseline coupling" note. fn baseline_path(name: &str) -> PathBuf { PathBuf::from(concat!( env!("CARGO_MANIFEST_DIR"), "/../epiphany-editor-gui/goldens" )) .join(format!("{name}.png")) } }