//! The constraint-solver interface (Chapter 9 "Constraint-Solver Interface") //! and the v0 stub solver. //! //! Chapter 9 specifies the *interface* and its contracts, not an algorithm. //! This module implements the interface surface in full shape — the //! [`ConstraintSolver`] trait (`solve`/`solve_incremental`/`tier`/`version`, //! `Send + Sync`), [`SolverConfig`] (profile, budget, tie-breaking weights), //! [`SolverState`], the [`InvalidationSet`] (slots/bands/constraints/glyphs), and //! a [`SolveReport`] with its full diagnostic surface (unsatisfied constraints, //! warnings, a [`QualityMetricVector`], budget used, state) — and a //! [`StubSolver`] that, per the QUICKSTART, "returns `SolveStatus::Solved` with //! the input geometry verbatim" — for a constraint-free problem; with //! constraints declared it stays a renderable passthrough but claims no //! satisfaction (see [`StubSolver`]). //! //! **The stub computes no quality metrics** (QUICKSTART: "only the interface — //! don't implement quality metrics"): the //! [`QualityMetricVector`]/[`NormalizedMetric`] *types* and the //! [`TieBreakingWeights`] exist (the interface requires them), and the Quality //! Metric Catalog's normative anchors and threshold tables are transcribed in //! [`crate::quality`] for solvers that do measure (`epiphany-engrave`). The //! `StubSolver` is not a conformant solver and passes no reference suite, so it //! reports the [`SolverTier::Stub`] tier (the honest non-conformance rung, below //! `Minimal`) and an all-worst metric vector. Those values are deliberately //! conservative placeholders, not computed quality measurements; the real solver //! replaces them. use epiphany_core::TypedObjectId; use crate::constrained::{ConstrainedLayoutIR, GlyphObjectId}; use crate::glyph::{all_available, BravuraCatalog, GlyphCatalog}; use crate::resolved::{ PrimitiveIndices, ResolvedGlyph, ResolvedLayoutIR, ResolvedPage, ResolvedSystem, }; use crate::spatial::{Margins, Rect, Size2D}; use crate::vertical_band::VerticalBandId; /// The solver status (Chapter 9 §"The Solver Report"). Variants and their /// authority rules are quoted from the spec. #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum SolveStatus { /// All hard constraints satisfied, target quality reached. Solved, /// All hard constraints satisfied, but warnings were generated. SolvedWithWarnings, /// Deterministic budget exhausted before reaching target quality. The /// returned layout still satisfies all hard constraints. PartialBudgetExhausted, /// Hard constraints cannot be simultaneously satisfied; the layout is /// diagnostic-only. Unsatisfiable, /// Solver bug or unexpected error; the layout is diagnostic-only. InternalError, } impl SolveStatus { /// Whether a layout under this status may be rendered as authoritative /// (Chapter 9 §"The Solver Report"). pub fn is_renderable(self) -> bool { matches!( self, SolveStatus::Solved | SolveStatus::SolvedWithWarnings | SolveStatus::PartialBudgetExhausted ) } } /// The conformance tier a solver claims (Chapter 9 §"Conformance Tiers"). /// /// `Stub` is below the spec's three conformance tiers: it is *not* a conformance /// claim but its honest absence — an interface-only solver that evaluates no /// constraints and computes no quality metrics reports `Stub`, never `Minimal`, /// so a caller cannot mistake the passthrough for the lowest conformant tier. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub enum SolverTier { /// Not a conformance tier: an interface-only / passthrough solver that /// evaluates no constraints and computes no quality metrics (e.g. /// [`StubSolver`]). Ordered below every conformant tier. Stub, /// Minimal Layout Solver. Minimal, /// Standard Engraving Solver. Standard, /// Advanced / Extension-Aware Solver. Advanced, } /// A solver's implementation version (Chapter 9: within a fixed version, /// identical input produces identical output). #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct SolverVersion(pub u32); /// The conformance profile under which to solve (Chapter 9 §"The Solver /// Interface": `SolverConfig.profile` — selects metric thresholds and the active /// constraint/extension set). The registered profile catalog and each profile's /// threshold column are the Quality Metric Catalog's Chapter 6, transcribed as /// [`crate::quality::profile_thresholds`]. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Default)] pub enum SolverProfile { /// Fast, low-quality (draft) profile. Draft, /// The reference engraving-quality profile. #[default] Standard, /// The highest-quality (publication) profile. Publication, } /// Tie-breaking weights among layouts of equivalent quality (Chapter 9 /// §"Quality Metrics": `TieBreakingWeights`). The normative defaults are the /// Quality Metric Catalog's Chapter 4: every weight `1.0` — exactly this /// type's [`Default`]. #[derive(Copy, Clone, PartialEq, Debug)] pub struct TieBreakingWeights { pub collision: f64, pub spacing: f64, pub slur_shape: f64, pub beam_slope: f64, pub vertical_density: f64, pub system_break: f64, pub page_fill: f64, pub casting_off: f64, pub symbol_density: f64, } impl Default for TieBreakingWeights { fn default() -> Self { TieBreakingWeights { collision: 1.0, spacing: 1.0, slur_shape: 1.0, beam_slope: 1.0, vertical_density: 1.0, system_break: 1.0, page_fill: 1.0, casting_off: 1.0, symbol_density: 1.0, } } } /// The deterministic budget (Chapter 9 §"The Solver Interface": `SolverBudget`). /// Wall-clock time is advisory only; the canonical layout depends on the /// deterministic counters. #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub struct SolverBudget { pub max_iterations: u64, pub max_nodes: u64, pub max_constraint_evaluations: u64, pub advisory_wall_time_ms: Option, } impl Default for SolverBudget { fn default() -> Self { SolverBudget { max_iterations: u64::MAX, max_nodes: u64::MAX, max_constraint_evaluations: u64::MAX, advisory_wall_time_ms: None, } } } /// The deterministic budget consumed by a solve (Chapter 9: `SolverBudgetUsed`). #[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] pub struct SolverBudgetUsed { pub iterations: u64, pub nodes: u64, pub constraint_evaluations: u64, pub wall_time_ms: u64, } /// Solver configuration (Chapter 9 §"The Solver Interface": `SolverConfig`): /// the conformance profile, the deterministic budget, and the tie-breaking /// weights. #[derive(Copy, Clone, PartialEq, Debug, Default)] pub struct SolverConfig { pub profile: SolverProfile, pub budget: SolverBudget, pub tie_breaking: TieBreakingWeights, } /// A quality metric normalized to `[0.0, 1.0]`, lower is better (Chapter 9 /// §"Quality Metrics": `NormalizedMetric`). #[derive(Copy, Clone, PartialEq, PartialOrd, Debug, Default)] pub struct NormalizedMetric(pub f64); impl NormalizedMetric { /// Constructs, panicking if the value is not finite or out of `[0.0, 1.0]` /// — conforming implementations construct only valid values (Chapter 9). pub fn new(value: f64) -> Self { assert!(value.is_finite(), "NormalizedMetric must be finite"); assert!( (0.0..=1.0).contains(&value), "NormalizedMetric must lie in [0.0, 1.0]" ); NormalizedMetric(value) } } /// An extension-contributed quality metric id (Chapter 9: `ExtensionMetricId`). #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct ExtensionMetricId(pub u128); /// An extension-contributed quality metric (Chapter 9: `ExtensionMetric`). #[derive(Copy, Clone, PartialEq, Debug)] pub struct ExtensionMetric { pub metric_id: ExtensionMetricId, pub value: NormalizedMetric, } /// The quality metric vector for a layout (Chapter 9 §"Quality Metrics": /// `QualityMetricVector`). An interface-only solver that computes no metrics /// reports the conservative all-worst placeholder /// ([`QualityMetricVector::unmeasured`], every metric `1.0`), never a measured /// value, so a caller cannot mistake an unmeasured layout for a good one. (The /// derived [`Default`] is all-`0.0`/nominal-best and is *not* what the stub /// reports.) A measuring solver computes each axis per the Quality Metric /// Catalog's formulas, normalized through [`crate::quality::normalize`] with /// the catalog's pinned anchors ([`crate::quality::anchors`]). #[derive(Clone, PartialEq, Debug, Default)] pub struct QualityMetricVector { pub collision_penalty: NormalizedMetric, pub spacing_distortion: NormalizedMetric, pub slur_shape_penalty: NormalizedMetric, pub beam_slope_penalty: NormalizedMetric, pub vertical_density_penalty: NormalizedMetric, pub system_break_penalty: NormalizedMetric, pub page_fill_efficiency: NormalizedMetric, pub casting_off_quality: NormalizedMetric, pub symbol_density_uniformity: NormalizedMetric, pub extension_metrics: Vec, } impl QualityMetricVector { /// A conservative placeholder for an interface-only solver that does not /// compute quality metrics. Every built-in metric is worst-valued. pub fn unmeasured() -> Self { let worst = NormalizedMetric::new(1.0); QualityMetricVector { collision_penalty: worst, spacing_distortion: worst, slur_shape_penalty: worst, beam_slope_penalty: worst, vertical_density_penalty: worst, system_break_penalty: worst, page_fill_efficiency: worst, casting_off_quality: worst, symbol_density_uniformity: worst, extension_metrics: Vec::new(), } } } /// Opaque solver state threaded into [`ConstraintSolver::solve_incremental`] /// (Chapter 9 §"The Solver Report": `SolverState`). v0 records the solver /// version and the resolved-glyph count, enough to drive the observational- /// equivalence contract for the trivial stub. #[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] pub struct SolverState { pub solver_version: Option, pub resolved_glyphs: usize, } /// The scope of an incremental invalidation (Chapter 9 §"Incremental Solving": /// `InvalidationScope`). The solver MAY widen this conservatively; it MUST NOT /// narrow it. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub enum InvalidationScope { ObjectLocal, MeasureLocal, SystemLocal, PageLocal, RegionLocal, WholeScore, } /// A horizontal spring-slot id (Chapter 7 §"Spring Slots": `SpringSlotId`). #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct SpringSlotId(pub u128); /// A constraint identifier referenced by [`SolveReport::unsatisfied_constraints`] /// (Chapter 9: `ConstraintId`). The stub never reports any: it evaluates no /// constraints, so it neither claims one satisfied nor names one unsatisfied. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct ConstraintId(pub u128); /// The strength a constraint binds the solver with (Chapter 9 §"Strength /// Levels": `ConstraintStrength`). Constraints do not carry this in the IR — /// the spec's [`crate::LayoutConstraint`] enum has no strength field — so it is /// attached by rule via [`crate::LayoutConstraint::strength`]. #[derive(Copy, Clone, PartialEq, Debug)] pub enum ConstraintStrength { /// Hard constraint. The solver MUST satisfy it or return /// [`SolveStatus::Unsatisfiable`], and MUST NOT treat it as if it were /// `Preferred` for any reason, including quality optimization (Chapter 9 /// §"Strength Levels"). Required, /// Soft constraint with an associated weight. The solver minimizes the /// weighted violation when optimizing; an unhonoured preference is a /// warning ([`SolverWarningKind::LargeSoftConstraintViolation`]), never an /// `Unsatisfiable`. Preferred { weight: f64 }, } /// A declared invalidation (Chapter 9: `InvalidationSet`) over the invalidated /// slots, bands, constraints, and glyphs. #[derive(Clone, PartialEq, Eq, Debug)] pub struct InvalidationSet { pub scope: InvalidationScope, pub slots: Vec, pub bands: Vec, pub constraints: Vec, pub glyphs: Vec, } /// A normative quality-metric axis (Chapter 9 §"Quality Metrics"), referenced by /// [`SolverWarningKind::QualityFloorApproached`]. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub enum QualityMetricKind { Collision, Spacing, SlurShape, BeamSlope, VerticalDensity, SystemBreak, PageFill, CastingOff, SymbolDensity, } /// An extension-defined solver-warning id (Chapter 9: `ExtensionWarningId`). #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct ExtensionWarningId(pub u128); /// The kind of a non-fatal solver warning (Chapter 9 §"The Solver Report": /// `SolverWarningKind`) — every normative variant. #[derive(Clone, PartialEq, Debug)] pub enum SolverWarningKind { LargeSoftConstraintViolation { constraint: ConstraintId, magnitude: f64, }, UnusualLayoutDecision(String), QualityFloorApproached { metric: QualityMetricKind, }, ExtensionWarning(ExtensionWarningId), } /// A non-fatal solver warning (Chapter 9 §"The Solver Report": `SolverWarning`). #[derive(Clone, PartialEq, Debug)] pub struct SolverWarning { pub kind: SolverWarningKind, pub affected_objects: Vec, pub message: String, } /// The solver report (Chapter 9 §"The Solver Report"). The `layout` is always /// present; under a failure `status` it is diagnostic-only and MUST NOT be used /// as if valid. #[derive(Clone, PartialEq, Debug)] pub struct SolveReport { pub status: SolveStatus, /// Whether every hard constraint is satisfied. pub satisfied_hard_constraints: bool, pub layout: ResolvedLayoutIR, /// Unsatisfied hard constraints, if any (empty under `Solved`). pub unsatisfied_constraints: Vec, /// Non-fatal warnings about the solution. pub warnings: Vec, /// Quality metric vector for the returned layout. pub metric_vector: QualityMetricVector, /// Budget consumed during this solve. pub budget_used: SolverBudgetUsed, /// Updated solver state for subsequent incremental calls. pub state: SolverState, } /// The constraint-solver interface (Chapter 9 §"The Solver Interface"). `solve` /// and `solve_incremental` MUST be pure functions of their inputs within the /// determinism contract; the trait is `Send + Sync` per the spec. pub trait ConstraintSolver: Send + Sync { /// The solver's identifying conformance tier. fn tier(&self) -> SolverTier; /// The solver's implementation version. fn version(&self) -> SolverVersion; /// Solve from scratch. fn solve(&self, input: &ConstrainedLayoutIR, config: &SolverConfig) -> SolveReport; /// Solve incrementally over the declared invalidation scope. Must be /// observationally equivalent to [`ConstraintSolver::solve`] restricted to /// that scope (Chapter 9 §"Observational Equivalence"). fn solve_incremental( &self, input: &ConstrainedLayoutIR, prior: &SolverState, invalidations: &InvalidationSet, config: &SolverConfig, ) -> SolveReport; } /// The v0 stub solver (QUICKSTART, Agent E: "the stub returns /// `SolveStatus::Solved` with the input geometry verbatim"). /// /// It copies each glyph's baseline anchor into its resolved position unchanged, /// preserves provenance, carries the engraving decisions and catalog forward, /// and reports all hard constraints satisfied — provided every glyph's metrics /// are bundled and the catalog hash actually covers the consulted metrics /// (Chapter 7 §7.3.2). A glyph whose metrics are not bundled, or a catalog hash /// that does not match its glyphs, is a well-formedness failure reported as /// [`SolveStatus::InternalError`] (never a panic). /// /// **Declared constraints are not evaluated** ([`SolverTier::Stub`]), and the /// report is honest about it in both directions: the solve stays renderable /// (geometry passes through; unevaluated constraints are not a defect in the /// *input*), but `satisfied_hard_constraints` is `false` and a warning names /// the gap — the stub never claims satisfaction it did not check. Chapter 9 /// has no status for "renderable, constraints unevaluated", so the closest /// non-claiming renderable status, [`SolveStatus::SolvedWithWarnings`], is /// used (see DECISIONS.md). pub struct StubSolver; impl StubSolver { fn resolve(&self, input: &ConstrainedLayoutIR) -> SolveReport { let structural_valid = input.validate().is_ok(); // Short-circuit before catalog identity construction so an unknown glyph // yields InternalError rather than panicking in the metrics hash. let names: Vec<&str> = input .glyphs .iter() .map(|glyph| glyph.glyph.as_str()) .collect(); let metrics_available = all_available(names.iter().copied()); let catalog_valid = metrics_available && input.catalog == BravuraCatalog.identity(&names); let well_formed = structural_valid && catalog_valid; // This interface-only solver can preserve already-resolved geometry but // does not evaluate explicit constraints. It must not claim those are // satisfied merely because the input is structurally well formed. let unevaluated = input.constraints.len(); let status = if !well_formed { SolveStatus::InternalError } else if unevaluated > 0 { SolveStatus::SolvedWithWarnings } else { SolveStatus::Solved }; let warnings = if well_formed && unevaluated > 0 { vec![SolverWarning { kind: SolverWarningKind::UnusualLayoutDecision(format!( "the interface-only stub solver evaluated none of the {unevaluated} \ declared constraint(s); satisfaction is not claimed" )), affected_objects: Vec::new(), message: "declared constraints were not evaluated".to_owned(), }] } else { Vec::new() }; let glyphs: Vec = if structural_valid { input .glyphs .iter() .map(|g| ResolvedGlyph { provenance: g.provenance.clone(), glyph: g.glyph.clone(), position: g.baseline, transform: None, bounding_box: g.bounding_box, style: g.style, layer: g.layer, }) .collect() } else { Vec::new() }; // Strokes and curves pass through verbatim (the stub resolves no // geometry), gated on the same structural validity as the glyphs. let strokes = if structural_valid { input.strokes.clone() } else { Vec::new() }; let curves = if structural_valid { input.curves.clone() } else { Vec::new() }; let resolved_glyphs = glyphs.len(); // The stub resolves no per-system geometry (every region becomes one // degenerate default-rect system, per below), so it has no honest // per-system attribution to publish: every primitive is unowned // rather than a fabricated claim (W1 pin 3). let unowned = PrimitiveIndices { glyphs: (0..glyphs.len() as u32).collect(), strokes: (0..strokes.len() as u32).collect(), curves: (0..curves.len() as u32).collect(), }; let pages = input .regions .first() .map(|first| ResolvedPage { provenance: first.provenance.clone(), number: 1, size: Size2D::default(), margins: Margins::default(), systems: input .regions .iter() .map(|region| ResolvedSystem { provenance: region.provenance.clone(), bounding_box: Rect::default(), staves: Vec::new(), measures: Vec::new(), primitives: PrimitiveIndices::default(), }) .collect(), free_objects: Vec::new(), }) .into_iter() .collect(); SolveReport { status, // Honest in both directions: false when the input is malformed *and* // when constraints were declared but not evaluated. satisfied_hard_constraints: well_formed && unevaluated == 0, layout: ResolvedLayoutIR { source: input.source, pages, glyphs, strokes, curves, engraving_decisions: input.engraving_decisions.clone(), catalog: input.catalog.clone(), unowned, }, unsatisfied_constraints: Vec::new(), warnings, metric_vector: QualityMetricVector::unmeasured(), // The stub does no iterative work; its deterministic budget use is zero. budget_used: SolverBudgetUsed::default(), state: SolverState { solver_version: Some(self.version()), resolved_glyphs, }, } } } impl ConstraintSolver for StubSolver { fn tier(&self) -> SolverTier { // Honest: a passthrough that evaluates no constraints is below Minimal. SolverTier::Stub } fn version(&self) -> SolverVersion { SolverVersion(0) } fn solve(&self, input: &ConstrainedLayoutIR, _config: &SolverConfig) -> SolveReport { self.resolve(input) } fn solve_incremental( &self, input: &ConstrainedLayoutIR, _prior: &SolverState, _invalidations: &InvalidationSet, _config: &SolverConfig, ) -> SolveReport { // The stub resolves geometry verbatim, so a full re-solve is trivially // observationally equivalent to any scoped incremental solve // (Chapter 9 §"Observational Equivalence"). self.resolve(input) } } #[cfg(test)] mod tests { use super::*; use crate::constrained::GlyphObject; use crate::glyph::GlyphCatalogIdentity; use crate::provenance::{LayoutObjectId, Provenance}; use crate::spatial::Point; use crate::vertical_band::{VerticalBand, VerticalBandId}; use epiphany_core::{EventId, TypedObjectId, WallClockTime}; fn glyph(name: &'static str) -> GlyphObject { let source = TypedObjectId::Event(EventId::from_raw(1)); GlyphObject { provenance: Provenance::projected(source, vec![]), glyph: crate::GlyphReference::borrowed(name), horizontal_slot: SpringSlotId(0), baseline: Point::new(1.0, 0.0), vertical_band: VerticalBandId(0), bounding_box: crate::BoundingBox::default(), anchor: Point::ORIGIN, layer: 0, style: crate::GlyphStyle { rgba: 0x0000_00ff }, } } fn constrained(mut glyphs: Vec) -> ConstrainedLayoutIR { let band = VerticalBand::margin( LayoutObjectId(0), glyphs.iter().map(GlyphObject::id).collect(), ); for glyph in &mut glyphs { glyph.vertical_band = band.id; } let names: Vec<&str> = glyphs.iter().map(|glyph| glyph.glyph.as_str()).collect(); let catalog = BravuraCatalog.identity(&names); ConstrainedLayoutIR { source: crate::ScoreVersion::default(), regions: vec![], horizontal_slots: vec![crate::SpringSlot { id: SpringSlotId(0), time: crate::TimePoint::WallClock(WallClockTime(0)), min_width: crate::StaffSpace(1.0), preferred_width: crate::StaffSpace(1.0), max_width: None, stretch_factor: 1.0, compress_factor: 1.0, members: glyphs.iter().map(GlyphObject::id).collect(), }], glyphs, strokes: vec![], curves: vec![], vertical_bands: vec![band], constraints: vec![], break_origins: vec![], engraving_decisions: vec![], diagnostics: vec![], catalog, } } #[test] fn stub_reports_the_non_conformant_stub_tier_and_worst_metrics() { // Honest non-conformance: a passthrough reports Stub, never Minimal, and // Stub orders below every real conformance tier. assert_eq!(StubSolver.tier(), SolverTier::Stub); assert!(SolverTier::Stub < SolverTier::Minimal); assert_eq!(StubSolver.version(), SolverVersion(0)); let input = constrained(vec![glyph("noteheadBlack")]); assert_eq!( StubSolver .solve(&input, &SolverConfig::default()) .metric_vector, QualityMetricVector::unmeasured() ); } #[test] fn validate_rejects_dangling_constraint_references() { use crate::constrained::{ BreakKind, ConstrainedValidationError, GlyphObjectId, LayoutConstraint, }; let mut input = constrained(vec![glyph("noteheadBlack")]); assert!(input.validate().is_ok()); let real = input.glyphs[0].id(); // A constraint naming a glyph that is not in the set is rejected, not // silently accepted. let ghost = GlyphObjectId(real.0 ^ 0xABCD); input .constraints .push(LayoutConstraint::NoCollision { a: real, b: ghost }); assert_eq!( input.validate(), Err(ConstrainedValidationError::UnknownConstraintGlyph(ghost)) ); // A break constraint on a non-existent slot is rejected. input.constraints = vec![LayoutConstraint::SystemBreakAt { slot: SpringSlotId(999), kind: BreakKind::Hard, }]; assert_eq!( input.validate(), Err(ConstrainedValidationError::UnknownConstraintSlot( SpringSlotId(999) )) ); // A well-formed constraint reference validates — and the stub solver // still does not *evaluate* it: the solve stays renderable, but it does // not claim the constraint satisfied. input.constraints = vec![LayoutConstraint::NoCollision { a: real, b: real }]; assert!(input.validate().is_ok()); let report = StubSolver.solve(&input, &SolverConfig::default()); assert_eq!(report.status, SolveStatus::SolvedWithWarnings); assert!(!report.satisfied_hard_constraints); } #[test] fn unknown_glyph_yields_internal_error_not_panic() { let mut unknown = glyph("noSuchGlyph"); let band = VerticalBand::margin(LayoutObjectId(0), vec![unknown.id()]); unknown.vertical_band = band.id; let input = ConstrainedLayoutIR { source: crate::ScoreVersion::default(), regions: vec![], horizontal_slots: vec![crate::SpringSlot { id: SpringSlotId(0), time: crate::TimePoint::WallClock(WallClockTime(0)), min_width: crate::StaffSpace(1.0), preferred_width: crate::StaffSpace(1.0), max_width: None, stretch_factor: 1.0, compress_factor: 1.0, members: vec![unknown.id()], }], glyphs: vec![unknown], strokes: vec![], curves: vec![], vertical_bands: vec![band], constraints: vec![], break_origins: vec![], engraving_decisions: vec![], diagnostics: vec![], catalog: GlyphCatalogIdentity::default(), }; let report = StubSolver.solve(&input, &SolverConfig::default()); assert_eq!(report.status, SolveStatus::InternalError); assert!(!report.satisfied_hard_constraints); } #[test] fn well_formed_input_solves_verbatim() { let input = constrained(vec![glyph("noteheadBlack")]); let report = StubSolver.solve(&input, &SolverConfig::default()); assert_eq!(report.status, SolveStatus::Solved); assert!(report.satisfied_hard_constraints); assert_eq!(report.layout.glyphs[0].position, input.glyphs[0].baseline); assert_eq!(report.state.resolved_glyphs, 1); assert!(report.unsatisfied_constraints.is_empty()); assert!(report.warnings.is_empty()); } #[test] fn the_stub_solver_publishes_every_primitive_unowned() { // (m2) The stub resolves no per-system geometry (every region becomes // one degenerate default-rect `ResolvedSystem`, `resolve` above), so // it must not coerce an unattributed primitive onto system 0 — real, // non-trivial counts, via the real `to_logical`/`to_constrained` // pipeline over a rich generated score (RS-2's own construction). use crate::{to_constrained, to_logical}; use epiphany_core::generators::valid_score_rich; let score = valid_score_rich(0xF302); let mut input = to_constrained(&to_logical(&score)); assert_eq!( input.regions.len(), 3, "the rich fixture's real region count" ); // `valid_score_rich` carries no slur, so hand-add a curve (mirrors // `strokes_survive_the_solve_and_enter_the_canonical_bytes` below) to // exercise all three flat arrays, not just glyphs and strokes. input.curves.push(crate::Curve { provenance: input.glyphs[0].provenance.clone(), p0: crate::Point::new(0.0, 0.0), p1: crate::Point::new(1.0, 1.0), p2: crate::Point::new(2.0, 1.0), p3: crate::Point::new(3.0, 0.0), thickness: crate::StaffSpace(0.1), layer: 0, style: crate::GlyphStyle::default(), line: epiphany_core::LineStyle::Solid, vertical_band: input.glyphs[0].vertical_band, }); let report = StubSolver.solve(&input, &SolverConfig::default()); let layout = &report.layout; // Real counts (not `> 0`): the rich fixture's own glyph/stroke tally. assert_eq!(layout.glyphs.len(), input.glyphs.len()); assert_eq!(layout.strokes.len(), input.strokes.len()); assert_eq!( layout.glyphs.len(), 11, "the rich fixture's real glyph count" ); assert_eq!( layout.strokes.len(), 38, "the rich fixture's real stroke count" ); assert_eq!(layout.curves.len(), 1); // Every system's own bucket is empty — the stub attributes nothing. let system_count = layout.systems().count(); assert_eq!(system_count, input.regions.len()); for system in layout.systems() { assert!(system.primitives.glyphs.is_empty()); assert!(system.primitives.strokes.is_empty()); assert!(system.primitives.curves.is_empty()); } // Everything unowned: the exact index range, in order. assert_eq!( layout.unowned.glyphs, (0..layout.glyphs.len() as u32).collect::>() ); assert_eq!( layout.unowned.strokes, (0..layout.strokes.len() as u32).collect::>() ); assert_eq!(layout.unowned.curves, vec![0]); } #[test] fn strokes_survive_the_solve_and_enter_the_canonical_bytes() { let mut input = constrained(vec![glyph("noteheadBlack")]); let baseline = StubSolver .solve(&input, &SolverConfig::default()) .layout .canonical_bytes(); input.strokes.push(crate::Stroke { provenance: input.glyphs[0].provenance.clone(), vertical_band: input.glyphs[0].vertical_band, from: crate::Point::new(0.0, 0.0), to: crate::Point::new(1.5, 0.0), thickness: crate::StaffSpace(0.13), layer: 0, style: crate::GlyphStyle::default(), }); let solved = StubSolver.solve(&input, &SolverConfig::default()); assert_eq!( solved.layout.strokes.len(), 1, "the stroke survives the solve" ); assert_ne!( solved.layout.canonical_bytes(), baseline, "a stroke changes the resolved canonical bytes" ); } #[test] fn forged_catalog_metadata_is_rejected() { let mut input = constrained(vec![glyph("noteheadBlack")]); input.catalog.font_id = crate::glyph::FontId::owned("Not Bravura"); let report = StubSolver.solve(&input, &SolverConfig::default()); assert_eq!(report.status, SolveStatus::InternalError); assert!(!report.satisfied_hard_constraints); let mut input = constrained(vec![glyph("noteheadBlack")]); input.catalog.smufl_version.minor_centi += 1; assert_eq!( StubSolver.solve(&input, &SolverConfig::default()).status, SolveStatus::InternalError ); let mut input = constrained(vec![glyph("noteheadBlack")]); input.catalog.font_version = None; assert_eq!( StubSolver.solve(&input, &SolverConfig::default()).status, SolveStatus::InternalError ); } #[test] fn dangling_band_and_non_finite_geometry_are_rejected() { let mut dangling = constrained(vec![glyph("noteheadBlack")]); dangling.vertical_bands.clear(); assert_eq!( StubSolver.solve(&dangling, &SolverConfig::default()).status, SolveStatus::InternalError ); let mut non_finite = constrained(vec![glyph("noteheadBlack")]); non_finite.glyphs[0].baseline = Point::new(f32::NAN, 0.0); let report = StubSolver.solve(&non_finite, &SolverConfig::default()); assert_eq!(report.status, SolveStatus::InternalError); assert!(report.layout.glyphs.is_empty()); } #[test] fn explicit_constraints_are_not_falsely_reported_satisfied() { let mut input = constrained(vec![glyph("noteheadBlack")]); let glyph = input.glyphs[0].id(); input .constraints .push(crate::LayoutConstraint::NoCollision { a: glyph, b: glyph }); let report = StubSolver.solve(&input, &SolverConfig::default()); // Unevaluated constraints are not a defect in the input, so the solve is // renderable — but satisfaction is not claimed, and a warning names the gap. assert_eq!(report.status, SolveStatus::SolvedWithWarnings); assert!(report.status.is_renderable()); assert!(!report.satisfied_hard_constraints); assert_eq!(report.warnings.len(), 1); assert!(report.unsatisfied_constraints.is_empty()); // The geometry still passes through verbatim. assert_eq!(report.layout.glyphs[0].position, input.glyphs[0].baseline); } #[test] fn incremental_is_observationally_equivalent_to_full() { let input = constrained(vec![glyph("noteheadBlack"), glyph("gClef")]); let full = StubSolver.solve(&input, &SolverConfig::default()); let inc = StubSolver.solve_incremental( &input, &full.state, &InvalidationSet { scope: InvalidationScope::WholeScore, slots: vec![], bands: vec![], constraints: vec![], glyphs: vec![], }, &SolverConfig::default(), ); assert_eq!(full.layout, inc.layout); } #[test] fn normalized_metric_accepts_its_range() { assert_eq!(NormalizedMetric::new(0.0), NormalizedMetric(0.0)); assert_eq!(NormalizedMetric::new(1.0), NormalizedMetric(1.0)); } #[test] #[should_panic(expected = "[0.0, 1.0]")] fn normalized_metric_rejects_out_of_range() { let _ = NormalizedMetric::new(1.5); } }