diff --git a/crates/epiphany-engrave/DECISIONS.md b/crates/epiphany-engrave/DECISIONS.md index 5e805eb..30460ab 100644 --- a/crates/epiphany-engrave/DECISIONS.md +++ b/crates/epiphany-engrave/DECISIONS.md @@ -111,12 +111,14 @@ inputs the solver cannot measure. be the spec's intent. Break constraints are evaluated against the **final break structure**. 4. **Page geometry is an engraver parameter (`PageGeometry`), defaulted to A4 - at an 8 mm staff.** The spec names `Canvas.layout_defaults` ("paper size, - margins") but never defines the type, and core does not implement it; - adding a graph field now would violate the companion's frozen-layout rule, - so the graph home (`CanvasLayoutDefaults`) is **staged to the data-model - schema major** and the engraver takes the geometry as a constructor - parameter. Default arithmetic (1 staff space = staff height / 4 = 2.0 mm at + at an 8 mm staff.** Adding a `Canvas` graph field is a schema-major change + under the companion's frozen-layout rule, so it was staged to the data-model + schema major. **Schema major 1 now defines the type** (`CanvasLayoutDefaults + { page_size: CanvasSize, margins: CanvasMargins }`, staff spaces, A4/8mm + default) and ratifies its wire form (core spec + Binary Format 0.3.0, + Phase A); the **code graph home lands in Phase C** (`Canvas` gains the + field) and the engraver reads it in Phase C′. Until then the engraver takes + the geometry as a constructor parameter. Default arithmetic (1 staff space = staff height / 4 = 2.0 mm at an 8 mm staff): A4 210 × 297 mm → **105 × 148.5** staff spaces; 15 mm margins → **7.5** staff spaces; content area 180 × 267 mm → **90 × 133.5** staff spaces. 90 staff spaces wraps the ten-measure hand-off fixture diff --git a/spec/PASS12_BATCH.md b/spec/PASS12_BATCH.md index 436c924..eea278b 100644 --- a/spec/PASS12_BATCH.md +++ b/spec/PASS12_BATCH.md @@ -62,7 +62,7 @@ code instead is the failure mode this batch exists to prevent. | P12-K9 | `epiphany-ops` K | Differing-value re-creates (a live id re-carried with different content: `CreateStaff`, the carried `TimeSignature`, container creates) refuse with `TargetMissing`, which misnames the situation. Decide whether a dedicated `PreconditionFailureReason` (appended) is warranted. | G / Pass 12 (vocabulary) | | P12-K10 | `epiphany-ops` K | Undo strand-blocks (StrictInverse refusing to tombstone a minted object still referenced by a live non-member, e.g. a staff with a surviving instance) reuse `ConflictKind::TransactionConflict`. Decide whether undo refusals deserve their own conflict kind. | G / Pass 12 (undo) | | P12-K11 | `epiphany-ops` K | An undo's value restorations enter the write chains as ordinary writes by the undo op, so a second undo of the same transaction sees the first as a superseding writer (Conflicted/skip) while absence-restorations repeat idempotently — a documented asymmetry. Decide whether chain writes need distinguished undo provenance so repeated undo is uniformly idempotent. | G / Pass 12 (undo) | -| P12-I7 | `epiphany-engrave` I | Page geometry has no graph home: the spec names `Canvas.layout_defaults` ("paper size, margins") but defines no type, and adding a `Canvas` field is a schema-major change under the companion's frozen-layout rule. Casting-off therefore uses an engraver-side `PageGeometry` default (A4 at an 8mm staff: 105x148.5 ss, 7.5 ss margins, documented arithmetic). Define `CanvasLayoutDefaults` with the data-model schema-major; until then solvers MAY default. | G (graph model, schema-major) | +| P12-I7 **(IN PROGRESS — schema-major-1 track)** | `epiphany-engrave` I | Page geometry had no graph home: the spec named `Canvas.layout_defaults` ("paper size, margins") but defined no type, and adding a `Canvas` field is a schema-major change under the companion's frozen-layout rule. Casting-off therefore uses an engraver-side `PageGeometry` default (A4 at an 8mm staff: 105x148.5 ss, 7.5 ss margins). **Phase A of the schema-major-1 bump defines the type** (`CanvasLayoutDefaults { page_size: CanvasSize, margins: CanvasMargins }` in staff spaces, A4/8mm default) and ratifies its v1 wire form (Binary Format 0.3.0); the **graph home lands in code in Phase C** (`Canvas` gains the field), after which the engraver reads it (Phase C′) — until fully landed, solvers MAY default. | G (graph model, schema-major) | | P12-I8 | `epiphany-engrave` I | Break-constraint satisfaction predicate: implemented as "a `SystemBreakAt`/`PageBreakAt` is satisfied iff the final layout starts a system/page at that slot" (a region-first slot is trivially satisfied). Ch7/Ch9 never define satisfaction for break constraints; ratify the predicate. | G / Pass 12 (solver) | | P12-I9 | `epiphany-layout-ir` I | Honouring a user break must attribute the decision to its override (`DecisionSource::UserOverride(id)`), but constraints carry no override identity; implemented via a `ConstrainedLayoutIR.break_origins` sidecar populated by `to_constrained`. Bless the sidecar or widen the normalized constraint record. | G / Pass 12 (solver) | | P12-I10 | `epiphany-layout-ir` I | System-spanning strokes split at system boundaries need synthesized provenance for continuation segments; implemented as `SynthesisKind::Registered(SYSTEM_CONTINUATION_SYNTHESIS)` with a deterministic `(original, ordinal)` instance key. Add a first-class continuation synthesis kind or bless the registered id. | G / Pass 12 (provenance) | diff --git a/spec/binary_format.pdf b/spec/binary_format.pdf index ba7c6f1..b032b39 100644 Binary files a/spec/binary_format.pdf and b/spec/binary_format.pdf differ diff --git a/spec/binary_format.tex b/spec/binary_format.tex index cfb90b0..7edfd2c 100644 --- a/spec/binary_format.tex +++ b/spec/binary_format.tex @@ -235,7 +235,7 @@ {\Large\scshape\color{epiphanyslate}Binary Format}\\[6pt] {\large\itshape\color{epiphanyslate}A companion to the Core Specification}\\[14pt] {\color{epiphanygold}\rule{3in}{0.8pt}}\\[24pt] - {\normalsize\color{epiphanyink}Version 0.2.0 --- Phase 2/3 (canonical wire format: primitives through bundle physical layout + K0 and Phase-3-tranche payload framing)}\\[4pt] + {\normalsize\color{epiphanyink}Version 0.3.0 --- Phase 2/3 (canonical wire format: primitives through bundle physical layout + K0 and Phase-3-tranche payload framing; schema major~1 data-model expansion)}\\[4pt] {\small\color{epiphanyslate}Normative for the byte layouts it defines} \vfill \end{titlepage} @@ -395,7 +395,10 @@ which. \texttt{ResolvedLayoutIR} canonical output (Chapter~\ref{ch:noncanon}). & Little-endian integers; \texttt{u64} little-endian counts and length - prefixes. Deliberate, golden-locked divergence from regime (a). \\ + prefixes --- a golden-locked divergence from regime (a). Schema major~1 + moves the \emph{resolved-layout} surface to \texttt{u32}; the + manifest-embedded barrier blobs stay \texttt{u64} + (Section~\ref{sec:evolution:major1}). \\ (c) & Identifiers and hash-derived forms: the 128-bit identifier family, \texttt{ReplicaId}, \texttt{OperationId}, \texttt{ConflictId}, the \texttt{TypedObjectId} discriminant, digest truncations @@ -406,14 +409,20 @@ which. \end{longtable} \begin{rationale} -Regimes (a) and (b) differ only in prefix width, and the divergence is -\emph{tolerated rather than repaired}: the two surfaces were golden-locked -independently (the barrier blobs and the resolved-layout output carry their -own literal-byte anchors), they never embed one another's framing, and -unifying the prefix width now would break locked bytes for zero benefit. The -divergence is recorded as an open question for the next schema-major revision -(Chapter~\ref{ch:evolution}). Regime (c) is not a divergence at all: an -identifier's byte form \emph{is} its sort key, so big-endian is load-bearing. +In schema major~0, regimes (a) and (b) differ only in prefix width, and the +divergence was \emph{tolerated rather than repaired}: the two surfaces were +golden-locked independently (the barrier blobs and the resolved-layout output +carry their own literal-byte anchors), they never embed one another's framing, +and unifying the prefix width within major~0 would break locked bytes for zero +benefit. Once a major bump was scheduled for data-model reasons, part of the +unification became free to carry: schema major~1 moves the resolved-layout +surface (an independent non-canonical \texttt{LayoutCache}) to regime (a)'s +\texttt{u32} prefixes and re-locks its goldens. The barrier blobs cannot follow +in the same step --- they are carried opaquely inside the \emph{canonical} +manifest, which stays major~0 --- so they keep regime (b) until a manifest- +revising major (Chapter~\ref{ch:evolution}). Regime (c) is not a divergence at +all: an identifier's byte form \emph{is} its sort key, so big-endian is +load-bearing. \end{rationale} \section{Primitive Composition Rules} @@ -2078,8 +2087,13 @@ conservative treatment of unknown extensions --- are core Chapter~8 bytes only. These layouts are \textbf{regime (b)}: all counts and length prefixes are -\texttt{u64} little-endian --- a deliberate, golden-locked divergence from -the \texttt{u32} regime (Section~\ref{sec:conventions:regimes}). +\texttt{u64} little-endian --- a deliberate, golden-locked divergence from the +\texttt{u32} regime (Section~\ref{sec:conventions:regimes}). They \textbf{keep +this form in schema major~1}: unlike the resolved-layout surface, these blobs +are carried \emph{opaquely inside the canonical manifest} +(Section~\ref{sec:bundle:manifest}), which stays major~0, so a major-0 reader +must continue to parse them as \texttt{u64}. Their unification waits for a +future major that revises the manifest (Section~\ref{sec:evolution:major1}). \section{Framing} \label{sec:barriers:framing} @@ -2202,7 +2216,9 @@ The core specification delegates schema evolution to this document (core Chapter~8, \sectionsc{Schema Versioning}: \emph{``Schema evolution is governed by the Binary Format companion specification, which defines the wire encoding for each schema version''}). This chapter defines the wire -rules for schema major~0. +rules for schema evolution. Chapters~\ref{ch:values}--\ref{ch:barriers} +specify the schema-major-0 layouts; Section~\ref{sec:evolution:major1} +specifies the schema-major-1 delta and the migration between them. \section{The Chunk-Level Gate} \label{sec:evolution:gate} @@ -2212,7 +2228,13 @@ Every chunk declares a \texttt{SchemaVersion} (major, minor) in its \begin{itemize} \item \textbf{Major} = incompatible. A reader \MUST{} reject a chunk whose - schema major it does not support. The current major is~\tablenums{0}. + schema major it does not support. A reader supports a contiguous accept-set + $[\textsc{min}, \textsc{max}]$ of majors and rejects any chunk whose major + falls \emph{outside} $[\textsc{min}, \textsc{max}]$ --- too new (above + \textsc{max}) or, once \textsc{min} rises past a retired major, too old + (Section~\ref{sec:evolution:major1}). Two majors are defined: + \tablenums{0} and \tablenums{1}; the reference implementation's accept-set + is $\{0, 1\}$ ($\textsc{min} = 0$, $\textsc{max} = 1$). \item \textbf{Minor} = additive. v0 readers verify the major only; the minor is a \emph{record}, not a gate --- but it is a mandatory record: a writer \MUST{} raise the chunk schema minor when it emits any @@ -2232,6 +2254,15 @@ fourteenth field within major~0}. The manifest is a positional struct whose decoder rejects trailing bytes, so manifest-body extension is major-gated in v0 exactly like every other struct. +Adding a field is a \textsc{major} change \emph{regardless of its type}: an +\texttt{Option} field is not ``optional'' in the wire sense --- it still +occupies a new positional slot (a presence byte, then the payload when +present), which shifts every subsequent field and rejects under the +trailing-bytes rule. There is no ``downgrade to minor'' for an \texttt{Option} +addition. The first exercise of this is schema major~1 +(Section~\ref{sec:evolution:major1}), which adds \texttt{Instrument.range} +(an \texttt{Option}) among other fields. + The \emph{only} minor-additive mechanism in schema major~0 is \textbf{appending discriminants to open vocabularies}: @@ -2273,14 +2304,181 @@ Operation Catalog (\sectionsc{v0 $\rightarrow$ v1 Payload Migration}) because it was semantic rather than byte-level (Section~\ref{sec:ops:v0}); byte-level migrations belong here. -\begin{openquestion} -Whether the \texttt{u64}/\texttt{u32} length-prefix divergence between -regimes (a) and (b) (Section~\ref{sec:conventions:regimes}) gets unified at -the next schema major. Unification would simplify decoders and cost one -coordinated re-lock of the barrier-blob and resolved-layout goldens; -leaving it is free but permanent. Decide when the first major bump is -scheduled for other reasons --- the divergence alone does not justify one. -\end{openquestion} +The \texttt{u64}/\texttt{u32} length-prefix divergence between regimes (a) and +(b) (Section~\ref{sec:conventions:regimes}), left open in earlier revisions for +a future major, is \textbf{partly resolved in schema major~1} +(Section~\ref{sec:evolution:major1}): the first major bump, scheduled for +data-model reasons, carries the unification for the independent non-canonical +resolved-layout surface. The barrier/extension blobs remain regime~(b) because +they ride the canonical manifest, which this bump keeps at major~0; their +unification stays open for a manifest-revising major. + +\section{Schema Major 1} +\label{sec:evolution:major1} + +Schema major~1 is the first data-model expansion major, defined here in full: +the changed value layouts, the length-prefix unification, and the +byte-for-byte migration from major~0. It bundles every change that was +deferred to ``the next major'' so that one coordinated migration discharges +them together. + +\subsection{Where the changed fields reach} +The three added fields land in different chunk classes, and the major is +assigned \textbf{per payload type}, not per chunk kind: + +\begin{itemize} + \item \texttt{Canvas.layout\_defaults} and \texttt{Instrument.range} appear + \emph{only} in the acceleration-cache full-\texttt{Score} snapshot (a + \texttt{ChunkKind::Snapshot}). No operation payload embeds a + \texttt{Canvas} or an \texttt{Instrument} value (there is no + \texttt{CreateCanvas}/\texttt{CreateInstrument}; canvas is the inline + genesis singleton and instruments live in score genesis), so these two + are confined to that one \textbf{non-canonical} chunk. + \item \texttt{Region.permits\_spanning\_slurs} reaches the snapshot + \emph{and} the \textbf{canonical operation layer}: + \texttt{CreateRegion} embeds the full \texttt{Region} value + (Section~\ref{sec:ops:payload}), so its v1 payload carries the v1 + \texttt{Region} layout. An operation-envelope block containing a + \texttt{CreateRegion} therefore encodes v1 bytes and is stamped + \textbf{major~1}; a block with no such operation stays \textbf{major~0}. + Operation-envelope blocks are canonical, so this is a canonical + operation-layer change, not merely a cache change. + \item \texttt{ChunkKind::Snapshot} is itself payload-polymorphic: the same + kind carries \emph{either} an acceleration-cache full \texttt{Score} (as + above, stamped major~1) \emph{or} the canonical-base + \texttt{MaterializedState}, disambiguated by manifest role. The canonical + base embeds no \texttt{Canvas}, \texttt{Instrument}, or \texttt{Region} + value, so it is unchanged and stays \textbf{major~0}. Its content-hash + preimage (Section~\ref{sec:bundle:chunks}) includes the schema version, so + re-stamping the unchanged base at major~1 would change its chunk id and + churn every \texttt{SnapshotRef} for zero layout benefit. A writer + \MUSTNOT{} do so; a conformance test \SHOULD{} assert the canonical base + is byte-identical across the bump. +\end{itemize} + +\subsection{Cross-major reader behaviour} +Two rules follow from the chunk classes above and the core specification's +\sectionsc{Schema Versioning} canonical/non-canonical distinction: + +\begin{itemize} + \item \textbf{Non-canonical chunks --- discard and regenerate.} The layout + caches and the acceleration snapshot are non-canonical. A reader meeting a + foreign-major one \MAY{} discard and regenerate it rather than decode it: a + major-1 reader regenerates a major-0 layout cache instead of carrying a + frozen old-width decoder, and it migrates a major-0 acceleration + \texttt{Score} snapshot on read (cheaper than replaying the operation log). + \item \textbf{Canonical chunks --- parse or open read-only.} The manifest and + the canonical base stay major~0 and are always parseable. A major-1 op + block is canonical: a major-0-only reader \MUSTNOT{} discard it (that would + fork canonical state) --- it opens the bundle in read-only preservation + mode, having read the major-0 base and manifest but being unable to replay + the v1 \texttt{CreateRegion} operations. A major-1 reader migrates such a + block on read (below). +\end{itemize} + +So a major-0-only reader opens a major-1 bundle \emph{fully} only when the +bundle contains no v1 \texttt{CreateRegion} operation; otherwise it opens +read-only. It always reads the canonical base and manifest, and always +discards the higher-major non-canonical caches. + +\subsection{The accept-set gate} +A reader supports a contiguous set of majors $[\textsc{min}, \textsc{max}]$ +(the reference implementation: $\{0, 1\}$, i.e. $\textsc{min} = 0$, +$\textsc{max} = 1$). The chunk-level gate +(Section~\ref{sec:evolution:gate}) rejects a chunk whose major falls outside +$[\textsc{min}, \textsc{max}]$ --- too new above \textsc{max}, or too old +below \textsc{min} once a reader drops support for a retired major; it no +longer rejects on inequality with a single supported major. + +\subsection{Changed and new value layouts} +These extend Chapter~\ref{ch:values}'s regime~(a) rules. As there, \textbf{the +wire form is the reference implementation's struct layout} --- which is a +reduced subset of the core specification's fuller data model (the code's +\texttt{Instrument} carries a subset of the model's fields, etc.). The wire +form ratifies the code, not the model; a struct's v1 layout \textbf{appends} +its new field(s) after its existing major-0 fields: + +\begin{itemize} + \item \texttt{Canvas} $=$ \texttt{regions} \cat{} \texttt{layout\_defaults}, + where \texttt{CanvasLayoutDefaults} $=$ \texttt{page\_size} + (\texttt{CanvasSize} $=$ \texttt{width} \cat{} \texttt{height}, two + \texttt{CanonicalF64} leaves) \cat{} \texttt{margins} + (\texttt{CanvasMargins} $=$ four \texttt{CanonicalF64} leaves: top, + right, bottom, left). + \item \texttt{Instrument} $=$ \texttt{id} \cat{} \texttt{name} \cat{} + \texttt{range} (\texttt{Option}: a presence byte, then for + \texttt{Some} a \texttt{PitchRange} $=$ \texttt{lowest} \cat{} + \texttt{highest}, each a \texttt{Pitch} under this chapter's rules). + \item \texttt{Region} appends \texttt{permits\_spanning\_slurs}, one bare + bool byte (\tablenums{0}/\tablenums{1}), after its major-0 fields. + \item \textbf{Operation layer:} the \texttt{CreateRegion} payload embeds a + length-prefixed \texttt{Region} (Section~\ref{sec:ops:payload}), so its v1 + form carries the v1 \texttt{Region} layout (the appended bool inside the + embedded value). This is the one \emph{canonical} byte change in major~1; + the operation envelope's own field order is unchanged, only its embedded + \texttt{Region} grows. +\end{itemize} +Every embedded leaf keeps this chapter's framing (a \texttt{CanonicalF64} is a +\texttt{u32} LE length \cat{} 8 bytes $= 12$; a bool is one bare byte). + +\subsection{Length-prefix unification (partial)} +Regime~(b) (Section~\ref{sec:conventions:regimes}) is \emph{narrowed}, not +fully retired, in major~1. The \texttt{ResolvedLayoutIR} output --- an +independent, non-canonical \texttt{LayoutCache} chunk --- moves from +\texttt{u64} to \texttt{u32} LE counts and length prefixes, matching +regime~(a), and its golden anchors re-lock in the same step. Because it is +non-canonical, a major-1 reader regenerates a major-0 resolved-layout cache +rather than decoding its \texttt{u64} form --- no frozen \texttt{u64} decoder +is required. + +The extension-declaration and edit-barrier blobs (Chapter~\ref{ch:barriers}) +\textbf{stay regime~(b)}: they are carried opaquely inside the canonical +manifest (Section~\ref{sec:bundle:manifest}), which stays major~0, so a +major-0 reader must keep parsing them as \texttt{u64}. Unifying them would +require bumping the manifest itself, which this data-model bump deliberately +avoids; that unification waits for a manifest-revising major. + +\subsection{Migration from major~0} +The v0${\to}$v1 translation is total and default-filling: every new field has +a canonical default, so no value is unrecoverable --- including the +\texttt{CreateRegion} operation payload, whose embedded \texttt{Region} +default-fills structurally. This is unlike the \emph{semantic}, +context-dependent operation-payload migration of Section~\ref{sec:ops:v0} +(which could be irreversible); the major-1 migration needs no score context. + +\begin{center} +\begin{tabular}{p{1.9in} p{3.4in}} + \toprule + \textbf{major-0 form} & \textbf{major-1 form} \\ + \midrule + \texttt{Canvas} $=$ \texttt{regions} & + append \texttt{layout\_defaults} $=$ the A4/8\,mm default (page + $105 \times 148.5$, margins $7.5$ staff spaces) \\ + \texttt{Instrument} $=$ \texttt{id}, \texttt{name} & + append \texttt{range} $=$ \texttt{None} (presence byte \tablenums{0}) \\ + \texttt{Region} $= \ldots$ (snapshot) & + append \texttt{permits\_spanning\_slurs} $=$ \tablenums{0} + (\texttt{false}) \\ + \texttt{CreateRegion} op payload & + default-fill the embedded \texttt{Region}'s appended field + (\texttt{permits\_spanning\_slurs} $=$ \tablenums{0}); \emph{canonical} --- + a v0 op block migrates on read, and a block bearing a v1 \texttt{CreateRegion} + is major~1 (a major-0-only reader opens the bundle read-only) \\ + \texttt{ResolvedLayoutIR} \texttt{u64} prefixes & + non-canonical \texttt{LayoutCache}: discard and regenerate at major~1 + under \texttt{u32} (no in-place byte translation) \\ + barrier / extension-declaration blobs & + stay regime~(b) \texttt{u64} (manifest-embedded, manifest stays major~0); + unchanged in this bump \\ + canonical-base \texttt{MaterializedState} & + unchanged, byte-identical, stays major~0 \\ + \bottomrule +\end{tabular} +\end{center} + +A reader migrates a major-0 acceleration \texttt{Score} snapshot on read +(cheaper than replaying the operation log); a writer emits only major-1 forms +for the changed payloads and leaves the canonical base at major~0. % =========================================================================== \chapter{Non-Canonical Pinned Encodings} @@ -2321,9 +2519,13 @@ $\texttt{LayoutObjectId} = \mathrm{trunc128}(\mathrm{BLAKE3}( The resolved layout's canonical bytes are the \textbf{byte-equal conformance surface} of core Chapter~7: two conforming engravers given the same score, -profile, and glyph catalog must produce identical bytes. The encoding is -regime (b) --- \texttt{u64} LE counts and length prefixes --- with every -geometric coordinate quantized to the $1/1024$ staff-space grid as a +profile, and glyph catalog must produce identical bytes. In schema major~0 the +encoding is regime (b) (\texttt{u64} LE counts and length prefixes); schema +major~1 moves it to regime (a)'s \texttt{u32} prefixes with the rest of the +unification (Section~\ref{sec:evolution:major1}), re-locking this surface's +goldens. Because the surface is non-canonical, a reader regenerates a +foreign-major layout cache rather than decoding it. Either way every geometric +coordinate is quantized to the $1/1024$ staff-space grid as a \texttt{QuantizedCoord} (8 LE bytes; Chapter~\ref{ch:primitives}); a non-finite or out-of-range coordinate is a determinism violation and \MUST{} be rejected, never normalized. The top-level section order is: @@ -2556,6 +2758,23 @@ layouts of Section~\ref{sec:values:representative}. assignment changed. Semantics: Operation Catalog 0.5.0 (\sectionsc{CreateStaff}, \sectionsc{Meter and Tempo Overwrites}, \sectionsc{SetStaffLayout}, and the value-restoring \sectionsc{UndoTransaction} revision). \\ + \today & Schema evolution / Graph value layouts & 0.3.0 --- Defines + \textbf{schema major~1}, the first data-model expansion major + (Section~\ref{sec:evolution:major1}): appends \texttt{Canvas.layout\_defaults} + (new \texttt{CanvasLayoutDefaults}/\texttt{CanvasSize}/\texttt{CanvasMargins} + layouts, snapshot-only), \texttt{Instrument.range} (\texttt{Option}, + snapshot-only), and \texttt{Region.permits\_spanning\_slurs} (also inside the + \emph{canonical} \texttt{CreateRegion} operation payload); narrows regime~(b) + by unifying the non-canonical resolved-layout length prefixes to \texttt{u32} + (the manifest-embedded barrier blobs stay \texttt{u64}, partly resolving the + standing open question); pins per-payload-type major assignment (the + canonical-base \texttt{MaterializedState} stays major~0, byte-identical), + the accept-set gate ($[\textsc{min},\textsc{max}]$), the cross-major reader + rules (discard-and-regenerate non-canonical chunks; parse-or-read-only for + canonical ones, so a major-0 reader opens a bundle with v1 \texttt{CreateRegion} + ops read-only), and the total default-filling v0${\to}$v1 migration table. + Clarifies that any field add is a major change regardless of + \texttt{Option}-ness. \\ \bottomrule \end{longtable} diff --git a/spec/core_spec.pdf b/spec/core_spec.pdf index 7894d78..1947860 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 b6454a2..528d66c 100644 --- a/spec/core_spec.tex +++ b/spec/core_spec.tex @@ -883,6 +883,27 @@ the other in general. storage makes the pitch self-describing. \end{rationale} +\begin{lstlisting}[language=Rust] +/// A closed range of pitches, used for advisory declarations: an +/// instrument's playable compass and an indeterminate event's pitch hint. +/// Both bounds are full pitches. +pub struct PitchRange { + pub lowest: Pitch, + pub highest: Pitch, +} +\end{lstlisting} + +A \texttt{PitchRange} is \emph{advisory}. Whether a given pitch lies within +it is decidable only when the pitch and both bounds share a pitch space that +defines an order (the common CMN case); cross-space or partially-ordered +comparisons are treated as ``not out of range'' --- a sound-but-incomplete +check, as with region time overlap. A range is well-formed when +\texttt{lowest} does not sort above \texttt{highest} in a common ordered +space. A solver \MAY{} flag an out-of-range pitch but \MUSTNOT{} treat the +range as a hard constraint. The type is defined with schema major~1, closing +the gap in which \texttt{IndeterminacyHints} and \texttt{Instrument} named a +\texttt{PitchRange} that no section defined. + \section{Scale Position} \label{sec:pitch:scale-position} @@ -4228,8 +4249,40 @@ pub struct Canvas { /// system layout, page-break behavior. pub layout_defaults: CanvasLayoutDefaults, } + +/// Canvas-level layout defaults. Page size and margins are in staff +/// spaces; a solver reads them as its default page geometry and MAY +/// override per solve. The type is defined with schema major 1 (its field +/// was named on the canvas from the first draft; only its type was +/// undefined). +pub struct CanvasLayoutDefaults { + pub page_size: CanvasSize, + pub margins: CanvasMargins, +} + +/// A page size in staff spaces (1 staff space = staff height / 4). +pub struct CanvasSize { + pub width: f64, + pub height: f64, +} + +/// Page margins in staff spaces. +pub struct CanvasMargins { + pub top: f64, + pub right: f64, + pub bottom: f64, + pub left: f64, +} \end{lstlisting} +The default is \textbf{A4 portrait at an 8\,mm staff} (1 staff space = +2\,mm): page $105 \times 148.5$ staff spaces, $7.5$-staff-space margins, +hence a $90 \times 133.5$ content area. Each staff-space scalar is realized +canonically as a \texttt{CanonicalF64} leaf; the reference engraver maps a +\texttt{CanvasLayoutDefaults} onto its internal page geometry. This closes +the long-standing gap in which the canvas named \texttt{layout\_defaults} but +no chapter defined \texttt{CanvasLayoutDefaults}. + \subsection{Regions} \begin{lstlisting}[language=Rust] @@ -4254,6 +4307,12 @@ pub struct Region { /// Optional tempo and tuning overrides scoped to this region. pub local_tempo_map: Option, pub local_tuning_overrides: Vec, + + /// Whether spanners (slurs first of all) may cross this region's + /// boundary into an adjacent region. Default false: cross-region + /// spanning is not permitted unless a region opts in. Added with + /// schema major 1. + pub permits_spanning_slurs: bool, } pub enum RegionContent { @@ -10452,9 +10511,17 @@ pub struct SchemaVersion { \item Minor version changes are backward-compatible. A reader supporting major version $N$ \MUST{} accept any minor version $N.\textit{m}$ for $m \leq$ the reader's supported minor. - Minor version changes \MUST{} only add optional fields or - enumeration variants in a way preserving the wire format of - prior values. + Under the positional, frozen-layout wire form, a minor change + \MUST{} only \emph{append discriminants to the append-safe + vocabularies the Binary Format companion defines} (its open + \texttt{Registered} vocabularies, plus ratified appends to + \texttt{OperationKind}, \texttt{OperationKindTag}, + \texttt{OperationPayload}, and the closed value-layer unions), which + leaves the bytes of every prior value untouched; it \MUSTNOT{} add or + reorder a struct field --- adding a field, even an \texttt{Option}, + occupies a new positional slot and shifts subsequent bytes, so it is a + \emph{major} change (Binary Format companion, + \sectionsc{What ``Additive'' Means Here}). \item The manifest's \texttt{manifest\_schema\_version} declares the schema of the manifest itself. Each chunk's \texttt{schema\_version} field declares the schema of that @@ -10465,6 +10532,26 @@ pub struct SchemaVersion { \end{itemize} \end{requirement} +Schema major~1 is the first data-model expansion major: it adds +\texttt{Canvas.layout\_defaults}, \texttt{Instrument.range}, and +\texttt{Region.permits\_spanning\_slurs} to the graph, and unifies the +non-canonical resolved-layout length-prefix width. Its wire form and the +byte-for-byte migration from major~0 are defined in the Binary Format +companion. The canonical-base \texttt{MaterializedState} embeds none of these +values, so it is byte-identical across the bump and stays major~0. +\texttt{Canvas.layout\_defaults} and \texttt{Instrument.range} reach only the +non-canonical acceleration (full-\texttt{Score}) snapshot; but +\texttt{Region.permits\_spanning\_slurs} also reaches the \emph{canonical} +operation layer, because \texttt{CreateRegion} embeds a full \texttt{Region}, +so an operation-envelope block bearing a v1 \texttt{CreateRegion} is major~1. +Consequently a major-0-only reader opens a major-1 bundle \emph{fully} only +when the bundle carries no v1 \texttt{CreateRegion} operation; otherwise it +reads the major-0 canonical base and manifest but opens read-only (it cannot +replay the v1 canonical operations), per the canonical/non-canonical rules +above. It always discards the higher-major non-canonical caches. A major-1 +reader migrates a major-0 acceleration snapshot and a major-0 +\texttt{CreateRegion} payload on read (default-filling the new fields). + \section{Format Profiles} \label{sec:format:profiles} @@ -14578,6 +14665,23 @@ layouts they own versus inherit: score/canvas slots remain deliberately unavailable pending an addressable root model (P12-K8). \\ + \today & Schema major 1 (data-model expansion) & + Defines the two referenced-but-undefined types + \texttt{CanvasLayoutDefaults} (with \texttt{CanvasSize} / + \texttt{CanvasMargins}, staff-space page geometry, A4/8\,mm default; + closes P12-I7) and \texttt{PitchRange} (advisory pitch compass, used by + \texttt{Instrument.range} and \texttt{IndeterminacyHints}); adds + \texttt{Region.permits\_spanning\_slurs} (default false, P12-K7). Records + that schema major~1 is the first data-model expansion major: the + canonical-base \texttt{MaterializedState} embeds none of these values and + stays major~0, byte-identical, but \texttt{Region.permits\_spanning\_slurs} + reaches the \emph{canonical} \texttt{CreateRegion} operation payload, so a + major-0 reader opens a bundle carrying v1 \texttt{CreateRegion} ops + read-only. The wire form, the resolved-layout length-prefix unification + (the manifest-embedded barrier blobs stay \texttt{u64}), and the + byte-for-byte v0${\to}$v1 migration are ratified in the Binary Format + companion (0.2.0 $\rightarrow$ 0.3.0). + \\ \bottomrule \end{longtable} diff --git a/spec/quality_metric_catalog.pdf b/spec/quality_metric_catalog.pdf index a709806..ce05f1c 100644 Binary files a/spec/quality_metric_catalog.pdf and b/spec/quality_metric_catalog.pdf differ diff --git a/spec/quality_metric_catalog.tex b/spec/quality_metric_catalog.tex index 6422fd0..3c006e6 100644 --- a/spec/quality_metric_catalog.tex +++ b/spec/quality_metric_catalog.tex @@ -515,11 +515,12 @@ assembles its \texttt{SolveReport}: \item the solve's constrained input $C$ (a \texttt{ConstrainedLayoutIR}: horizontal spring slots, vertical bands, declared constraints); \item the declared page geometry the solve was configured with: the - content width $W$ and content height $H$, in staff spaces. (The score - graph has no home for page geometry yet --- the core names - \texttt{Canvas.layout\_defaults} without defining it, tracked as - Pass-12 row P12-I7 --- so the geometry is a solver parameter, and the - Reference Suite companion requires each suite entry to declare it.) + content width $W$ and content height $H$, in staff spaces. (Schema + major~1 defines \texttt{Canvas.layout\_defaults} and its type + \texttt{CanvasLayoutDefaults}, P12-I7; the reference implementation's + code graph home lands in a later phase, so until then the geometry is a + solver parameter, and the Reference Suite companion requires each suite + entry to declare it.) \end{enumerate} Notation used throughout Chapter~\ref{ch:metrics}: diff --git a/spec/reference_suite.pdf b/spec/reference_suite.pdf index 4cac66a..f4c54c8 100644 Binary files a/spec/reference_suite.pdf and b/spec/reference_suite.pdf differ diff --git a/spec/reference_suite.tex b/spec/reference_suite.tex index 26e0964..99ea4a0 100644 --- a/spec/reference_suite.tex +++ b/spec/reference_suite.tex @@ -410,10 +410,10 @@ under: documented default --- A4 portrait at an 8\,mm staff height: page $105 \times 148.5$ staff spaces, margins $7.5$ staff spaces on all four sides, hence a content area of $90 \times 133.5$ staff spaces. - (The score graph has no home for page geometry yet: the core names - \texttt{Canvas.layout\_defaults} without defining a type, tracked as - Pass-12 row P12-I7, so geometry is declared per entry as a solver - parameter.) + (Schema major~1 defines \texttt{Canvas.layout\_defaults} and its type + \texttt{CanvasLayoutDefaults} (P12-I7); the reference implementation's + code graph home lands in a later phase, so until then geometry is + declared per entry as a solver parameter.) \item the \textbf{solver configuration}: the \texttt{SolverConfig} fields. In v0.1 every entry uses the default configuration --- the \texttt{Standard} profile, an unbounded deterministic budget, and