epiphany/crates/epiphany-engrave
Levi Neuwirth b1bfe04d6c Inter-staff review fix: attribute a curve by its arc, not its nearest glyph
The stem tear-off repaired in 4132a7a had a twin one layer deeper, live in
the same fixture and baked into the same golden: the bottom staff's slur was
attributed to the TOP staff, kept shift 0, and tore off its own notes.

A distance metric cannot fix this one. A slur's start endpoint is deliberately
lifted off its notes -- staff_top + gap above, staff_bottom - gap below -- into
the inter-staff zone, where the nearest glyph is routinely a note on the
adjacent staff (here, a top-staff ledger note). So attribute a curve the way it
is drawn: the arc's direction picks the side. An upward arc (p1.y >= p0.y) hangs
below a staff -- take the greatest staff-line band bottom at or above p0.y; a
downward arc sits above one -- take the smallest band top at or below p0.y.
Fall back to the nearest band mid-line for a curve inside a staff.

Locked by a_slur_travels_with_its_own_staff, verified to fail without the fix
(d_bottom=8.695 > d_top=7.235 -- the slur riding the wrong staff).

Also assert the two metrics the solve moves rather than leaving them untested:
collision_penalty is 0 (the staves separate cleanly) and vertical_density_penalty
saturates at 1.0 -- the solve targets content extents while the metric scores the
realized gap against the band model's preferred height. That is the same
metric-vs-solver tension as casting_off under justification; DECISIONS.md records
it, plus three further reviewed gaps: staff-less content takes shift 0, the
preferred gap is read from the constructor rather than the region's declared
band, and the 3+-staff cumulative cascade is correct but unexercised.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 10:05:14 -04:00
..
src Inter-staff review fix: attribute a curve by its arc, not its nearest glyph 2026-07-09 10:05:14 -04:00
Cargo.toml Phase 3 tranche 1: casting-off, K1 schema-fill, value-restoring undo 2026-07-02 21:55:26 -04:00
DECISIONS.md Inter-staff review fix: attribute a curve by its arc, not its nearest glyph 2026-07-09 10:05:14 -04:00
README.md Phase 3 tranche 1: casting-off, K1 schema-fill, value-restoring undo 2026-07-02 21:55:26 -04:00

README.md

epiphany-engrave

Agent I's engraving constraint solver (spec Chapter 9): turns a ConstrainedLayoutIR into a ResolvedLayoutIR with real geometry. It is the production-side replacement for epiphany-layout-ir's interface-only StubSolver; the two live in separate crates so the spec's core/product boundary stays sharp.

Status: Minimal tier, with casting-off

  • Engraver runs a deterministic horizontal spacing pass (the first axis of the two-pass spring layout): each spring slot is placed left-to-right by a collision-aware advance derived from real glyph bearings.
  • A casting-off pass (Phase 3's layout track) then breaks the spaced line into systems at measure boundaries (greedy first-fit against a PageGeometry — default A4 portrait at an 8 mm staff), stacks systems vertically at the vertical-band model's inter-system gap, assigns pages by content height, and populates the real ResolvedPage/ResolvedSystem tree. Every position is baked into a single y-up world frame (pages stacked vertically), so the SVG renderer and hit-testing consume the flat glyph/stroke lists unchanged.
  • The IR's declared constraints are evaluated — geometric families against the pre-casting spaced frame, break constraints against the final break structure (hard breaks are always honoured; a pathological soft break is skipped and recorded as an IrOverride decision). Chosen breaks are recorded as EngravingDecisions with SynthesisKind::EngravedBreak targets, attributed to the user override that requested them when one did.
  • It reports SolverTier::Minimal: hard constraints (break family included) satisfied, no optimality claim — the quality-metric vector stays the honest all-worst placeholder until the Quality Metric Catalog lands.

Deferred: the vertical soft-spring solve, per-system justification/stretch, optimal break search, widow/orphan control, and casting-off caching. See DECISIONS.md.

use epiphany_engrave::Engraver;
use epiphany_layout_ir::{ConstraintSolver, SolverConfig};

let report = Engraver::default().solve(&constrained_ir, &SolverConfig::default());
assert!(report.satisfied_hard_constraints);
let resolved = report.layout; // real pages/systems; hand to epiphany-render-svg