epiphany/crates/epiphany-testkit/src/layout_stub.rs

1609 lines
57 KiB
Rust

//! The layout round-trip harness (v0 acceptance criterion 6 — Chapter 7's IR
//! contract):
//!
//! > A score graph → LogicalLayoutIR → stub-solved ResolvedLayoutIR → RenderIR
//! > interface call completes without panic and without losing provenance
//! > back-references.
//!
//! **Agent E (`epiphany-layout-ir`) has landed.** This module was formerly a
//! faithful in-tree *stub* of the layout IR; per the QUICKSTART re-point note it
//! now drives the **real** crate, re-exporting its IR types behind the same
//! [`round_trip`] signature the acceptance suite already calls. The
//! provenance-preservation contract the harness asserts is implemented and
//! tested inside `epiphany-layout-ir` itself; the testkit keeps deterministic
//! generators for E's public types (Agent F's "generators for every public type
//! in A through E" mandate) and exercises the real `round_trip` on the testkit's
//! hand-off fixtures.
//!
//! The module name is retained so the harness entry point stays
//! `layout_stub::round_trip`.
// Re-export the real layout IR: the four stages, `TimeAxisModel`, the provenance
// back-references, the glyph-catalog identity, the engraving-decision and
// vertical-band models, the edit-barrier types, and the stub solver.
pub use epiphany_layout_ir::*;
use crate::rng::Rng;
// --- Deterministic generators for Agent E's public types (Agent F mandate) ---
/// A generated layout-object identifier value.
pub fn gen_layout_object_id(rng: &mut Rng) -> LayoutObjectId {
LayoutObjectId(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128)
}
/// A synthesis kind (every variant, including the registered form).
pub fn gen_synthesis_kind(rng: &mut Rng) -> SynthesisKind {
match rng.below(7) {
0 => SynthesisKind::CancellationAccidental,
1 => SynthesisKind::KeySignatureNatural,
2 => SynthesisKind::GeneratedRest,
3 => SynthesisKind::EngravedBreak,
4 => SynthesisKind::MultimeasureRest,
5 => SynthesisKind::Cautionary,
_ => SynthesisKind::Registered(SynthesisRegistryId(
((rng.next_u64() as u128) << 64) | rng.next_u64() as u128,
)),
}
}
/// A time-axis model (every variant of the tagged enum).
pub fn gen_time_axis_model(rng: &mut Rng) -> TimeAxisModel {
match rng.below(4) {
0 => TimeAxisModel::Metric(MetricTimeAxis::default()),
1 => TimeAxisModel::Proportional(ProportionalTimeAxis {
duration_ns: rng.range(0, 1 << 40) as i64,
space_per_second: gen_staff_space(rng),
placements: vec![],
}),
2 => TimeAxisModel::Aleatoric(AleatoricTimeAxis::default()),
_ => TimeAxisModel::Registered(
TimeAxisRegistryId(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128),
SerializedRegisteredAxis(vec![rng.next_u64() as u8]),
),
}
}
/// A provenance record with a random source and a few dependency
/// back-references, **internally consistent**: a synthesized provenance carries
/// `Some(kind)` *and* the matching `synthesized_layout_id`; a projected one
/// carries `None` *and* `stable_layout_id`. (It never sets a synthesis kind while
/// keeping the plain source-only id.)
pub fn gen_provenance(rng: &mut Rng) -> Provenance {
let source = crate::generators::typed_object_id(rng);
let mut dependencies = Vec::new();
for _ in 0..rng.range_usize(0, 3) {
dependencies.push(crate::generators::typed_object_id(rng));
}
if rng.boolean() {
let kind = gen_synthesis_kind(rng);
Provenance::synthesized(
source,
kind,
SynthesisInstanceKey(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128),
dependencies,
)
} else {
Provenance::projected(source, dependencies)
}
}
/// A bundled SMuFL glyph name.
fn gen_glyph_name(rng: &mut Rng) -> &'static str {
BRAVURA_METRICS[rng.below(BRAVURA_METRICS.len() as u64) as usize]
.name
.as_ref()
}
/// A solver status (every variant).
pub fn gen_solve_status(rng: &mut Rng) -> SolveStatus {
*rng.choose(&[
SolveStatus::Solved,
SolveStatus::SolvedWithWarnings,
SolveStatus::PartialBudgetExhausted,
SolveStatus::Unsatisfiable,
SolveStatus::InternalError,
])
}
/// A glyph-catalog identity (the bundled Bravura identity).
pub fn gen_glyph_catalog_identity(_rng: &mut Rng) -> GlyphCatalogIdentity {
GlyphCatalogIdentity::default()
}
/// A 2-D staff-space point.
pub fn gen_point(rng: &mut Rng) -> Point {
// Spread points over a few hundred staff spaces at sub-grid resolution.
let coord = |r: &mut Rng| (r.range(0, 400_000) as f32) / 1000.0;
Point::new(coord(rng), coord(rng))
}
/// A logical engraving-content payload — every [`LayoutContent`] variant, so the
/// generator/fuzz surface exercises the enriched payloads, not just structural.
pub fn gen_layout_content(rng: &mut Rng) -> LayoutContent {
fn time(rng: &mut Rng) -> TimePoint {
if rng.boolean() {
TimePoint::Musical(epiphany_core::MusicalPosition(
epiphany_core::RationalTime::new(rng.range(0, 16) as i64, 4).expect("nonzero"),
))
} else {
TimePoint::WallClock(epiphany_core::WallClockTime(rng.range(0, 10_000) as i64))
}
}
fn duration(numerator: i64, denominator: i64) -> epiphany_core::MusicalDuration {
epiphany_core::MusicalDuration(
epiphany_core::RationalTime::new(numerator, denominator).expect("nonzero"),
)
}
fn component(rng: &mut Rng, offset: epiphany_core::MusicalDuration) -> PlacedComponent {
PlacedComponent {
offset,
component: epiphany_core::NotatedComponent {
base_value: *rng.choose(&[
epiphany_core::NoteValue::Whole,
epiphany_core::NoteValue::Half,
epiphany_core::NoteValue::Quarter,
epiphany_core::NoteValue::Eighth,
epiphany_core::NoteValue::Sixteenth,
]),
dots: rng.range(0, 4) as u8,
tuplet: None,
tied_to_next: rng.boolean(),
},
tuplet: None,
}
}
fn components(rng: &mut Rng) -> Vec<PlacedComponent> {
let mut out = vec![component(rng, epiphany_core::MusicalDuration::zero())];
if rng.boolean() {
out.push(component(rng, duration(1, 4)));
}
out
}
fn clefs(rng: &mut Rng) -> Vec<PlacedClef> {
if rng.boolean() {
vec![
PlacedClef {
time: time(rng),
clef: epiphany_core::Clef::treble(),
},
PlacedClef {
time: time(rng),
clef: epiphany_core::Clef::bass(),
},
]
} else {
Vec::new()
}
}
fn keys(rng: &mut Rng) -> Vec<PlacedKeySignature> {
if rng.boolean() {
vec![PlacedKeySignature {
time: time(rng),
key: epiphany_core::KeySignature::new(rng.range(0, 14) as i8 - 7)
.expect("generator stays in -7..=7"),
}]
} else {
Vec::new()
}
}
fn spelling(rng: &mut Rng) -> Option<epiphany_core::PitchSpelling> {
rng.boolean().then(|| {
let nominal = *rng.choose(&[
epiphany_core::CmnNominal::C,
epiphany_core::CmnNominal::D,
epiphany_core::CmnNominal::E,
epiphany_core::CmnNominal::F,
epiphany_core::CmnNominal::G,
epiphany_core::CmnNominal::A,
epiphany_core::CmnNominal::B,
]);
epiphany_core::PitchSpelling::cmn(nominal, rng.range(2, 7) as i8)
})
}
fn barline(rng: &mut Rng) -> BarlineKind {
match rng.below(3) {
0 => BarlineKind::Interior,
1 => BarlineKind::RegionEnd,
_ => BarlineKind::Final,
}
}
fn placement(rng: &mut Rng) -> RepeatPlacement {
match rng.below(3) {
0 => RepeatPlacement::At(time(rng)),
1 => RepeatPlacement::RegionEnd,
_ => RepeatPlacement::Unresolved,
}
}
match rng.below(6) {
0 => LayoutContent::Structural,
1 => LayoutContent::Staff(StaffContent {
default_clef: epiphany_core::Clef::default(),
clefs: clefs(rng),
keys: keys(rng),
}),
2 => LayoutContent::Note(NoteContent {
position: time(rng),
components: components(rng),
pitches: vec![NotePitch {
pitch: epiphany_core::PitchId::new(epiphany_core::ReplicaId(7), rng.next_u64()),
spelling: spelling(rng),
}],
}),
3 => LayoutContent::Rest(RestContent {
position: time(rng),
components: components(rng),
staff_position: rng
.boolean()
.then(|| epiphany_core::StaffPosition(rng.range(0, 9) as i16)),
}),
4 => LayoutContent::Measure(MeasureContent {
start: time(rng),
barline: barline(rng),
time_signature: rng.boolean().then(|| TimeSignatureContent {
numerator: rng.range(1, 13) as u16,
denominator: *rng.choose(&[2, 4, 8, 16]),
}),
}),
_ => LayoutContent::Repeat(RepeatContent {
barlines: rng.boolean(),
start: placement(rng),
end: placement(rng),
voltas: (0..rng.below(3))
.map(|index| VoltaContent {
endings: vec![index as u32 + 1],
start: placement(rng),
end: placement(rng),
})
.collect(),
}),
}
}
/// A logical layout object (provenance + an optional owning staff + content).
pub fn gen_layout_object(rng: &mut Rng) -> LayoutObject {
LayoutObject::from_projection_with_content(
gen_provenance(rng),
rng.boolean().then(|| crate::generators::staff_id(rng)),
gen_layout_content(rng),
)
}
/// A constrained glyph object (provenance + glyph + baseline anchor + band).
pub fn gen_glyph_object(rng: &mut Rng) -> GlyphObject {
let glyph_name = gen_glyph_name(rng);
GlyphObject {
provenance: gen_provenance(rng),
glyph: GlyphReference::borrowed(glyph_name),
horizontal_slot: gen_spring_slot_id(rng),
baseline: gen_point(rng),
vertical_band: gen_vertical_band_id(rng),
bounding_box: metrics(glyph_name)
.expect("generator chooses a bundled glyph")
.bounding_box(),
anchor: gen_point(rng),
layer: rng.range(0, 8) as i32,
style: GlyphStyle {
rgba: rng.next_u64() as u32,
},
}
}
/// A resolved glyph (provenance + glyph + definitive position).
pub fn gen_resolved_glyph(rng: &mut Rng) -> ResolvedGlyph {
ResolvedGlyph {
provenance: gen_provenance(rng),
glyph: GlyphReference::borrowed(gen_glyph_name(rng)),
position: gen_point(rng),
transform: None,
bounding_box: gen_glyph_bounding_box(rng),
style: GlyphStyle {
rgba: rng.next_u64() as u32,
},
layer: rng.range(0, 8) as i32,
}
}
/// A logical layout region (provenance + time axis + a few objects).
pub fn gen_layout_region(rng: &mut Rng) -> LayoutRegion {
let n = rng.range_usize(0, 4);
let region = crate::generators::region_id(rng);
let objects: Vec<LayoutObject> = (0..n).map(|_| gen_layout_object(rng)).collect();
let staves = objects.iter().filter_map(LayoutObject::staff).collect();
LayoutRegion {
provenance: Provenance::projected(epiphany_core::TypedObjectId::Region(region), vec![]),
coordinate_system: LocalCoordinateSystem::default(),
time_axis: gen_time_axis_model(rng),
vertical_extent: VerticalExtent { staves },
objects,
}
}
/// A logical layout IR (a few regions and engraving decisions).
pub fn gen_logical_layout_ir(rng: &mut Rng) -> LogicalLayoutIR {
let n = rng.range_usize(0, 3);
let d = rng.range_usize(0, 3);
LogicalLayoutIR {
source: ScoreVersion::default(),
regions: (0..n).map(|_| gen_layout_region(rng)).collect(),
engraving_decisions: (0..d).map(|_| gen_engraving_decision(rng)).collect(),
overrides: vec![],
cross_region: vec![],
}
}
/// A non-glyph line primitive (a staff line / stem / barline), so the
/// generator/fuzz surface exercises strokes alongside glyphs.
pub fn gen_stroke(rng: &mut Rng, band: VerticalBandId) -> Stroke {
Stroke {
provenance: gen_provenance(rng),
from: gen_point(rng),
to: gen_point(rng),
thickness: gen_staff_space(rng),
layer: rng.range(0, 8) as i32 - 4,
style: GlyphStyle {
rgba: rng.next_u64() as u32,
},
// A stroke names a band that exists; validation rejects a dangling one.
vertical_band: band,
}
}
/// A cubic-bézier curve primitive with generated provenance and geometry.
pub fn gen_curve(rng: &mut Rng, band: VerticalBandId) -> Curve {
Curve {
provenance: gen_provenance(rng),
p0: gen_point(rng),
p1: gen_point(rng),
p2: gen_point(rng),
p3: gen_point(rng),
thickness: gen_staff_space(rng),
layer: rng.range(0, 8) as i32 - 4,
style: GlyphStyle {
rgba: rng.next_u64() as u32,
},
line: *rng.choose(&[
epiphany_core::LineStyle::Solid,
epiphany_core::LineStyle::Dashed,
epiphany_core::LineStyle::Dotted,
]),
// A curve names a band that exists; validation rejects a dangling one.
vertical_band: band,
}
}
/// A constrained layout IR whose catalog hash covers exactly its glyph metrics
/// (so the real stub solver accepts it as well-formed), with an **internally
/// consistent** vertical band: every glyph names the band, and the band's
/// members are exactly those glyphs.
pub fn gen_constrained_layout_ir(rng: &mut Rng) -> ConstrainedLayoutIR {
let n = rng.range_usize(0, 8);
let mut glyphs: Vec<GlyphObject> = (0..n).map(|_| gen_glyph_object(rng)).collect();
// One band owning exactly these glyphs; point every glyph at it.
let band = VerticalBand::staff(
crate::generators::staff_id(rng),
glyphs.iter().map(|g| g.id()).collect(),
);
for g in &mut glyphs {
g.vertical_band = band.id;
}
let horizontal_slots: Vec<SpringSlot> = glyphs
.iter_mut()
.enumerate()
.map(|(index, glyph)| {
let id = SpringSlotId(glyph.id().0);
glyph.horizontal_slot = id;
SpringSlot {
id,
time: TimePoint::WallClock(epiphany_core::WallClockTime(index as i64)),
min_width: StaffSpace(1.0),
preferred_width: StaffSpace(1.5),
max_width: None,
stretch_factor: 1.0,
compress_factor: 1.0,
members: vec![glyph.id()],
}
})
.collect();
let metrics_hash = metrics_hash_for(glyphs.iter().map(|g| g.glyph.as_str()));
let decisions = rng.range_usize(0, 3);
ConstrainedLayoutIR {
source: ScoreVersion::default(),
regions: vec![],
horizontal_slots,
glyphs,
strokes: (0..rng.range_usize(0, 3))
.map(|_| gen_stroke(rng, band.id))
.collect(),
curves: (0..rng.range_usize(0, 3))
.map(|_| gen_curve(rng, band.id))
.collect(),
vertical_bands: vec![band],
constraints: vec![],
break_origins: vec![],
engraving_decisions: (0..decisions)
.map(|_| gen_engraving_decision(rng))
.collect(),
diagnostics: Vec::new(),
catalog: GlyphCatalogIdentity {
metrics_hash,
..GlyphCatalogIdentity::default()
},
}
}
/// A resolved layout IR produced by the real stub-solver interface.
pub fn gen_resolved_layout_ir(rng: &mut Rng) -> ResolvedLayoutIR {
StubSolver
.solve(&gen_constrained_layout_ir(rng), &SolverConfig::default())
.layout
}
pub fn gen_resolved_staff(rng: &mut Rng) -> ResolvedStaff {
let staff = crate::generators::staff_id(rng);
ResolvedStaff {
provenance: Provenance::projected(epiphany_core::TypedObjectId::Staff(staff), vec![]),
staff,
bounding_box: gen_rect(rng),
}
}
pub fn gen_resolved_measure(rng: &mut Rng) -> ResolvedMeasure {
let measure = crate::generators::measure_id(rng);
ResolvedMeasure {
provenance: Provenance::projected(epiphany_core::TypedObjectId::Measure(measure), vec![]),
measure,
bounding_box: gen_rect(rng),
}
}
pub fn gen_resolved_system(rng: &mut Rng) -> ResolvedSystem {
ResolvedSystem {
provenance: gen_provenance(rng),
bounding_box: gen_rect(rng),
staves: (0..rng.range_usize(0, 2))
.map(|_| gen_resolved_staff(rng))
.collect(),
measures: (0..rng.range_usize(0, 2))
.map(|_| gen_resolved_measure(rng))
.collect(),
}
}
pub fn gen_resolved_page(rng: &mut Rng) -> ResolvedPage {
ResolvedPage {
provenance: gen_provenance(rng),
number: rng.range(1, 500) as u32,
size: gen_size2d(rng),
margins: gen_margins(rng),
systems: (0..rng.range_usize(0, 2))
.map(|_| gen_resolved_system(rng))
.collect(),
free_objects: (0..rng.range_usize(0, 2))
.map(|_| gen_glyph_object_id(rng))
.collect(),
}
}
/// A render primitive with generated provenance, glyph, and geometry.
pub fn gen_render_primitive(rng: &mut Rng) -> RenderPrimitive {
RenderPrimitive {
provenance: gen_provenance(rng),
glyph: GlyphReference::borrowed(gen_glyph_name(rng)),
position: gen_point(rng),
transform: None,
bounding_box: gen_glyph_bounding_box(rng),
style: GlyphStyle {
rgba: rng.next_u64() as u32,
},
layer: rng.range(0, 8) as i32,
}
}
/// A RenderIR generated through the resolved-to-render projection.
pub fn gen_render_ir(rng: &mut Rng) -> RenderIR {
to_render(&gen_resolved_layout_ir(rng))
}
/// A complete solver report generated through the solver interface.
pub fn gen_solve_report(rng: &mut Rng) -> SolveReport {
StubSolver.solve(&gen_constrained_layout_ir(rng), &SolverConfig::default())
}
/// A self-consistent round-trip report value.
pub fn gen_round_trip_report(rng: &mut Rng) -> RoundTripReport {
let render = gen_render_ir(rng);
let recovered_sources = render
.primitives
.iter()
.map(|primitive| primitive.provenance.source)
.chain(render.strokes.iter().map(|stroke| stroke.provenance.source))
.chain(render.curves.iter().map(|curve| curve.provenance.source))
.collect();
let total = render.primitives.len() + render.strokes.len() + render.curves.len();
RoundTripReport {
status: SolveStatus::Solved,
logical_objects: total,
glyphs: render.primitives.len(),
render_strokes: render.strokes.len(),
render_curves: render.curves.len(),
render_primitives: total,
recovered_sources,
}
}
// --- Generators for the rest of E's public surface (Agent F mandate) ---
/// A staff-space measurement.
pub fn gen_staff_space(rng: &mut Rng) -> StaffSpace {
StaffSpace((rng.range(0, 400_000) as f32) / 1000.0)
}
/// A 2-D staff-space size.
pub fn gen_size2d(rng: &mut Rng) -> Size2D {
Size2D {
width: gen_staff_space(rng),
height: gen_staff_space(rng),
}
}
/// A staff-space bounding box.
pub fn gen_bounding_box(rng: &mut Rng) -> BoundingBox {
let c = |r: &mut Rng| (r.range(0, 8000) as f32) / 1000.0;
BoundingBox::new(c(rng), c(rng), c(rng), c(rng))
}
pub fn gen_rect(rng: &mut Rng) -> Rect {
Rect {
origin: gen_point(rng),
size: gen_size2d(rng),
}
}
pub fn gen_transform_2d(rng: &mut Rng) -> Transform2D {
let mut transform = Transform2D::default();
transform.matrix[0][2] = gen_staff_space(rng).0;
transform.matrix[1][2] = gen_staff_space(rng).0;
transform
}
pub fn gen_margins(rng: &mut Rng) -> Margins {
Margins {
top: gen_staff_space(rng),
right: gen_staff_space(rng),
bottom: gen_staff_space(rng),
left: gen_staff_space(rng),
}
}
pub fn gen_score_version(rng: &mut Rng) -> ScoreVersion {
let mut bytes = [0; 32];
for chunk in bytes.chunks_exact_mut(8) {
chunk.copy_from_slice(&rng.next_u64().to_le_bytes());
}
ScoreVersion(bytes)
}
pub fn gen_local_coordinate_system(rng: &mut Rng) -> LocalCoordinateSystem {
LocalCoordinateSystem {
transform: gen_transform_2d(rng),
}
}
pub fn gen_vertical_extent(rng: &mut Rng) -> VerticalExtent {
VerticalExtent {
staves: (0..rng.range_usize(0, 3))
.map(|_| crate::generators::staff_id(rng))
.collect(),
}
}
pub fn gen_cross_region_object(rng: &mut Rng) -> CrossRegionObject {
CrossRegionObject {
provenance: gen_provenance(rng),
regions: (0..rng.range_usize(1, 3))
.map(|_| crate::generators::region_id(rng))
.collect(),
staff: rng.boolean().then(|| crate::generators::staff_id(rng)),
}
}
pub fn gen_dependency_index(rng: &mut Rng) -> DependencyIndex {
let mut index = DependencyIndex::default();
for _ in 0..rng.range_usize(0, 4) {
index.insert(&gen_provenance(rng));
}
index
}
pub fn gen_layout_cache(rng: &mut Rng) -> LayoutCache {
LayoutCache {
dependencies: gen_dependency_index(rng),
logical: Default::default(),
constrained: Default::default(),
resolved: Default::default(),
fine_cache: FineLayoutCache::default(),
}
}
pub fn gen_system_id(rng: &mut Rng) -> SystemId {
SystemId(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128)
}
pub fn gen_logical_region_cache(rng: &mut Rng) -> LogicalRegionCache {
LogicalRegionCache {
objects: (0..rng.range_usize(0, 3))
.map(|_| gen_layout_object_id(rng))
.collect(),
}
}
pub fn gen_constrained_region_cache(rng: &mut Rng) -> ConstrainedRegionCache {
ConstrainedRegionCache {
objects: (0..rng.range_usize(0, 3))
.map(|_| gen_layout_object_id(rng))
.collect(),
}
}
pub fn gen_resolved_system_cache(rng: &mut Rng) -> ResolvedSystemCache {
ResolvedSystemCache {
objects: (0..rng.range_usize(0, 3))
.map(|_| gen_layout_object_id(rng))
.collect(),
}
}
pub fn gen_fine_layout_cache(rng: &mut Rng) -> FineLayoutCache {
FineLayoutCache {
objects: (0..rng.range_usize(0, 3))
.map(|_| gen_layout_object_id(rng))
.collect(),
}
}
/// A glyph-object identifier value.
pub fn gen_glyph_object_id(rng: &mut Rng) -> GlyphObjectId {
GlyphObjectId(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128)
}
pub fn gen_synthesis_instance_key(rng: &mut Rng) -> SynthesisInstanceKey {
SynthesisInstanceKey(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128)
}
pub fn gen_glyph_style(rng: &mut Rng) -> GlyphStyle {
GlyphStyle {
rgba: rng.next_u64() as u32,
}
}
pub fn gen_time_point(rng: &mut Rng) -> TimePoint {
if rng.boolean() {
TimePoint::Musical(epiphany_core::MusicalPosition(
epiphany_core::RationalTime::new(rng.range(0, 16) as i64, 4)
.expect("positive denominator"),
))
} else {
TimePoint::WallClock(epiphany_core::WallClockTime(rng.next_u64() as i64))
}
}
pub fn gen_time_range(rng: &mut Rng) -> TimeRange {
if rng.boolean() {
let start = epiphany_core::MusicalPosition(
epiphany_core::RationalTime::new(rng.range(0, 8) as i64, 4)
.expect("positive denominator"),
);
TimeRange::Musical {
start: start.clone(),
end: start
+ epiphany_core::MusicalDuration(
epiphany_core::RationalTime::new(1, 4).expect("positive denominator"),
),
}
} else {
let start = epiphany_core::WallClockTime(rng.range(0, 10_000) as i64);
TimeRange::WallClock {
start,
end: epiphany_core::WallClockTime(start.0 + 1_000),
}
}
}
pub fn gen_spring_slot(rng: &mut Rng) -> SpringSlot {
SpringSlot {
id: gen_spring_slot_id(rng),
time: gen_time_point(rng),
min_width: StaffSpace(1.0),
preferred_width: StaffSpace(2.0),
max_width: rng.boolean().then_some(StaffSpace(3.0)),
stretch_factor: 1.0,
compress_factor: 1.0,
members: (0..rng.range_usize(0, 3))
.map(|_| gen_glyph_object_id(rng))
.collect(),
}
}
pub fn gen_constrained_layout_region(rng: &mut Rng) -> ConstrainedLayoutRegion {
let region = crate::generators::region_id(rng);
ConstrainedLayoutRegion {
provenance: Provenance::projected(epiphany_core::TypedObjectId::Region(region), vec![]),
glyphs: (0..rng.range_usize(0, 3))
.map(|_| gen_glyph_object_id(rng))
.collect(),
time_axis: gen_time_axis_model(rng),
}
}
pub fn gen_layout_constraint(rng: &mut Rng) -> LayoutConstraint {
match rng.below(6) {
0 => LayoutConstraint::NoCollision {
a: gen_glyph_object_id(rng),
b: gen_glyph_object_id(rng),
},
1 => LayoutConstraint::Align {
a: gen_glyph_object_id(rng),
b: gen_glyph_object_id(rng),
axis: if rng.boolean() {
Axis::Horizontal
} else {
Axis::Vertical
},
},
2 => LayoutConstraint::PositionWithin {
glyph: gen_glyph_object_id(rng),
region: gen_rect(rng),
},
3 => LayoutConstraint::SystemBreakAt {
slot: gen_spring_slot_id(rng),
kind: if rng.boolean() {
BreakKind::Hard
} else {
BreakKind::Soft
},
},
4 => LayoutConstraint::PageBreakAt {
slot: gen_spring_slot_id(rng),
kind: if rng.boolean() {
BreakKind::Hard
} else {
BreakKind::Soft
},
},
_ => LayoutConstraint::Registered(
ConstraintRegistryId(rng.next_u64() as u128),
ConstraintParameters(vec![rng.next_u64() as u8]),
),
}
}
/// A constraint strength (both variants).
pub fn gen_constraint_strength(rng: &mut Rng) -> ConstraintStrength {
if rng.boolean() {
ConstraintStrength::Required
} else {
ConstraintStrength::Preferred {
weight: (rng.range(1, 1_000) as f64) / 100.0,
}
}
}
/// A bundled glyph metric (drawn from the in-tree Bravura table).
pub fn gen_glyph_metric(rng: &mut Rng) -> GlyphMetric {
BRAVURA_METRICS[rng.below(BRAVURA_METRICS.len() as u64) as usize].clone()
}
/// A SMuFL version. `minor_centi` is fraction-normalized (P13-S12): a small
/// integer sampled directly (as the previous generator did) would denote
/// 1.00-1.05 as hundredths, versions SMuFL never released. Instead sample the
/// minor digit string from real SMuFL minor releases and normalize it through
/// [`SmuflVersion::from_decimal`], the same constructor production code uses,
/// so the generator can never emit a value the type's own invariant forbids.
pub fn gen_smufl_version(rng: &mut Rng) -> SmuflVersion {
const MINOR_RELEASES: [&str; 5] = ["12", "18", "20", "3", "4"];
let major = rng.range(1, 2) as u16;
let minor_digits = MINOR_RELEASES[rng.below(MINOR_RELEASES.len() as u64) as usize];
SmuflVersion::from_decimal(major, minor_digits).expect("a real SMuFL minor release")
}
/// A font identifier (the bundled font).
pub fn gen_font_id(rng: &mut Rng) -> FontId {
if rng.boolean() {
FontId::BRAVURA
} else {
FontId::owned(format!("runtime-font-{}", rng.next_u64()))
}
}
/// A decision source (every variant).
pub fn gen_decision_source(rng: &mut Rng) -> DecisionSource {
match rng.below(3) {
0 => DecisionSource::Automatic,
1 => DecisionSource::UserOverride(EngravingOverrideId(rng.next_u64() as u128)),
_ => DecisionSource::IrOverride,
}
}
/// An engraving-decision kind (a representative subset).
pub fn gen_engraving_decision_kind(rng: &mut Rng) -> EngravingDecisionKind {
match rng.below(5) {
0 => EngravingDecisionKind::StemDirection(if rng.boolean() {
StemDirection::Up
} else {
StemDirection::Down
}),
1 => EngravingDecisionKind::LedgerLineCount(rng.range(0, 5) as u8),
2 => EngravingDecisionKind::SystemBreak,
3 => EngravingDecisionKind::PageBreak,
_ => EngravingDecisionKind::Registered(EngravingDecisionRegistryId(rng.next_u64() as u128)),
}
}
/// An engraving-decision record with a content-derived id.
pub fn gen_engraving_decision(rng: &mut Rng) -> EngravingDecision {
EngravingDecision::with_source(
gen_layout_object_id(rng),
gen_engraving_decision_kind(rng),
gen_decision_source(rng),
)
}
pub fn gen_override_target(rng: &mut Rng) -> OverrideTarget {
if rng.boolean() {
OverrideTarget::ScoreGraph(crate::generators::typed_object_id(rng))
} else {
OverrideTarget::IrSynthesized(gen_layout_object_id(rng))
}
}
/// A break anchor for a generated break override (a representative subset: a
/// wall-clock instant or an event anchor without offset).
pub fn gen_break_anchor(rng: &mut Rng) -> epiphany_core::TimeAnchor {
if rng.boolean() {
epiphany_core::TimeAnchor::WallClock {
time: epiphany_core::WallClockTime(rng.range(0, 10_000) as i64),
}
} else {
epiphany_core::TimeAnchor::Event {
id: epiphany_core::EventId::from_raw(rng.next_u64() as u128),
offset: epiphany_core::AnchorOffset::Zero,
}
}
}
pub fn gen_override_kind(rng: &mut Rng) -> OverrideKind {
match rng.below(9) {
0 => OverrideKind::StemDirection(if rng.boolean() {
epiphany_core::StemDirection::Up
} else {
epiphany_core::StemDirection::Down
}),
1 => OverrideKind::AccidentalParenthesized(rng.boolean()),
2 => OverrideKind::AccidentalVisible(rng.boolean()),
3 => OverrideKind::SystemBreak {
anchor: gen_break_anchor(rng),
},
4 => OverrideKind::PageBreak {
anchor: gen_break_anchor(rng),
},
5 => OverrideKind::HiddenObject,
6 => OverrideKind::CustomPosition(gen_point(rng)),
7 => OverrideKind::LedgerLineSuppression,
_ => OverrideKind::Registered(rng.next_u64() as u128),
}
}
pub fn gen_engraving_override(rng: &mut Rng) -> EngravingOverride {
EngravingOverride {
id: EngravingOverrideId(rng.next_u64() as u128),
target: gen_override_target(rng),
kind: gen_override_kind(rng),
priority: if rng.boolean() {
OverridePriority::Hard
} else {
OverridePriority::Soft
},
origin: match rng.below(4) {
0 => OverrideOrigin::User {
author: AuthorId(rng.next_u64() as u128),
timestamp: Timestamp(rng.next_u64() as i64),
},
1 => OverrideOrigin::Import {
format: ForeignFormatId(rng.next_u64() as u128),
},
2 => OverrideOrigin::Plugin {
plugin: PluginId(rng.next_u64() as u128),
},
_ => OverrideOrigin::Internal,
},
}
}
/// A vertical-band kind (every variant).
pub fn gen_vertical_band_kind(rng: &mut Rng) -> VerticalBandKind {
match rng.below(4) {
0 => VerticalBandKind::Staff(crate::generators::staff_id(rng)),
1 => VerticalBandKind::InterStaffGap,
2 => VerticalBandKind::InterSystemGap,
_ => VerticalBandKind::MarginBand,
}
}
/// A vertical band (every constructor / kind exercised).
pub fn gen_vertical_band(rng: &mut Rng) -> VerticalBand {
let members: Vec<GlyphObjectId> = (0..rng.range_usize(0, 3))
.map(|_| gen_glyph_object_id(rng))
.collect();
match rng.below(4) {
0 => VerticalBand::staff(crate::generators::staff_id(rng), members),
1 => VerticalBand::margin(LayoutObjectId(rng.next_u64() as u128), members),
2 => VerticalBand::inter_staff_gap(gen_vertical_band_id(rng)),
_ => VerticalBand::inter_system_gap(gen_vertical_band_id(rng)),
}
}
/// An operation-kind tag (every variant Agent C's type provides, including the
/// registered form).
pub fn gen_operation_kind_tag(rng: &mut Rng) -> OperationKindTag {
match rng.below(30) {
0 => OperationKindTag::InsertEvent,
1 => OperationKindTag::DeleteEvent,
2 => OperationKindTag::ModifyEvent,
3 => OperationKindTag::RespellPitch,
4 => OperationKindTag::Transpose,
5 => OperationKindTag::CreateCrossCutting,
6 => OperationKindTag::DeleteCrossCutting,
7 => OperationKindTag::ModifyCrossCutting,
8 => OperationKindTag::ChangeRegionTimeModel,
9 => OperationKindTag::InsertRegion,
10 => OperationKindTag::DeleteRegion,
11 => OperationKindTag::InsertStaffInstance,
12 => OperationKindTag::DeleteStaffInstance,
13 => OperationKindTag::SetUserSystemBreak,
14 => OperationKindTag::SetUserPageBreak,
15 => OperationKindTag::DeclareTransaction,
16 => OperationKindTag::InsertIdentifiedPitch,
17 => OperationKindTag::DeleteIdentifiedPitch,
18 => OperationKindTag::ModifyIdentifiedPitch,
19 => OperationKindTag::CreateVoice,
20 => OperationKindTag::DeleteVoice,
21 => OperationKindTag::SetMetadata,
22 => OperationKindTag::SetMetricGrid,
// The appended vocabulary: the Phase-3 tranche (24..=27) — this
// generator had gone stale at 23 — and the repeat pair (28/29).
23 => OperationKindTag::InsertStaff,
24 => OperationKindTag::SetTimeSignature,
25 => OperationKindTag::SetTempoSegment,
26 => OperationKindTag::SetStaffLayout,
27 => OperationKindTag::CreateRepeatStructure,
28 => OperationKindTag::DeleteRepeatStructure,
_ => OperationKindTag::Registered(epiphany_ops::OperationKindRegistryId(
rng.next_u64() as u128
)),
}
}
/// An object kind (the kind of a generated score-graph object).
pub fn gen_object_kind(rng: &mut Rng) -> ObjectKind {
ObjectKind::of(&crate::generators::typed_object_id(rng))
}
/// A pitch-space id drawn from the built-in catalog.
fn gen_pitch_space_id(rng: &mut Rng) -> epiphany_core::PitchSpaceId {
let names = ["cmn-12", "edo-31", "ji-5limit"];
epiphany_core::PitchSpaceId::new(names[rng.below(names.len() as u64) as usize])
}
/// A barrier scope (every variant).
pub fn gen_barrier_scope(rng: &mut Rng) -> BarrierScope {
match rng.below(8) {
0 => BarrierScope::WholeScore,
1 => BarrierScope::Region(crate::generators::region_id(rng)),
2 => BarrierScope::StaffInstance(crate::generators::staff_instance_id(rng)),
3 => BarrierScope::AnalysisLayer(crate::generators::analysis_layer_id(rng)),
4 => BarrierScope::ObjectSet(
(0..rng.range_usize(0, 3))
.map(|_| crate::generators::typed_object_id(rng))
.collect(),
),
5 => BarrierScope::PitchSpace(gen_pitch_space_id(rng)),
6 => BarrierScope::TuningContext,
_ => BarrierScope::Registered(BarrierScopeRegistryId(rng.next_u64() as u128)),
}
}
/// A barrier condition (bounded recursion depth; every variant).
pub fn gen_barrier_condition(rng: &mut Rng) -> BarrierCondition {
match rng.below(7) {
0 => BarrierCondition::Always,
1 => BarrierCondition::ObjectExists(crate::generators::typed_object_id(rng)),
2 => BarrierCondition::ObjectHasExtensionData {
object: crate::generators::typed_object_id(rng),
extension: ExtensionRef(rng.next_u64() as u128),
},
3 => BarrierCondition::All(vec![BarrierCondition::Always]),
4 => BarrierCondition::Any(vec![
BarrierCondition::Always,
BarrierCondition::ObjectExists(crate::generators::typed_object_id(rng)),
]),
5 => BarrierCondition::Not(Box::new(BarrierCondition::Always)),
_ => BarrierCondition::Registered(BarrierConditionRegistryId(rng.next_u64() as u128)),
}
}
/// An edit barrier keyed on operation-kind tags.
pub fn gen_edit_barrier(rng: &mut Rng) -> EditBarrier {
EditBarrier {
scope: gen_barrier_scope(rng),
affected_object_kinds: (0..rng.range_usize(0, 3))
.map(|_| gen_object_kind(rng))
.collect(),
prohibited_operation_kinds: (0..rng.range_usize(0, 3))
.map(|_| gen_operation_kind_tag(rng))
.collect(),
condition: gen_barrier_condition(rng),
}
}
/// An edit context (an object's structural location, every field exercised).
pub fn gen_edit_context(rng: &mut Rng) -> EditContext {
EditContext {
region: rng.boolean().then(|| crate::generators::region_id(rng)),
staff_instance: rng
.boolean()
.then(|| crate::generators::staff_instance_id(rng)),
analysis_layer: rng
.boolean()
.then(|| crate::generators::analysis_layer_id(rng)),
pitch_space: rng.boolean().then(|| gen_pitch_space_id(rng)),
}
}
/// A solver tier (every variant).
pub fn gen_solver_tier(rng: &mut Rng) -> SolverTier {
*rng.choose(&[
SolverTier::Stub,
SolverTier::Minimal,
SolverTier::Standard,
SolverTier::Advanced,
])
}
/// A solver version.
pub fn gen_solver_version(rng: &mut Rng) -> SolverVersion {
SolverVersion(rng.next_u64() as u32)
}
/// A deterministic solver budget.
pub fn gen_solver_budget(rng: &mut Rng) -> SolverBudget {
SolverBudget {
max_iterations: rng.next_u64(),
max_nodes: rng.next_u64(),
max_constraint_evaluations: rng.next_u64(),
advisory_wall_time_ms: rng.boolean().then(|| rng.range(0, 1000)),
}
}
/// A consumed-budget record.
pub fn gen_solver_budget_used(rng: &mut Rng) -> SolverBudgetUsed {
SolverBudgetUsed {
iterations: rng.next_u64(),
nodes: rng.next_u64(),
constraint_evaluations: rng.next_u64(),
wall_time_ms: rng.next_u64(),
}
}
/// A solver state.
pub fn gen_solver_state(rng: &mut Rng) -> SolverState {
SolverState {
solver_version: rng.boolean().then(|| gen_solver_version(rng)),
resolved_glyphs: rng.range_usize(0, 32),
}
}
/// A solver profile (every variant).
pub fn gen_solver_profile(rng: &mut Rng) -> SolverProfile {
*rng.choose(&[
SolverProfile::Draft,
SolverProfile::Standard,
SolverProfile::Publication,
])
}
/// Tie-breaking weights.
pub fn gen_tie_breaking_weights(rng: &mut Rng) -> TieBreakingWeights {
let w = |r: &mut Rng| (r.range(0, 4000) as f64) / 1000.0;
TieBreakingWeights {
collision: w(rng),
spacing: w(rng),
slur_shape: w(rng),
beam_slope: w(rng),
vertical_density: w(rng),
system_break: w(rng),
page_fill: w(rng),
casting_off: w(rng),
symbol_density: w(rng),
}
}
/// A solver configuration (profile + budget + tie-breaking).
pub fn gen_solver_config(rng: &mut Rng) -> SolverConfig {
SolverConfig {
profile: gen_solver_profile(rng),
budget: gen_solver_budget(rng),
tie_breaking: gen_tie_breaking_weights(rng),
}
}
/// A normalized quality metric in `[0, 1]`.
pub fn gen_normalized_metric(rng: &mut Rng) -> NormalizedMetric {
NormalizedMetric::new((rng.range(0, 1000) as f64) / 1000.0)
}
/// A constraint id.
pub fn gen_constraint_id(rng: &mut Rng) -> ConstraintId {
ConstraintId(rng.next_u64() as u128)
}
/// A spring-slot id.
pub fn gen_spring_slot_id(rng: &mut Rng) -> SpringSlotId {
SpringSlotId(rng.next_u64() as u128)
}
/// A quality-metric kind (every variant).
pub fn gen_quality_metric_kind(rng: &mut Rng) -> QualityMetricKind {
*rng.choose(&[
QualityMetricKind::Collision,
QualityMetricKind::Spacing,
QualityMetricKind::SlurShape,
QualityMetricKind::BeamSlope,
QualityMetricKind::VerticalDensity,
QualityMetricKind::SystemBreak,
QualityMetricKind::PageFill,
QualityMetricKind::CastingOff,
QualityMetricKind::SymbolDensity,
])
}
/// An extension-warning id.
pub fn gen_extension_warning_id(rng: &mut Rng) -> ExtensionWarningId {
ExtensionWarningId(rng.next_u64() as u128)
}
/// A solver warning (every kind).
pub fn gen_solver_warning(rng: &mut Rng) -> SolverWarning {
let kind = match rng.below(4) {
0 => SolverWarningKind::LargeSoftConstraintViolation {
constraint: gen_constraint_id(rng),
magnitude: (rng.range(0, 10_000) as f64) / 1000.0,
},
1 => SolverWarningKind::UnusualLayoutDecision("stub".to_string()),
2 => SolverWarningKind::QualityFloorApproached {
metric: gen_quality_metric_kind(rng),
},
_ => SolverWarningKind::ExtensionWarning(gen_extension_warning_id(rng)),
};
SolverWarning {
kind,
affected_objects: (0..rng.range_usize(0, 2))
.map(|_| crate::generators::typed_object_id(rng))
.collect(),
message: "generated".to_string(),
}
}
/// An extension-metric id.
pub fn gen_extension_metric_id(rng: &mut Rng) -> ExtensionMetricId {
ExtensionMetricId(rng.next_u64() as u128)
}
/// An extension-contributed quality metric.
pub fn gen_extension_metric(rng: &mut Rng) -> ExtensionMetric {
ExtensionMetric {
metric_id: gen_extension_metric_id(rng),
value: gen_normalized_metric(rng),
}
}
/// A (non-default) quality metric vector.
pub fn gen_quality_metric_vector(rng: &mut Rng) -> QualityMetricVector {
QualityMetricVector {
collision_penalty: gen_normalized_metric(rng),
spacing_distortion: gen_normalized_metric(rng),
slur_shape_penalty: gen_normalized_metric(rng),
beam_slope_penalty: gen_normalized_metric(rng),
vertical_density_penalty: gen_normalized_metric(rng),
system_break_penalty: gen_normalized_metric(rng),
page_fill_efficiency: gen_normalized_metric(rng),
casting_off_quality: gen_normalized_metric(rng),
symbol_density_uniformity: gen_normalized_metric(rng),
extension_metrics: (0..rng.range_usize(0, 2))
.map(|_| gen_extension_metric(rng))
.collect(),
}
}
/// A render target (every variant).
pub fn gen_render_target(rng: &mut Rng) -> RenderTarget {
*rng.choose(&[
RenderTarget::Pdf,
RenderTarget::Svg,
RenderTarget::Screen,
RenderTarget::Print,
])
}
pub fn gen_color_configuration(rng: &mut Rng) -> ColorConfiguration {
ColorConfiguration {
color_space: *rng.choose(&[
ColorSpace::Srgb,
ColorSpace::DisplayP3,
ColorSpace::Cmyk,
ColorSpace::Grayscale,
]),
embed_profile: rng.boolean(),
}
}
pub fn gen_rasterization_configuration(rng: &mut Rng) -> RasterizationConfiguration {
RasterizationConfiguration {
antialias: rng.boolean(),
dpi: rng.range(72, 1201) as u32,
}
}
/// A render configuration.
pub fn gen_render_configuration(rng: &mut Rng) -> RenderConfiguration {
RenderConfiguration {
target: gen_render_target(rng),
color: gen_color_configuration(rng),
rasterization: gen_rasterization_configuration(rng),
}
}
/// An invalidation scope (every variant).
pub fn gen_invalidation_scope(rng: &mut Rng) -> InvalidationScope {
*rng.choose(&[
InvalidationScope::ObjectLocal,
InvalidationScope::MeasureLocal,
InvalidationScope::SystemLocal,
InvalidationScope::PageLocal,
InvalidationScope::RegionLocal,
InvalidationScope::WholeScore,
])
}
/// An invalidation set over generated slots, bands, constraints, and glyphs.
pub fn gen_invalidation_set(rng: &mut Rng) -> InvalidationSet {
let n = |r: &mut Rng| r.range_usize(0, 3);
InvalidationSet {
scope: gen_invalidation_scope(rng),
slots: (0..n(rng)).map(|_| gen_spring_slot_id(rng)).collect(),
bands: (0..n(rng)).map(|_| gen_vertical_band_id(rng)).collect(),
constraints: (0..n(rng)).map(|_| gen_constraint_id(rng)).collect(),
glyphs: (0..n(rng)).map(|_| gen_glyph_object_id(rng)).collect(),
}
}
// --- Remaining public-type generators (Agent F mandate completeness) ---
/// A render-scale context.
pub fn gen_scale_context(rng: &mut Rng) -> ScaleContext {
ScaleContext {
points_per_staff_space: 4.0 + (rng.range(0, 6000) as f32) / 1000.0,
}
}
/// A glyph anchor.
pub fn gen_glyph_anchor(rng: &mut Rng) -> GlyphAnchor {
let names = ["stemUpNW", "stemDownSE", "cutOutNE"];
GlyphAnchor {
name: std::borrow::Cow::Borrowed(names[rng.below(names.len() as u64) as usize]),
x: rng.range(0, 2048) as i32,
y: rng.range(0, 2048) as i32,
}
}
/// A vertical-band id.
pub fn gen_vertical_band_id(rng: &mut Rng) -> VerticalBandId {
VerticalBandId(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128)
}
/// A time-axis kind (every variant).
pub fn gen_time_axis_kind(rng: &mut Rng) -> TimeAxisKind {
match rng.below(4) {
0 => TimeAxisKind::Metric,
1 => TimeAxisKind::Proportional,
2 => TimeAxisKind::Aleatoric,
_ => TimeAxisKind::Registered(TimeAxisRegistryId(rng.next_u64() as u128)),
}
}
/// An engraving-decision id (content-derived from a generated decision).
pub fn gen_engraving_decision_id(rng: &mut Rng) -> EngravingDecisionId {
gen_engraving_decision(rng).id
}
/// A Sem-ver font version.
pub fn gen_sem_ver(rng: &mut Rng) -> SemVer {
SemVer::new(
rng.range(0, 4) as u32,
rng.range(0, 400) as u32,
rng.range(0, 9) as u32,
)
}
/// Glyph render data availability.
pub fn gen_glyph_render_data(rng: &mut Rng) -> GlyphRenderData {
GlyphRenderData {
outline: if rng.boolean() {
vec![gen_path_command(rng), PathCommand::Close]
} else {
vec![]
},
bitmap: rng.boolean().then(|| gen_glyph_bitmap(rng)),
}
}
pub fn gen_path_command(rng: &mut Rng) -> PathCommand {
match rng.below(4) {
0 => PathCommand::MoveTo(gen_point(rng)),
1 => PathCommand::LineTo(gen_point(rng)),
2 => PathCommand::CurveTo {
control1: gen_point(rng),
control2: gen_point(rng),
to: gen_point(rng),
},
_ => PathCommand::Close,
}
}
pub fn gen_glyph_bitmap(rng: &mut Rng) -> GlyphBitmap {
GlyphBitmap {
width: 1,
height: 1,
rgba8: vec![rng.next_u64() as u8; 4],
}
}
/// A staff-space bounding box drawn from a bundled glyph.
pub fn gen_glyph_bounding_box(rng: &mut Rng) -> BoundingBox {
gen_glyph_metric(rng).bounding_box()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fixtures;
use epiphany_core::TypedObjectId;
use epiphany_determinism::CanonicalEncode;
#[test]
fn edit_barrier_blob_codec_round_trips_generated_barriers() {
// Property gate for the provisional manifest-blob byte form
// (`ExtensionDeclaration.edit_barriers` / `.affected_object_kinds`):
// any generated barrier set encodes to a blob that decodes and
// re-encodes byte-identically, and each decoded barrier's own canonical
// bytes round-trip through the single-barrier decoder.
let mut rng = Rng::new(0xBA22_1E2C_0DEC);
for _ in 0..256 {
let barriers: Vec<EditBarrier> = (0..rng.range_usize(0, 4))
.map(|_| gen_edit_barrier(&mut rng))
.collect();
let blob = encode_edit_barriers(&barriers);
let decoded = decode_edit_barriers(&blob).expect("a canonical blob decodes");
assert_eq!(
encode_edit_barriers(&decoded),
blob,
"decode → re-encode is byte-identical"
);
for barrier in &decoded {
let bytes = barrier.to_canonical_bytes();
assert_eq!(
EditBarrier::decode_canonical_bytes(&bytes).as_ref(),
Ok(barrier)
);
}
let kinds: Vec<ObjectKind> = (0..rng.range_usize(0, 6))
.map(|_| gen_object_kind(&mut rng))
.collect();
let kind_blob = encode_affected_object_kinds(&kinds);
let decoded_kinds =
decode_affected_object_kinds(&kind_blob).expect("a canonical blob decodes");
assert_eq!(encode_affected_object_kinds(&decoded_kinds), kind_blob);
}
}
#[test]
fn ir_generators_are_deterministic_and_well_formed() {
let mut a = Rng::new(13);
let mut b = Rng::new(13);
// Deterministic from the seed.
assert_eq!(gen_logical_layout_ir(&mut a), gen_logical_layout_ir(&mut b));
let generated = gen_logical_layout_ir(&mut Rng::new(14));
let constrained = try_to_constrained(&generated)
.expect("generated logical IR must be structurally transformable");
// The spacing stage emits real constraints, which the stub does not
// evaluate: the solve is renderable either way, and satisfaction is
// claimed exactly when the problem is constraint-free.
let report = StubSolver.solve(&constrained, &SolverConfig::default());
assert!(report.status.is_renderable());
assert_eq!(
report.satisfied_hard_constraints,
constrained.constraints.is_empty()
);
// Provenance ids are consistent with their synthesis: a projected one
// (no synthesis) carries the source-only id; a synthesized one does not.
// Every generated constrained IR is accepted by the real stub solver.
let mut rng = Rng::new(99);
for _ in 0..64 {
let p = gen_provenance(&mut rng);
match p.synthesis {
None => assert_eq!(p.stable_id, stable_layout_id(&p.source)),
Some(_) => assert_ne!(p.stable_id, stable_layout_id(&p.source)),
}
let ci = gen_constrained_layout_ir(&mut rng);
assert_eq!(
StubSolver.solve(&ci, &SolverConfig::default()).status,
SolveStatus::Solved
);
let _ = gen_glyph_object(&mut rng);
let _ = gen_resolved_glyph(&mut rng);
let _ = gen_time_axis_model(&mut rng);
let _ = gen_synthesis_kind(&mut rng);
let _ = gen_solve_status(&mut rng);
let _ = gen_glyph_catalog_identity(&mut rng);
let _ = gen_layout_object_id(&mut rng);
let _ = gen_resolved_layout_ir(&mut rng);
let _ = gen_render_primitive(&mut rng);
let _ = gen_render_ir(&mut rng);
let _ = gen_solve_report(&mut rng);
let _ = gen_round_trip_report(&mut rng);
// The rest of E's public surface.
let _ = gen_staff_space(&mut rng);
let _ = gen_size2d(&mut rng);
let _ = gen_bounding_box(&mut rng);
let _ = gen_glyph_object_id(&mut rng);
let _ = gen_glyph_metric(&mut rng);
let _ = gen_smufl_version(&mut rng);
let _ = gen_font_id(&mut rng);
let _ = gen_engraving_decision(&mut rng);
let _ = gen_vertical_band(&mut rng);
let _ = gen_vertical_band_kind(&mut rng);
let barrier = gen_edit_barrier(&mut rng);
// The barrier canonically encodes (Appendix D), and prohibition is
// well-defined against a generated edit context.
let _ = barrier.to_canonical_bytes();
let _ = barrier.prohibits_edit(
gen_operation_kind_tag(&mut rng),
&crate::generators::typed_object_id(&mut rng),
&gen_edit_context(&mut rng),
&AlwaysLiveOracle,
);
let _ = gen_solver_config(&mut rng);
let _ = gen_solver_tier(&mut rng);
let _ = gen_solver_version(&mut rng);
let _ = gen_invalidation_set(&mut rng);
// The remaining public surface.
let _ = gen_solver_budget_used(&mut rng);
let _ = gen_solver_state(&mut rng);
let _ = gen_solver_profile(&mut rng);
let _ = gen_tie_breaking_weights(&mut rng);
let _ = gen_normalized_metric(&mut rng);
let _ = gen_constraint_id(&mut rng);
let _ = gen_spring_slot_id(&mut rng);
let _ = gen_solver_warning(&mut rng);
let _ = gen_scale_context(&mut rng);
let _ = gen_glyph_anchor(&mut rng);
let _ = gen_vertical_band_id(&mut rng);
let _ = gen_time_axis_kind(&mut rng);
let _ = gen_engraving_decision_id(&mut rng);
let _ = gen_sem_ver(&mut rng);
let _ = gen_glyph_render_data(&mut rng);
let _ = gen_glyph_bounding_box(&mut rng);
let _ = gen_decision_source(&mut rng);
let _ = gen_engraving_decision_kind(&mut rng);
let _ = gen_object_kind(&mut rng);
let _ = gen_barrier_scope(&mut rng);
let _ = gen_barrier_condition(&mut rng);
let _ = gen_invalidation_scope(&mut rng);
let _ = gen_size2d(&mut rng);
let _ = gen_layout_object(&mut rng);
let _ = gen_layout_region(&mut rng);
let _ = gen_font_id(&mut rng);
let _ = gen_smufl_version(&mut rng);
let _ = gen_quality_metric_kind(&mut rng);
let _ = gen_extension_warning_id(&mut rng);
let _ = gen_extension_metric(&mut rng);
let _ = gen_extension_metric_id(&mut rng);
let _ = gen_quality_metric_vector(&mut rng);
let _ = gen_render_target(&mut rng);
let _ = gen_render_configuration(&mut rng);
let _ = gen_glyph_render_data(&mut rng);
let _ = gen_vertical_band(&mut rng);
let _ = gen_rect(&mut rng);
let _ = gen_transform_2d(&mut rng);
let _ = gen_margins(&mut rng);
let _ = gen_score_version(&mut rng);
let _ = gen_local_coordinate_system(&mut rng);
let _ = gen_vertical_extent(&mut rng);
let _ = gen_cross_region_object(&mut rng);
let _ = gen_synthesis_instance_key(&mut rng);
let _ = gen_glyph_style(&mut rng);
let _ = gen_time_point(&mut rng);
let _ = gen_time_range(&mut rng);
let _ = gen_spring_slot(&mut rng);
let _ = gen_constrained_layout_region(&mut rng);
let _ = gen_layout_constraint(&mut rng);
let _ = gen_constraint_strength(&mut rng);
let _ = gen_engraving_override(&mut rng);
let _ = gen_resolved_staff(&mut rng);
let _ = gen_resolved_measure(&mut rng);
let _ = gen_resolved_system(&mut rng);
let _ = gen_resolved_page(&mut rng);
let _ = gen_dependency_index(&mut rng);
let _ = gen_layout_cache(&mut rng);
let _ = gen_system_id(&mut rng);
let _ = gen_logical_region_cache(&mut rng);
let _ = gen_constrained_region_cache(&mut rng);
let _ = gen_resolved_system_cache(&mut rng);
let _ = gen_fine_layout_cache(&mut rng);
let _ = gen_color_configuration(&mut rng);
let _ = gen_rasterization_configuration(&mut rng);
let _ = gen_path_command(&mut rng);
let _ = gen_glyph_bitmap(&mut rng);
}
}
#[test]
fn solver_rejects_a_catalog_hash_that_misses_its_glyphs() {
let mut rng = Rng::new(8);
let mut input = gen_constrained_layout_ir(&mut rng);
// Ensure there is at least one glyph to consult.
if input.glyphs.is_empty() {
input = gen_constrained_layout_ir(&mut Rng::new(3));
}
assert_eq!(
StubSolver.solve(&input, &SolverConfig::default()).status,
SolveStatus::Solved
);
input.catalog.metrics_hash[0] ^= 1;
assert_eq!(
StubSolver.solve(&input, &SolverConfig::default()).status,
SolveStatus::InternalError,
"solver must reject a catalog hash that does not cover consulted metrics"
);
}
#[test]
fn ten_measure_single_staff_round_trips() {
// The QUICKSTART's headline case for Agent E's hand-off: a real
// 10-measure single-staff score, driven through the real crate.
let score = fixtures::ten_measure_single_staff(0xA11CE);
let report = round_trip(&score);
assert!(report.glyphs > 0);
assert_eq!(
report.render_primitives,
report.glyphs + report.render_strokes
);
let measures = report
.recovered_sources
.iter()
.filter(|s| matches!(s, TypedObjectId::Measure(_)))
.count();
assert_eq!(measures, 10, "all ten measures must be laid out");
assert!(report
.recovered_sources
.iter()
.any(|s| matches!(s, TypedObjectId::Tie(_))));
assert!(report
.recovered_sources
.iter()
.any(|s| matches!(s, TypedObjectId::ChordSymbol(_))));
}
}