Rescope the band-ownership MUST to the constrained stage; ratify it

Review follow-up on fc411ea. Finding 1 was correct and was mine to own: I wrote
"every primitive -- glyph, stroke, and curve alike -- MUST declare a
VerticalBandId" and placed it in the ResolvedLayoutIR section, where the adjacent
ResolvedGlyph listing has no such field. Under a literal read the implementation
was non-conforming against a requirement I had just added.

The rule belongs to the stage that has the obligation. req:layoutir:primitive-
band-ownership moves to Ch7 §ConstrainedLayoutIR §Vertical Bands and is scoped to
"every primitive the projection presents to the solver". A new companion,
req:layoutir:resolved-band-ownership, says what survives: a resolved Stroke/Curve
MUST retain its band (casting-off and the inter-staff solve both relocate them
and must attribute them), while a ResolvedGlyph carries none -- its ownership is
consumed during the solve and already baked into its resolved position, so the
field would be dead weight in a rendering fingerprint. That requirement also
carries the non-canonical clause: canonical_bytes omits vertical_band even from
the primitives that do retain it.

Finding 2: the ConstrainedLayoutIR listing gains strokes/curves. They have been
in the code since staff lines and were never listed -- so an implementation
following Chapter 7 could not have supplied non-glyph ownership to the solver at
all, which makes this the listing gap that actually blocked the new MUST.

Finding 3: the ratification trail now records the tranche -- a revision-history
row and a PASS12 log section naming both requirements, the three listing
corrections, the two commits, and the fact that adopting it churned no golden
(the declared owner agrees with the inferred one across the corpus, which is what
licensed the swap).

The log also records a pre-existing listing gap I found while checking my own
citations: ConstrainedLayoutIR still elides break_origins and catalog. I had
first written that neither is named anywhere in core_spec, then verified and
found both types ARE specified (req:layoutir:break-origin-attribution; Ch7 §Glyph
Catalog Identity) -- only the struct listing is incomplete, and unlike
strokes/curves neither gap blocks an implementation. Left as a Pass-13 candidate
rather than widened into this tranche.

Spec rebuilds clean: no LaTeX errors, no undefined references, both new labels
registered. Workspace 30/30, conformance 8/8; no Rust changed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-07-09 11:00:09 -04:00
parent fc411ea7ac
commit ced4b7255e
3 changed files with 132 additions and 26 deletions

View File

@ -235,3 +235,46 @@ appearance (Phrase/Editorial's own line, distinct from the SpanStyle dash),
duration-aware slur height (would pull the clamped short/long slurs back into
the shallow-arc band), collision-aware slur reshaping (the Standard-tier
`slur_shape` driver) — all Standard-tier work.
## Push-3 tranche (2026-07-09) — primitive band ownership
The Standard-tier inter-staff vertical solve needed to know which staff owns each
primitive. `GlyphObject` had always declared its `vertical_band`; `Stroke` and
`Curve` had not, so `epiphany-engrave` reconstructed ownership from geometry —
and got it wrong twice, tearing stems (`4132a7a`) and then slurs (`b1bfe04`) off
their own notes, both reaching a committed golden. A slur is the proof the
inference cannot be made safe: its endpoints are lifted clear of its own staff by
construction, into the zone where the nearest notehead belongs to the neighbouring
staff. Ratified as implemented in `efaebb9` (code) and `fc411ea` (Ch7 listings).
Layout-IR (Chapter 7) only, non-canonical — **no wire form, no companion version
move**, and no golden churn (the declared owner agrees with the inferred one on
the whole corpus, which is what licensed the swap).
| Item | Disposition | Spec locus | Consumer |
|---|---|---|---|
| Primitive band ownership | **adopt (new requirement)** — Ch7 gains `req:layoutir:primitive-band-ownership`: every primitive presented to the solver in `ConstrainedLayoutIR` (`GlyphObject`, `Stroke`, `Curve`) MUST declare the `VerticalBandId` that owns it and that band MUST exist; a vertical solver MUST take ownership from the declaration and MUST NOT infer it from geometry. Staff-less content names a non-`Staff` band. A band MUST therefore exist for every staff of a region and for its margin, members or not; only glyphs are band *members* (a stroke's/curve's reference is one-way) | core_spec Ch7 §ConstrainedLayoutIR §Vertical Bands (`req:layoutir:primitive-band-ownership`) | `epiphany-layout-ir` (`constrained.rs::{Stroke,Curve}.vertical_band`, band emission); `epiphany-engrave` (`casting.rs` attribution) |
| Ownership through the resolved stage | **adopt (new requirement)** — Ch7 gains `req:layoutir:resolved-band-ownership`: a resolved `Stroke`/`Curve` MUST retain its `vertical_band` (casting-off and the inter-staff solve both relocate them and must attribute them); a `ResolvedGlyph` carries none, its ownership already baked into its resolved position. Band ownership is non-canonical attribution metadata — it enters no content hash and no canonical encoding, so `ResolvedLayoutIR::canonical_bytes` omits it even from the primitives that carry it | core_spec Ch7 §ResolvedLayoutIR (`req:layoutir:resolved-band-ownership`) | `epiphany-layout-ir` (`resolved.rs::canonical_bytes`) |
| Struct listings | **adopt (ratify reality)** — the `ConstrainedLayoutIR` listing gains `strokes: Vec<Stroke>` / `curves: Vec<Curve>` (present in code since staff lines, never listed — without them Chapter 7 cannot describe supplying non-glyph ownership to the solver); the `Stroke`/`Curve` listings gain `vertical_band`; `Stroke`'s stale "the vertical-band model does not contain" gloss is dropped | core_spec Ch7 §ConstrainedLayoutIR, §ResolvedLayoutIR | `epiphany-layout-ir` |
**Version movements.** None on the wire: core spec Chapter 7 gains two
`req:layoutir:*` requirements, three struct-listing corrections, and a
revision-history row. Operation Catalog stays 0.7.0, Binary Format stays 0.6.0 —
layout geometry is non-canonical.
**Known listing gap (pre-existing, not introduced here).** The `ConstrainedLayoutIR`
listing still elides two fields the code carries: `break_origins: Vec<BreakOrigin>`
and `catalog: GlyphCatalogIdentity`. Both *types* are specified — the former's
semantics by `req:layoutir:break-origin-attribution`, the latter by Ch7 §Glyph
Catalog Identity — so only the struct listing is incomplete, and neither gap
blocks an implementation the way a missing `strokes`/`curves` would have. Tracked
as a Pass-13 candidate, deliberately not fixed here: the listing correction in
this tranche is scoped to exactly what `req:layoutir:primitive-band-ownership`
depends on.
**Deferred (documented, not open candidates).** A height model for the
inter-staff gap band — the missing piece behind both staff-less content placed
*between* two staves (it holds still while the lower staff descends away) and
`vertical_density_penalty` saturating at 1.0 when the solve targets content
extents while the metric scores the realized gap against the band's *preferred*
height. Both are named in `epiphany-engrave/DECISIONS.md`; neither is a Pass-13
ambiguity.

Binary file not shown.

View File

@ -9138,9 +9138,48 @@ pub enum VerticalBandKind {
}
\end{lstlisting}
\begin{requirement}
\label{req:layoutir:primitive-band-ownership}
\textbf{Primitive band ownership is declared, not inferred.} Every
primitive the projection presents to the solver in
\texttt{ConstrainedLayoutIR} --- \texttt{GlyphObject},
\texttt{Stroke}, and \texttt{Curve} alike --- \MUST{} declare the
\texttt{VerticalBandId} of the band that owns it, and that band
\MUST{} exist in \texttt{vertical\_bands}. A vertical solver \MUST{}
take a primitive's owning staff from that declaration and
\MUSTNOT{} infer it from the primitive's geometry. Content owned by
no staff (a page-margin annotation, a structure spanning several
staves) names a band whose kind is not \texttt{Staff}.
This is a correctness requirement, not a convenience. A primitive's
drawn position does not determine its owner: a stem shares its
horizontal column with the staff above, and a slur's endpoints are
\emph{lifted clear} of its own staff by construction, landing in the
inter-staff zone where the nearest notehead routinely belongs to the
neighbouring staff. A solver that guesses from proximity will
attribute such a primitive to the wrong staff and, on renegotiating
the gaps between a system's staves, tear it off the notes it belongs
to.
A band \MUST{} therefore exist for every staff of a region, whether
or not any glyph landed in it, and for the region's margin ---
otherwise a primitive that names one has nowhere to point. A band
MAY carry no members.
Only glyphs are \emph{members} of a band
(\texttt{VerticalBand::members}), which is what realizes the spring
solve; a stroke's or curve's band reference is one-way. What becomes
of the declaration downstream is
\ref{req:layoutir:resolved-band-ownership}.
\end{requirement}
\subsection{Constraints}
The IR carries explicit constraint inputs for the solver:
The IR carries explicit constraint inputs for the solver. Beyond
glyphs it presents the non-glyph primitives --- \texttt{Stroke} and
\texttt{Curve}, whose shapes are listed with
\ref{req:layoutir:resolved-primitives} --- since the solver positions
those too:
\begin{lstlisting}[language=Rust]
pub struct ConstrainedLayoutIR {
@ -9157,6 +9196,14 @@ pub struct ConstrainedLayoutIR {
/// Glyph-level layout objects.
pub glyphs: Vec<GlyphObject>,
/// Non-glyph line primitives (staff lines, stems, barlines,
/// ledger lines, brackets) presented to the solver alongside
/// the glyphs.
pub strokes: Vec<Stroke>,
/// Cubic-Bezier curve primitives (slurs), likewise.
pub curves: Vec<Curve>,
/// Additional constraints not captured by spring parameters.
pub constraints: Vec<LayoutConstraint>,
@ -9326,32 +9373,26 @@ pub struct Curve {
\end{requirement}
\begin{requirement}
\label{req:layoutir:primitive-band-ownership}
\textbf{Primitive band ownership is declared, not inferred.} Every
primitive --- glyph, stroke, and curve alike --- \MUST{} declare the
\texttt{VerticalBandId} of the band that owns it, and the projection
\MUST{} name a band that exists in \texttt{vertical\_bands}. A
vertical solver \MUST{} take a primitive's owning staff from that
declaration and \MUSTNOT{} infer it from the primitive's geometry.
Content owned by no staff (a page-margin annotation, a structure
spanning several staves) names a band whose kind is not
\texttt{Staff}.
\label{req:layoutir:resolved-band-ownership}
\textbf{Band ownership through the resolved stage.} A resolved
\texttt{Stroke} and \texttt{Curve} \MUST{} retain the
\texttt{vertical\_band} they were given in
\texttt{ConstrainedLayoutIR}
(\ref{req:layoutir:primitive-band-ownership}), so a solver that
re-spaces or relocates them --- casting off into systems, or
renegotiating the gaps between a system's staves --- can attribute
each one to its owning staff. A \texttt{ResolvedGlyph} carries no
band: a glyph's ownership is consumed during the solve and its
outcome is already baked into the glyph's resolved position, so the
field would be dead weight in a rendering fingerprint.
This is a correctness requirement, not a convenience. A primitive's
drawn position does not determine its owner: a stem shares its
horizontal column with the staff above, and a slur's endpoints are
\emph{lifted clear} of its own staff by construction, landing in the
inter-staff zone where the nearest notehead routinely belongs to the
neighbouring staff. A solver that guesses from proximity will
attribute such a primitive to the wrong staff and, on renegotiating
the staff gaps, tear it off the notes it belongs to.
Only glyphs are \emph{members} of a band
(\texttt{VerticalBand::members}), which is what realizes the
horizontal spring solve; a stroke's or curve's band reference is
one-way. Like the primitives themselves, the reference is
non-canonical layout-attribution metadata: it enters no content hash
and no canonical encoding.
Band ownership is \emph{non-canonical layout-attribution metadata}.
It enters no content hash and no canonical encoding
(\ref{req:layoutir:object-id-derivation}): it draws nothing, so two
layouts differing only in it are the same rendered layout. A
conformant canonical encoding of \texttt{ResolvedLayoutIR} therefore
omits \texttt{vertical\_band} even from the strokes and curves that
carry it.
\end{requirement}
\begin{requirement}
@ -15570,6 +15611,28 @@ layouts they own versus inherit:
unchanged, no version move). Layout geometry stays non-canonical --- no
wire form, no companion version moves.
\\
\midrule
\today & Layout IR (primitive band ownership, Push 3) &
Chapter 7 gains \ref{req:layoutir:primitive-band-ownership}: every
primitive the projection presents to the solver ---
\texttt{GlyphObject}, \texttt{Stroke}, \texttt{Curve} --- declares the
\texttt{VerticalBandId} that owns it, naming a band that exists, and a
vertical solver takes ownership from that declaration rather than
inferring it from geometry. Ratifies-as-implemented the contract a
vertical solver needs and an inference cannot supply: a slur's
endpoints are lifted clear of its own staff, so its owner is not
recoverable from where it is drawn. The \texttt{ConstrainedLayoutIR}
listing gains \texttt{strokes} / \texttt{curves} (present since staff
lines, never listed) and the \texttt{Stroke} / \texttt{Curve} listings
gain \texttt{vertical\_band}; a band now exists for every staff of a
region and for its margin, members or not.
\ref{req:layoutir:resolved-band-ownership} carries the declaration
through the resolved stage on strokes and curves (a
\texttt{ResolvedGlyph} keeps none --- its ownership is already baked
into its position). Band ownership is non-canonical attribution
metadata: it enters no content hash and no canonical encoding, so no
wire form and no companion version moves.
\\
\bottomrule
\end{longtable}