diff --git a/spec/PASS12_RATIFICATION_LOG.md b/spec/PASS12_RATIFICATION_LOG.md index eab65f7..0366be0 100644 --- a/spec/PASS12_RATIFICATION_LOG.md +++ b/spec/PASS12_RATIFICATION_LOG.md @@ -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` / `curves: Vec` (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` +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. diff --git a/spec/core_spec.pdf b/spec/core_spec.pdf index 4b64d0b..e54d684 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 31d4b13..0c92192 100644 --- a/spec/core_spec.tex +++ b/spec/core_spec.tex @@ -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, + /// Non-glyph line primitives (staff lines, stems, barlines, + /// ledger lines, brackets) presented to the solver alongside + /// the glyphs. + pub strokes: Vec, + + /// Cubic-Bezier curve primitives (slurs), likewise. + pub curves: Vec, + /// Additional constraints not captured by spring parameters. pub constraints: Vec, @@ -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}