epiphany/crates/epiphany-layout-ir
Levi Neuwirth b9a9921d50 Phase 3 tranche 2: quality conformance companions, real metrics, multi-system click fix
857 workspace tests pass; clippy -D warnings, fmt, and rustdoc clean;
both new companions build with zero undefined references.

Quality Metric Catalog v0.1.0 (spec/quality_metric_catalog.tex, new):
- Formal definitions for all nine normative quality metrics, each with
  a raw measurement over resolved geometry and a clamped-linear
  normalization n = min(1, raw/R_worst) with pinned anchors.
- The vacuous-geometry rule (a metric over absent geometry evaluates
  to 0.0; the notated-but-unrendered honesty edge is an open
  question), all-1.0 default tie-breaking weights, and the per-tier
  threshold table — Minimal's uniform 0.90 deliberately fails the
  all-worst placeholder, forcing real measurement.
- Pins QualityMetricKind (referenced but never defined by the core
  spec) and the registered SolverProfile catalog (Draft selects the
  Minimal threshold column; Standard/Publication select Standard).
- QualityFloorApproached fires at 0.8x the applicable threshold and
  is status-neutral by requirement.

Reference Suite v0.1.0 (spec/reference_suite.tex, new):
- Six entries referenced by deterministic builder + seed (RS-1
  ten_measure_single_staff, RS-2 valid_score_rich, RS-3..6 corpus
  fixtures), each with the declared A4-at-8mm-staff solve geometry
  (Canvas.layout_defaults has no graph home yet, P12-I7).
- All entries required at Minimal; the same set is the pre-declared
  Standard bar (no implementation claims Standard yet). Fixed-
  expectation tests deliberately unused in v0.1.

Real metrics in the engraver (engrave/src/quality.rs, new;
layout-ir/src/quality.rs = the catalog constants transcribed):
- QualityMetricVector::unmeasured() replaced with computed values:
  collision sweep with the catalog's same-slot-cluster and stroke
  exclusions, per-system spacing CV, vertical gap deviations,
  system-break slack, page fill, casting-off width CV, symbol
  density; slur/beam vacuously 0.0 (no drawn geometry exists).
- Bit-identical across repeated solves (tested); floor warnings never
  change solve status; malformed inputs keep unmeasured(). The two
  all-worst test pins now assert real values; the StubSolver's
  unmeasured() stays (Stub genuinely computes nothing).

Reference-suite harness (testkit reference_suite module + tests):
- Each RS entry asserts the four-condition Minimal pass (hard
  constraints, byte/bit determinism, well-formed Minimal report,
  every axis within threshold) under the F1 Pass/Xfail discipline,
  with the measured table printed per run.
- HONEST FINDING, day one: RS-1 fails Minimal casting-off (measured
  1.0 vs 0.90) — greedy first-fit leaves a two-measure stub last
  system (width CV 0.6145). Encoded as an asserted Xfail row (fails
  on XPASS) and filed as P12-I11 (engrave balance pass, or catalog
  revision). P12-I12: the Standard spacing floor warns on short
  scores with wide lead measures.

Multi-system click-to-insert fix (editor-core):
- Casting-off exposed two inversion breaks: position_anchors fed a
  non-monotonic cross-system anchor list into a monotonic inverter
  (system-2 clicks resolved to system-1 times), and
  nearest_manifestation found only system 1's staff-line segment
  (system-2 clicks got system-1 pitch geometry).
- Fixed with a containing-system lookup over the resolved pages tree
  (containment, else nearest by vertical distance), per-system staff
  resolution, and per-system anchor filtering; degenerate-geometry
  fallback preserves the flat path, so all 84 pre-existing
  editor-core tests pass unmodified.
- Five regression tests through the real Engraver over the wrapped
  ten-measure fixture, each shown to fail without the fix; testkit
  gains dev-only dependencies on editor-core and engrave.

Trackers: P12-I11/I12 filed; DECISIONS entries in engrave, layout-ir,
and testkit; Phase-3 memory updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
2026-07-03 10:39:05 -04:00
..
src Phase 3 tranche 2: quality conformance companions, real metrics, multi-system click fix 2026-07-03 10:39:05 -04:00
Cargo.toml Land epiphany-layout-ir (Agent E): layout IR + solver interface 2026-06-19 19:58:01 -04:00
DECISIONS.md Phase 3 tranche 2: quality conformance companions, real metrics, multi-system click fix 2026-07-03 10:39:05 -04:00
README.md Land epiphany-layout-ir (Agent E): layout IR + solver interface 2026-06-19 19:58:01 -04:00

README.md

epiphany-layout-ir

The Epiphany layout intermediate representation and constraint-solver interface, implementing the normative requirements of Chapter 7 ("Layout Intermediate Representation") and the interface of Chapter 9 ("Constraint-Solver Interface") of the core specification (spec/core_spec.pdf). This is Agent E's crate per spec/QUICKSTART.md — it lands last among the implementation crates, building on Agent A's epiphany-determinism and Agent B's epiphany-core (and on Agent C's OperationKindTag for the edit-barrier types — see DECISIONS.md).

The IR sits between the score graph and two downstream consumers: the constraint solver, which resolves spacing and positioning, and the renderer, which produces final visual output. — Chapter 7

The score graph is the canonical truth about the music; this crate is a downstream projection of it. The transformation is a pipeline of four stages, each with its own type and a deterministic, provenance-preserving contract for the next.

What's here

Area Items Spec
Stage 1 — logical LogicalLayoutIR, LayoutRegion, composite LayoutObject, overrides/cross-region objects, to_logical Ch. 7 §"LogicalLayoutIR"
Stage 2 — constrained ConstrainedLayoutIR, SpringSlot, LayoutConstraint, GlyphObject, to_constrained Ch. 7 §"ConstrainedLayoutIR"
Stage 3 — resolved ResolvedLayoutIR, pages/systems/staves/measures, ResolvedGlyph Ch. 7 §"ResolvedLayoutIR"
Stage 4 — render (interface only) RenderIR, RenderPrimitive, RenderIRProducer, to_render Ch. 7 §"RenderIR"
Time axis canonical TimeAxisModel plus dynamic TimeAxis, including registered payload preservation Ch. 7 §"Layout Regions"
Provenance Provenance, LayoutObjectId, SynthesisKind, stable_layout_id (a pure function of the source — stable across relayouts) Ch. 7 §"Provenance"
Engraving decisions EngravingDecision/EngravingDecisionId/EngravingDecisionKind, DecisionSource Ch. 7 §"Engraving Decisions"
Vertical bands VerticalBand/VerticalBandId/VerticalBandKind Ch. 7 §"Vertical Bands"
Incremental cache LayoutCache, DependencyIndex, granular stage caches and invalidation Ch. 7 §"Incremental Layout and Caching"
Glyph catalog GlyphCatalog (metric-lookup interface) + in-tree BravuraCatalog, GlyphCatalogIdentity, SmuflVersion, FontId, GlyphMetric/GlyphAnchor, BRAVURA_METRICS/BRAVURA_VERSION, metrics_hash_for (MUSCFNTM-tagged) Ch. 7 §"Glyph Catalog Interface" / §7.3.2
Edit barriers EditBarrier, BarrierScope, BarrierCondition, ObjectKind, EditContext (precise scope evaluation), keyed on Agent C's OperationKindTag Ch. 8 §"Edit Barriers"
Solver interface ConstraintSolver (solve/solve_incremental, Send + Sync), SolverConfig/SolverBudget, SolverState, InvalidationSet, SolveReport, SolveStatus, SolverTier/SolverVersion, the v0 StubSolver Ch. 9
Round-trip round_trip, RoundTripReport, laid_out_object_ids Ch. 7 (v0 acceptance criterion 6)

The stub solver

Per the QUICKSTART, the v0 constraint solver is a stub: StubSolver returns SolveStatus::Solved with the input geometry verbatim (each glyph's resolved position is exactly its constrained baseline), preserves provenance, and reports all hard constraints satisfied. The real solver — Cassowary or otherwise — comes later (Chapter 9 specifies the interface, not the algorithm); v0 only needs to round-trip IR through the solver interface to prove the contracts hold. The one stub validates the full Chapter 7 §7.3.2 catalog identity and all slot, band, and geometry cross-references before reporting Solved. Explicit constraints are rejected because this interface-only solver cannot honestly evaluate them.

The round-trip (v0 acceptance criterion 6)

round_trip runs graph → LogicalLayoutIRConstrainedLayoutIR → stub-solved ResolvedLayoutIRRenderIR and asserts the contract every stage must satisfy:

  • the stub solver reports Solved with all hard constraints satisfied;
  • the complete Provenance of every object — source, synthesis, dependencies, and stable_id — survives every stage unchanged;
  • no two objects ever share a stable_id, so manifestation multiplicity is preserved (a source manifested in two regions stays two layout objects);
  • the stub solver returns the input geometry verbatim;
  • the set of score-graph sources recovered from the RenderIR is exactly the set laid out — a surjection onto graph identity (one source may back several manifestations, each with its own stable id).

Agent F's testkit drives this same entry point (layout_stub::round_trip) on the 10-measure single-staff hand-off fixture and the rich multi-region generator.

Algorithmic scope

Per the QUICKSTART ("a prototype baseline, not the product"), this crate implements the Chapter 7 IR contracts and interface types, not a production engraving engine. The real spacing and casting-off algorithms, quality-metric computation, constraint solver, and renderer remain later implementations of these interfaces.

Determinism

IR coordinates are single-precision staff spaces (StaffSpace(f32), Chapter 7 §7.2); the canonical ResolvedLayoutIR output quantizes them to the 1/1024 grid at serialization time (ResolvedLayoutIR::canonical_bytes), exactly as Appendix D §"Quantized Layout Coordinates" prescribes — quantization absorbs all f32 variation below 1/2048 staff space, so the canonical output is independent of the floating-point environment. The glyph-catalog identity hashes its consulted metrics (advance, bounding box, named anchors) under the MUSCFNTM domain tag, and the edit-barrier types carry a canonical encoding with set-valued fields emitted in canonical byte order (sorted and de-duplicated). See DECISIONS.md.

Tests

cargo test -p epiphany-layout-ir covers each module plus the round-trip on Agent B's valid_score / valid_score_rich generators. The end-to-end v0 acceptance gate (criterion 6) lives in the testkit.