Pass 13 reopens; P13-I1: complete the ConstrainedLayoutIR listing

Three parked candidates accumulated while the Standard-tier solver track closed
and the notation-quality pass landed, which is the threshold at which the house
rule opens a batch pass. PASS13_CANDIDATES.md reopens as Batch 2 with P13-I1
(this), P13-I2 (Staff::default_clef never consulted), P13-I3 (the notehead stem
anchors).

P13-I1 was filed as "two elided fields". Checking before writing turned up a
third, and it is the one that matters: `diagnostics: Vec<LayoutDiagnostic>`
appears NOWHERE in core_spec, though it is how the projection's honesty rule
manifests. So Chapter 7's listing gains break_origins, diagnostics, and catalog;
BreakOrigin and LayoutDiagnostic gain their shapes.

And it gains req:layoutir:coverage-diagnostics, ratified as implemented: an
object the projection cannot engrave faithfully -- a pitch with no resolved
spelling, a glyph the bound catalog does not carry -- MUST be recorded as a
LayoutDiagnostic AND still placed, as a fallback notehead or a zero-extent traced
anchor. It MUST NOT silently substitute a plausible shape, and MUST NOT drop the
object. Both halves matter: dropping it breaks the round-trip surjection, so 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.

Locked by an_unengravable_object_is_recorded_and_still_placed, which drives a
percussion clef (no bundled glyph) and asserts all three halves: the diagnostic
names the object, no glyph stands in for it, and a zero-extent anchor keeps its
provenance addressable.

Spec rebuilds clean, no undefined references, new label registered. No code
change beyond the test; zero golden churn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-07-09 14:42:32 -04:00
parent 24b6a34db9
commit b7dab0fd44
5 changed files with 145 additions and 6 deletions

View File

@ -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<LayoutDiagnostic>` 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`

View File

@ -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

View File

@ -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<BreakOrigin>` (named by `req:layoutir:break-origin-attribution`, its own shape unlisted), `catalog: GlyphCatalogIdentity` (its type specified, the field unlisted), and `diagnostics: Vec<LayoutDiagnostic>` — 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** |

Binary file not shown.

View File

@ -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<LayoutConstraint>,
/// 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<BreakOrigin>,
/// Engraving decisions, carried forward from the logical stage.
pub engraving_decisions: Vec<EngravingDecision>,
/// Engraving-coverage gaps, surfaced rather than hidden.
pub diagnostics: Vec<LayoutDiagnostic>,
/// 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 {