diff --git a/crates/epiphany-layout-ir/DECISIONS.md b/crates/epiphany-layout-ir/DECISIONS.md index b01a114..2fe4168 100644 --- a/crates/epiphany-layout-ir/DECISIONS.md +++ b/crates/epiphany-layout-ir/DECISIONS.md @@ -682,7 +682,7 @@ strokes and curves. cross-staff slur is not drawn at this tier (`staff.is_some()` guards the curve; it engraves to an anchor stroke), so a drawn curve always names a staff band. -## Parked: the ConstrainedLayoutIR listing is still abridged (2026-07-09) +## RESOLVED (P13-I1): the ConstrainedLayoutIR listing was abridged (2026-07-09) Chapter 7's `ConstrainedLayoutIR` listing gained `strokes` / `curves` when `req:layoutir:primitive-band-ownership` landed (that requirement depends on @@ -698,6 +698,18 @@ Neither blocks an implementation the way a missing `strokes`/`curves` did: both are governed by requirement text elsewhere, so a conformant implementer is not left guessing. +**Resolved as P13-I1.** Filing it turned up a *third* elided field, and the +substantive one: `diagnostics: Vec` appeared **nowhere** in +core_spec, though it is how the projection's honesty rule manifests. The listing +now carries all three, `BreakOrigin` and `LayoutDiagnostic` gain their shapes, and +Chapter 7 gains `req:layoutir:coverage-diagnostics`: an object the projection +cannot engrave faithfully MUST be recorded *and* still placed — a fallback +notehead or a zero-extent traced anchor — never silently substituted, never +dropped. Dropping it breaks the round-trip surjection (a hit-test can no longer +find what the author wrote); guessing produces a score that looks engraved and is +wrong, with nothing in the IR to say so. Ratified as implemented; locked by +`an_unengravable_object_is_recorded_and_still_placed`. + ## Parked: `Staff::default_clef` is never consulted (2026-07-09) `to_constrained` takes a staff instance's active clef from its `clef_sequence` diff --git a/crates/epiphany-layout-ir/src/constrained.rs b/crates/epiphany-layout-ir/src/constrained.rs index e01c046..3cb3c3b 100644 --- a/crates/epiphany-layout-ir/src/constrained.rs +++ b/crates/epiphany-layout-ir/src/constrained.rs @@ -4808,6 +4808,61 @@ mod tests { ); } + /// `req:layoutir:coverage-diagnostics`: an object the projection cannot + /// engrave faithfully is **recorded and still placed** — never guessed at, + /// never dropped. A percussion clef has no bundled glyph, so the staff + /// instance engraves to a zero-extent traced anchor that keeps its + /// provenance (a hit-test can still find it) while an `UnbundledGlyph` + /// diagnostic names the gap. + #[test] + fn an_unengravable_object_is_recorded_and_still_placed() { + let (mut score, _) = repeat_ready_score(47); + let instance_id = score.canvas.regions[0].staff_instances()[0].id; + score.canvas.regions[0] + .content + .staff_instances_mut() + .expect("staff-based")[0] + .clef_sequence + .push(epiphany_core::ClefChange { + anchor: TimeAnchor::WallClock { + time: epiphany_core::WallClockTime(0), + }, + clef: epiphany_core::Clef { + shape: epiphany_core::ClefShape::Percussion, + line: 3, + octave_shift: 0, + }, + }); + let c = to_constrained(&to_logical(&score)); + let source = TypedObjectId::StaffInstance(instance_id); + + // Recorded: the gap names the object and the glyph it wanted. + let diagnostic = c + .diagnostics + .iter() + .find(|d| d.source == source) + .expect("the unbundled clef is surfaced, not hidden"); + assert!( + matches!(diagnostic.kind, LayoutDiagnosticKind::UnbundledGlyph(_)), + "and says why: {:?}", + diagnostic.kind + ); + + // Not guessed: no glyph stands in for the clef. + assert!( + !c.glyphs.iter().any(|g| g.provenance.source == source), + "no plausible substitute is drawn" + ); + // Not dropped: a traced anchor keeps its provenance addressable. + let anchor = c + .strokes + .iter() + .find(|st| st.provenance.source == source) + .expect("the object is still placed, as a traced anchor"); + assert_eq!(anchor.from, anchor.to, "a zero-extent anchor draws no ink"); + assert_eq!(anchor.thickness.0, 0.0); + } + /// A stem points AWAY from the middle line — up for a head below it, down /// for a head above it or on it — and attaches on the side it points: an /// up-stem at the head's right edge, a down-stem at its left. A stem on a diff --git a/spec/PASS13_CANDIDATES.md b/spec/PASS13_CANDIDATES.md index 03a573c..62ed4bd 100644 --- a/spec/PASS13_CANDIDATES.md +++ b/spec/PASS13_CANDIDATES.md @@ -5,11 +5,12 @@ per the house rule (a batch pass opens at ≥3 candidates; this file opened when P13-D1/D2 joined P13-K1). Each entry names the owning DECISIONS record; this file is the index, not the analysis. -**Batch CLOSED (2026-07-08).** All four candidates are resolved (worked down in -order): P13-D3 and P13-K1 by the user's ratified calls ("fix the mint only" / -"reject the introduction"), P13-D1 and P13-D2 as correctness fixes with -convergence-locked / execute-then-fix regressions. No open Pass-13 candidates -remain; a future ≥3-candidate batch reopens the pass. +## Batch 1 — CLOSED (2026-07-08) + +All four candidates are resolved (worked down in order): P13-D3 and P13-K1 by +the user's ratified calls ("fix the mint only" / "reject the introduction"), +P13-D1 and P13-D2 as correctness fixes with convergence-locked / +execute-then-fix regressions. | Id | One-line statement | Filed in | Status | |---|---|---|---| @@ -17,3 +18,18 @@ remain; a future ≥3-candidate batch reopens the pass. | P13-D1 | Undo-driven event tombstones run graph-side re-anchor/cascade but never ledger-side `reanchor_for_tombstone`: structures leave the graph while staying `Live`, no `RepairRecord` — Ch6's same-step recording MUST is unmet for undo-driven tombstones (pre-existing class: slurs/spanners; repeats now too) | `crates/epiphany-ops/DECISIONS.md` (Schema major 2, Phase D) | **resolved** (Pass 13: `tombstone_undo_targets` runs the ledger re-anchor per event target; liveness guard; convergence-locked) | | P13-D2 | Cue-cascade recursion re-anchors against the triggering event before its tombstone lands in `objects`: a structure anchored on {X, cue-of-X} can record `Reanchored{to: X}` then `CascadeDeleted` in one effect (contradictory repair trail; plausible by code trace, unexecuted) | `crates/epiphany-ops/DECISIONS.md` (Schema major 2, Phase D) | **resolved** (Pass 13: `delete_event` tombstones before the graph delete, matching `cascade_cue`/undo; repro executed then fixed) | | P13-D3 | `CreateCrossCutting` validates only event endpoints (`CrossCuttingValue::endpoints()`), so a SPANNER anchored to a missing region/measure mints dangling past `anchor_target_exists`; and non-event referent tombstones (`DeleteRegion` under a region-anchored spanner/repeat) re-anchor nothing — "every referenced endpoint is live" is events-only as implemented | `crates/epiphany-ops/DECISIONS.md` (Phase D follow-up) | **resolved** (Pass 13: mint fixed via `anchor_object_refs`; non-event referent re-anchoring ratified events-only, user "fix the mint only") | + +## Batch 2 — OPEN (2026-07-09) + +Three candidates accumulated while the Standard-tier solver track closed and the +notation-quality pass (stems, slurs) landed. All three were **parked** as they +were found, each in `crates/epiphany-layout-ir/DECISIONS.md`, and reaching three +reopens the pass per the house rule. None is a live incorrectness in shipped +output; each is a place where the code, the spec, and the data disagree about +what is true. + +| Id | One-line statement | Filed in | Status | +|---|---|---|---| +| P13-I1 | Chapter 7's `ConstrainedLayoutIR` listing elides **three** fields the code carries: `break_origins: Vec` (named by `req:layoutir:break-origin-attribution`, its own shape unlisted), `catalog: GlyphCatalogIdentity` (its type specified, the field unlisted), and `diagnostics: Vec` — which appears **nowhere** in core_spec, though it is how the projection's honesty rule manifests: an unspellable pitch or an unbundled glyph is placed as a fallback *and recorded*, never silently guessed | `crates/epiphany-layout-ir/DECISIONS.md` ("the ConstrainedLayoutIR listing is still abridged") | **resolved** (Pass 13: listing gains all three fields; `BreakOrigin` and `LayoutDiagnostic` shapes added; new `req:layoutir:coverage-diagnostics` ratifies as-implemented that an unengravable object is recorded AND still placed — never guessed, never dropped) | +| P13-I2 | `Staff::default_clef` is never consulted: `to_constrained` takes the active clef from the staff instance's `clef_sequence` and falls back to `Clef::default()` (treble), so a bass-clef staff that declares its clef only on the `Staff` engraves as treble. The field is decorative in the projection — is it the fallback, or should it not exist? | `crates/epiphany-layout-ir/DECISIONS.md` ("`Staff::default_clef` is never consulted") | **open** | +| P13-I3 | `BRAVURA_METRICS`' `NOTEHEAD_ANCHORS` are hand-written, unconsumed, and doubly suspect: they name `stemUpNW`/`stemDownSE` — the corners a normal notehead's stems do *not* attach to, and a pair Bravura's `noteheadBlack` does not define — and their x of `1180` reads like 1.18 staff spaces written in thousandths rather than the table's `1/1024` units (1.18 sp = 1208). They enter only `metrics_hash`, so any correction moves the `GlyphCatalogIdentity` every conformance claim declares. The font is not vendored, so the values cannot be verified in-tree | `crates/epiphany-layout-ir/DECISIONS.md` ("the notehead stem anchors are unusable as written") | **open** | diff --git a/spec/core_spec.pdf b/spec/core_spec.pdf index e54d684..7e3eab9 100644 Binary files a/spec/core_spec.pdf and b/spec/core_spec.pdf differ diff --git a/spec/core_spec.tex b/spec/core_spec.tex index 0c92192..3c4cd7b 100644 --- a/spec/core_spec.tex +++ b/spec/core_spec.tex @@ -9138,6 +9138,27 @@ pub enum VerticalBandKind { } \end{lstlisting} +\begin{requirement} + \label{req:layoutir:coverage-diagnostics} + \textbf{Engraving-coverage gaps are surfaced, not guessed.} A projection + that cannot engrave an object faithfully --- a pitch with no resolved + spelling, a glyph the bound catalog + (\ref{sec:layoutir:catalog}) does not carry --- \MUST{} record a + \texttt{LayoutDiagnostic} naming the score-graph object and the kind of + gap, and \MUST{} still place the object: a fallback notehead on the clef + reference line, or a zero-extent traced anchor that keeps its provenance. + It \MUSTNOT{} silently substitute a plausible shape, and it \MUSTNOT{} + drop the object. + + Both halves matter. Dropping the object breaks the round-trip surjection + (\ref{sec:layoutir:provenance}) --- a hit-test can no longer find it, and + an editor can no longer select what the author wrote. Guessing a shape + produces a score that looks engraved and is wrong, with nothing in the IR + to say so. A diagnostic is not an error: the layout is still + \emph{renderable}, and a conformance claim is unaffected. It is the record + that the engraving is incomplete at a named place. +\end{requirement} + \begin{requirement} \label{req:layoutir:primitive-band-ownership} \textbf{Primitive band ownership is declared, not inferred.} Every @@ -9207,8 +9228,43 @@ pub struct ConstrainedLayoutIR { /// Additional constraints not captured by spring parameters. pub constraints: Vec, + /// Which user break override each projected break constraint came + /// from, so a solver can cite it in the decision it records. + pub break_origins: Vec, + /// Engraving decisions, carried forward from the logical stage. pub engraving_decisions: Vec, + + /// Engraving-coverage gaps, surfaced rather than hidden. + pub diagnostics: Vec, + + /// The glyph catalog this IR's metrics were drawn from. + pub catalog: GlyphCatalogIdentity, +} + +/// A projected break constraint's user-override attribution. +pub struct BreakOrigin { + pub slot: SpringSlotId, + pub class: BreakClass, + pub override_id: EngravingOverrideId, +} + +/// An object the projection could not engrave faithfully. +pub struct LayoutDiagnostic { + pub source: TypedObjectId, + pub kind: LayoutDiagnosticKind, +} + +pub enum LayoutDiagnosticKind { + /// A pitch reached the constrained pass with no resolved (or + /// non-CMN) spelling; its notehead is placed on the clef + /// reference line, but its true staff position is unknown. + MissingSpelling, + + /// A glyph the bound catalog does not carry; the object is + /// carried as a traced anchor rather than drawn at a guessed + /// shape. + UnbundledGlyph(GlyphReference), } pub enum LayoutConstraint {