//! The five committed fixture strings (recipe §2) and the precommitted //! measured expectations (recipe §4) the generator checks its own shaped //! output against. //! //! Every string below is a Rust string literal with every non-ASCII //! codepoint escaped (recipe §2: "so the file is unambiguous under any //! editor or normalization"). **Never re-record an expectation to make a //! check pass**: [`check_against_recipe`] returning an error is this crate's //! signal to stop and report the disagreement, not to edit the literal it //! failed against. use epiphany_core::{EventId, TypedObjectId}; use epiphany_layout_ir::{BoundingBox as RealBoundingBox, Point as RealPoint, Provenance}; use ttf_parser::GlyphId; use crate::faces::LoadedFace; use crate::identity::build_shaping_identity; use crate::invariants; use crate::shape::shape_text; use crate::types::{ SpikeBoundingBox, SpikeGlyphStyle, SpikePoint, SpikeResolvedText, SpikeTextAlign, }; use crate::{EM_SIZE_STAFF_SPACE, RUN_ORIGIN_STAFF}; /// One committed fixture: its id, W3 §5 purpose, and verbatim literal. pub struct FixtureDef { pub id: &'static str, pub purpose: &'static str, pub text: &'static str, } /// The five fixtures, in the recipe §2 table's order. This exact set, in /// this exact order, is what `output::FixtureFile::validate` restates as a /// literal roster (mirroring `round1-candidates/harness`'s `ROUND1_ROSTER` /// discipline). pub const FIXTURES: &[FixtureDef] = &[ FixtureDef { id: "F-A", purpose: "check 1 (faithful consumption), check 5 (accessibility)", text: "Allegro affettuoso \u{2014} al fine", }, FixtureDef { id: "F-B", purpose: "check 2 (fallback, forced)", text: "Coro \u{05D0}\u{05D1}\u{05D2}", }, FixtureDef { id: "F-C", purpose: "check 2 (uncovered codepoint)", text: "Coro \u{0627}", }, // **Not "check 3 (bidi)".** Ruled 2026-07-29 (recipe §1.2): check 3 // requires an Arabic/Latin run, no Arabic-capable face exists on this // machine, and pin 9 makes an absent required face an environmental // `NOT RUN`. F-D is Hebrew/Latin and cannot exercise contextual Arabic // joining, so it is scored on its own **Supplementary** row and must never // upgrade check 3 to PASS. The purpose string says so because this string // is what `fixtures.json`, `FIXTURES_SUMMARY.md` and every generator's // console output print — labelling it "check 3" there recreates exactly // the scoring ambiguity the ruling forbids, whatever the recipe says // elsewhere. FixtureDef { id: "F-D", purpose: "SUPPLEMENTARY bidi evidence (Hebrew/Latin) — check 3 remains NOT RUN \ (no Arabic-capable face; recipe §1.2)", text: "Allegro \u{05D0}\u{05D1}\u{05D2} con brio", }, FixtureDef { id: "F-E", purpose: "check 4 (hit testing / caret)", text: "Cafe\u{301} \u{2014} resume\u{301}", }, ]; /// Builds one fixture's `SpikeResolvedText`: shapes it against the resolved /// faces, computes `bounds`/`reserved_box`/`origin`, and asserts every W3 §5 /// invariant before returning. pub fn build_fixture( def: &FixtureDef, faces: &[LoadedFace], fixture_ordinal: u64, ) -> SpikeResolvedText { let shaped = shape_text(def.text, faces); // Recipe §3's nominal `(1.6, 0.0)` is not itself on the `1/1024` grid // (see `crate::quantize`'s doc comment) — invariant 5 requires every // position this crate records to be, `origin` included, so it is // quantized like everything else rather than kept as the raw literal. let origin = SpikePoint::new( crate::quantize::quantize_component(RUN_ORIGIN_STAFF.0 as f64), crate::quantize::quantize_component(RUN_ORIGIN_STAFF.1 as f64), ); let bounds = compute_bounds(&shaped.segments, faces, origin); // Reserved-box policy (§3E: "a solver policy over bounds — padding, a // minimum allocation"): this spike has no real solver, so the policy is // the simplest defensible one — bounds padded by a fixed 0.1 staff space // on every side — recorded as a *named* policy, not an unshaped guess. const PAD: f64 = 0.1; let reserved_box = SpikeBoundingBox { left: crate::quantize::quantize_component(bounds.left - PAD), bottom: crate::quantize::quantize_component(bounds.bottom - PAD), right: crate::quantize::quantize_component(bounds.right + PAD), top: crate::quantize::quantize_component(bounds.top + PAD), }; let identity = build_shaping_identity(faces.iter().map(|f| f.identity.clone()).collect()); let source = TypedObjectId::Event(EventId::from_raw(0xF00D_0000 + fixture_ordinal as u128)); let real_provenance = Provenance::projected(source, Vec::new()); let rt = SpikeResolvedText { provenance: (&real_provenance).into(), text: def.text.to_string(), shaping: identity, segments: shaped.segments, clusters: shaped.clusters, bounds, reserved_box, origin, align: SpikeTextAlign::Start, style: SpikeGlyphStyle { rgba: 0x0000_00ff }, layer: 0, }; let expect_unresolved = def.id == "F-C"; invariants::assert_all(def.id, &rt, expect_unresolved); if def.id == "F-D" { invariants::assert_direction_boundary_stops_differ("F-D", &rt, 8) .expect("F-D byte 8 direction-boundary stops"); invariants::assert_direction_boundary_stops_differ("F-D", &rt, 14) .expect("F-D byte 14 direction-boundary stops"); } check_against_recipe(def.id, &rt).unwrap_or_else(|e| { panic!( "{}: measured shaping disagrees with recipe §4's precommitted expectation — STOPPING \ per the packet's rule against silent re-recording:\n{e}", def.id ) }); rt } /// Real-type ink bounding box over every positioned glyph across every /// segment, computed from each glyph's *own resolving face*'s outline /// bounds (`ttf_parser::Face::glyph_bounding_box`) — never estimated from /// advances. A glyph with no outline (a space) contributes no extent but /// still occupies pen advance, exactly as `PositionedGlyph::offset` already /// records. fn compute_bounds( segments: &[crate::types::SpikeShapedSegment], faces: &[LoadedFace], origin: SpikePoint, ) -> SpikeBoundingBox { let mut left = f64::INFINITY; let mut bottom = f64::INFINITY; let mut right = f64::NEG_INFINITY; let mut top = f64::NEG_INFINITY; for seg in segments { let Some(face_idx) = seg.face else { continue }; let loaded = &faces[face_idx as usize]; let rb_face = loaded.face(); let upem = rb_face.units_per_em() as f64; let scale = EM_SIZE_STAFF_SPACE / upem; for g in &seg.glyphs { let Some(bbox) = rb_face.glyph_bounding_box(GlyphId(g.glyph_id as u16)) else { continue; }; let gx = origin.x + g.offset.x; let gy = origin.y + g.offset.y; left = left.min(gx + bbox.x_min as f64 * scale); right = right.max(gx + bbox.x_max as f64 * scale); bottom = bottom.min(gy + bbox.y_min as f64 * scale); top = top.max(gy + bbox.y_max as f64 * scale); } } if !left.is_finite() { // No glyph produced ink (a degenerate all-unresolved fixture) — an // empty box at the origin rather than an infinite one, matching // `epiphany_layout_ir::BoundingBox::default()`'s zero convention. let real_default: RealBoundingBox = RealBoundingBox::default(); return SpikeBoundingBox::from(real_default); } // Round-trip through the real `BoundingBox`/`Point` types (they carry no // extra invariant beyond `f32` storage) so this function is honestly // computing the *real* type's value, not a shape only this crate defines. let _real_point_smoke_test = RealPoint::new(left as f32, bottom as f32); SpikeBoundingBox { left: crate::quantize::quantize_component(left), bottom: crate::quantize::quantize_component(bottom), right: crate::quantize::quantize_component(right), top: crate::quantize::quantize_component(top), } } /// Checks the shaped result against recipe §4's precommitted, measured /// expectations. **A mismatch is stopped and reported, never silently /// re-recorded** — this function's only job is to say which fact disagreed. fn check_against_recipe(id: &str, rt: &SpikeResolvedText) -> Result<(), String> { match id { "F-A" => check_f_a(rt), "F-B" => check_f_b(rt), "F-C" => check_f_c(rt), "F-D" => check_f_d(rt), "F-E" => check_f_e(rt), other => Err(format!( "no recipe §4 check registered for fixture {other:?}" )), } } fn total_glyphs(rt: &SpikeResolvedText) -> usize { rt.segments.iter().map(|s| s.glyphs.len()).sum() } /// F-A: 28 codepoints, 30 bytes, 26 glyphs, all face 0. `ff` ligature at /// byte 9 -> gid 234; `fi` ligature at byte 26 -> gid 97; em dash -> gid 119. fn check_f_a(rt: &SpikeResolvedText) -> Result<(), String> { expect_eq("codepoints", rt.text.chars().count(), 28)?; expect_eq("bytes", rt.text.len(), 30)?; expect_eq("glyphs", total_glyphs(rt), 26)?; // Recipe §4 does not literally write "1 segment" for F-A — it says "all // face 0", from which a single segment follows (unidirectional Latin // text, one face throughout, nothing to split on). This is a derived // check, not a quoted number; `output::FixtureFile::validate` does not // repeat it as a "recipe §4 literal" for exactly that reason. expect_eq("segments", rt.segments.len(), 1)?; if rt.segments[0].face != Some(0) { return Err(format!( "F-A segment 0 resolved to face {:?}, recipe says face 0", rt.segments[0].face )); } expect_cluster_glyph("F-A ff ligature", rt, 9, 11, 234)?; expect_cluster_glyph("F-A fi ligature", rt, 26, 28, 97)?; // The em dash: U+2014 sits at byte offset 19 (after "Allegro affettuoso " // — "Allegro " is 8 bytes, "affettuoso " is 11 bytes, 8+11=19). expect_cluster_glyph("F-A em dash", rt, 19, 22, 119)?; Ok(()) } /// F-B: Latin head "Coro " (5 bytes) -> 5 glyphs on face 0; Hebrew tail /// (6 bytes) -> 3 glyphs on face 1, RTL, clusters descending 4/2/0 (segment- /// relative). Two segments, two faces. fn check_f_b(rt: &SpikeResolvedText) -> Result<(), String> { expect_eq("segments", rt.segments.len(), 2)?; let head = &rt.segments[0]; let tail = &rt.segments[1]; expect_eq( "F-B head bytes", (head.source.end - head.source.start) as usize, 5, )?; expect_eq("F-B head glyphs", head.glyphs.len(), 5)?; if head.face != Some(0) { return Err(format!( "F-B head resolved to face {:?}, recipe says face 0", head.face )); } expect_eq( "F-B tail bytes", (tail.source.end - tail.source.start) as usize, 6, )?; expect_eq("F-B tail glyphs", tail.glyphs.len(), 3)?; if tail.face != Some(1) { return Err(format!( "F-B tail resolved to face {:?}, recipe says face 1", tail.face )); } if tail.direction != crate::types::SpikeTextDirection::Rtl { return Err("F-B tail must be Rtl".to_string()); } // Clusters covering the tail must exist at absolute bytes 5,7,9 (segment- // relative 0,2,4), each a single grapheme/glyph, descending in the // shaped glyph *array* order (checked via clusters' recorded glyph // index order matching descending source offsets is implicit in the // shaping; here we assert the three source spans exist). for abs in [5u32, 7, 9] { let found = rt .clusters .clusters .iter() .any(|c| c.source.start == abs && c.segment == 1); if !found { return Err(format!( "F-B: no cluster starts at absolute byte {abs} in the Hebrew segment" )); } } Ok(()) } /// F-C: U+0627 resolves in neither face — an explicit unresolved cluster. fn check_f_c(rt: &SpikeResolvedText) -> Result<(), String> { let unresolved_count = rt.clusters.clusters.iter().filter(|c| !c.resolved).count(); if unresolved_count != 1 { return Err(format!( "F-C: expected exactly 1 unresolved cluster, found {unresolved_count}" )); } Ok(()) } /// F-D: base level 0; visual runs `0..8` (Latn, face 0), `8..14` (Hebr, /// face 1), `14..23` (Latn, face 0). Three segments. fn check_f_d(rt: &SpikeResolvedText) -> Result<(), String> { expect_eq("segments", rt.segments.len(), 3)?; let expected = [ (0u32, 8u32, Some(0u32)), (8, 14, Some(1)), (14, 23, Some(0)), ]; for (i, (s, e, face)) in expected.into_iter().enumerate() { let seg = &rt.segments[i]; if seg.source.start != s || seg.source.end != e { return Err(format!( "F-D segment[{i}] source {:?}, recipe says {s}..{e}", seg.source )); } if seg.face != face { return Err(format!( "F-D segment[{i}] face {:?}, recipe says {face:?}", seg.face )); } } if rt.segments[1].direction != crate::types::SpikeTextDirection::Rtl { return Err("F-D middle segment must be Rtl".to_string()); } Ok(()) } /// F-E: 15 codepoints, 19 bytes, 13 glyphs; `e`+U+0301 composes to gid 198 /// at byte 3 and again at byte 16; 13 graphemes. fn check_f_e(rt: &SpikeResolvedText) -> Result<(), String> { expect_eq("codepoints", rt.text.chars().count(), 15)?; expect_eq("bytes", rt.text.len(), 19)?; expect_eq("glyphs", total_glyphs(rt), 13)?; let grapheme_count: u32 = rt.clusters.clusters.iter().map(|c| c.grapheme_count).sum(); expect_eq("graphemes", grapheme_count as usize, 13)?; // e+U+0301 is 3 bytes (1 + 2); the composed cluster covering it should // span exactly 3 bytes and carry 1 glyph (gid 198), at byte 3 and byte 16. expect_composed_e_acute(rt, 3)?; expect_composed_e_acute(rt, 16)?; Ok(()) } fn expect_composed_e_acute(rt: &SpikeResolvedText, start: u32) -> Result<(), String> { let c = rt .clusters .clusters .iter() .find(|c| c.source.start == start) .ok_or_else(|| format!("F-E: no cluster starts at byte {start}"))?; if c.source.end - c.source.start != 3 { return Err(format!( "F-E: cluster at byte {start} spans {} bytes, recipe says 3 (e + U+0301)", c.source.end - c.source.start )); } if c.glyph_indices.len() != 1 { return Err(format!( "F-E: cluster at byte {start} carries {} glyphs, recipe says 1 (composed)", c.glyph_indices.len() )); } let seg = &rt.segments[c.segment]; let gid = seg.glyphs[c.glyph_indices[0] as usize].glyph_id; if gid != 198 { return Err(format!( "F-E: cluster at byte {start} is gid {gid}, recipe says gid 198" )); } Ok(()) } /// Asserts the (single) glyph named by the cluster starting at `start` /// (spanning to `end`) is `expected_gid`. fn expect_cluster_glyph( label: &str, rt: &SpikeResolvedText, start: u32, end: u32, expected_gid: u32, ) -> Result<(), String> { let c = rt .clusters .clusters .iter() .find(|c| c.source.start == start && c.source.end == end) .ok_or_else(|| format!("{label}: no cluster spans {start}..{end}"))?; if c.glyph_indices.len() != 1 { return Err(format!( "{label}: cluster {start}..{end} carries {} glyphs, expected exactly 1", c.glyph_indices.len() )); } let seg = &rt.segments[c.segment]; let gid = seg.glyphs[c.glyph_indices[0] as usize].glyph_id; if gid != expected_gid { return Err(format!( "{label}: cluster {start}..{end} is gid {gid}, recipe says gid {expected_gid}" )); } Ok(()) } fn expect_eq(label: &str, actual: usize, expected: usize) -> Result<(), String> { if actual != expected { return Err(format!( "{label}: measured {actual}, recipe §4 records {expected}" )); } Ok(()) } #[cfg(test)] mod tests { use super::*; use crate::faces::{resolve_declared_chain, FaceResolution, LoadedFace}; /// Loads the real declared faces, or `None` (pin 14: environment /// absence) — mirrors `output::tests::real_valid_file`'s pattern so /// these mutation tests exercise the *real* recipe-§4 checks against /// genuinely shaped output, not a hand-built stand-in. fn real_faces() -> Option> { let mut out = Vec::new(); for r in resolve_declared_chain() { match r { FaceResolution::Loaded(lf) => out.push(lf), FaceResolution::Missing { .. } => return None, } } Some(out) } fn require_faces() -> Vec { real_faces().expect("this test requires the two declared faces to be present") } #[test] fn check_f_a_kills_a_wrong_ligature_gid() { let faces = require_faces(); let mut rt = build_fixture(&FIXTURES[0], &faces, 0); let c = rt .clusters .clusters .iter() .find(|c| c.source.start == 9) .unwrap() .clone(); let seg = &mut rt.segments[c.segment]; seg.glyphs[c.glyph_indices[0] as usize].glyph_id = 1; // not gid 234 let err = check_f_a(&rt).unwrap_err(); assert!(err.contains("gid 234"), "{err}"); } #[test] fn check_f_a_kills_a_wrong_em_dash_gid() { let faces = require_faces(); let mut rt = build_fixture(&FIXTURES[0], &faces, 0); let c = rt .clusters .clusters .iter() .find(|c| c.source.start == 19) .unwrap() .clone(); let seg = &mut rt.segments[c.segment]; seg.glyphs[c.glyph_indices[0] as usize].glyph_id = 1; let err = check_f_a(&rt).unwrap_err(); assert!(err.contains("gid 119"), "{err}"); } #[test] fn check_f_b_kills_a_swapped_face_assignment() { let faces = require_faces(); let mut rt = build_fixture(&FIXTURES[1], &faces, 1); rt.segments[1].face = Some(0); // Hebrew tail must be face 1, not 0 let err = check_f_b(&rt).unwrap_err(); assert!(err.contains("face 1"), "{err}"); } #[test] fn check_f_c_kills_a_dropped_unresolved_cluster() { let faces = require_faces(); let mut rt = build_fixture(&FIXTURES[2], &faces, 2); rt.clusters.clusters.retain(|c| c.resolved); // simulate silently dropping it let err = check_f_c(&rt).unwrap_err(); assert!(err.contains("unresolved cluster"), "{err}"); } #[test] fn check_f_d_kills_a_wrong_segment_source_range() { let faces = require_faces(); let mut rt = build_fixture(&FIXTURES[3], &faces, 3); rt.segments[1].source = 9..14; // recipe says 8..14 let err = check_f_d(&rt).unwrap_err(); assert!(err.contains("recipe says 8..14"), "{err}"); } #[test] fn check_f_e_kills_a_wrong_composed_gid() { let faces = require_faces(); let mut rt = build_fixture(&FIXTURES[4], &faces, 4); let c = rt .clusters .clusters .iter() .find(|c| c.source.start == 3) .unwrap() .clone(); let seg = &mut rt.segments[c.segment]; seg.glyphs[c.glyph_indices[0] as usize].glyph_id = 1; // not gid 198 let err = check_f_e(&rt).unwrap_err(); assert!(err.contains("gid 198"), "{err}"); } #[test] fn fixture_roster_matches_the_recipe_table() { let ids: Vec<&str> = FIXTURES.iter().map(|f| f.id).collect(); assert_eq!(ids, ["F-A", "F-B", "F-C", "F-D", "F-E"]); assert_eq!(FIXTURES[0].text, "Allegro affettuoso \u{2014} al fine"); assert_eq!(FIXTURES[1].text, "Coro \u{05D0}\u{05D1}\u{05D2}"); assert_eq!(FIXTURES[2].text, "Coro \u{0627}"); assert_eq!( FIXTURES[3].text, "Allegro \u{05D0}\u{05D1}\u{05D2} con brio" ); assert_eq!(FIXTURES[4].text, "Cafe\u{301} \u{2014} resume\u{301}"); } /// Mutation-first: `expect_eq` must actually fail when the numbers /// disagree, not just when they happen to agree. #[test] fn expect_eq_kills_a_disagreement() { assert!(expect_eq("x", 5, 6).is_err()); assert!(expect_eq("x", 5, 5).is_ok()); } }