Pass 11: ratify provisional byte choices into normative spec text

Spec-revision pass (architecture unchanged) converting the v0
implementation's provisional, golden-locked choices into ratified
core_spec.tex text, so durable byte layouts are fixed before the
next-phase build-outs. Worklist: spec/PASS11_WORKLIST.md; per-item
dispositions: spec/PASS11_RATIFICATION_LOG.md.

Adopt-and-pin (bytes): TypedObjectId 16-bit BE discriminant table
0..=27 (added the 5 variants the code carried); promoted-voice
(MUSCSVCE), synthetic-pitch (MUSCSPCH, tuning always in identity),
and integrity-anomaly (MUSCSANM, now a reserved built-in tag)
derivations; ChunkKind/ProfileId/CompressionAlgorithm discriminants;
ManifestId preimage (manifest_id excluded); RationalTime/scalar
layouts + the codec convention baseline the Binary Format companion
inherits.

Decide-and-pin: tempo Linear interpolates speed (not bpm);
StructuralFieldCollision tags the winner Conflicted; lifted the
>2-way / partial-overlap voice-promotion rule to normative; pinned
TransactionCategory and ObjectKind core vocabularies; added
ResolutionAction::Dismiss so the Dismissed state is reachable by an
authored op; pinned the (non-canonical) LayoutObjectId derivation
(MUSCLOID).

Fixes: blob hashing is bare MUSCBLOB||payload (deleted the
contradictory "identically to chunks" phrasing); equal-generation
superblock rule (DivergentSameGeneration); defined ProfileConstraints
with the required RetentionPolicy + first-declared precedence; made
the DVV zero-based floor normative; reconciled the invariant count to
19 and named the three construction-time MUSTs — TupletRatio now
rejects degenerate ratios at construction (zero term or
actual==notated), enforced by a checked constructor + codec decode
validation.

Code changes carry regression tests; byte-layout golden tests now
cite their ratified requirements. Workspace green: cargo test, clippy
-D warnings, fmt; spec builds (lualatex, 253pp). Per-crate DECISIONS
files annotated with the ratification status.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011giSRaHCFCGm1Z2SWv6JHt
This commit is contained in:
Levi Neuwirth 2026-06-21 22:30:06 -04:00
parent 691f527e07
commit b2f2e204a7
27 changed files with 1268 additions and 127 deletions

8
.gitignore vendored
View File

@ -3,3 +3,11 @@
# These are tracked once they exist (kept for reproducible builds):
# Cargo.lock for the workspace
# LaTeX build intermediates (the spec PDF itself is tracked; these are not).
spec/*.aux
spec/*.fdb_latexmk
spec/*.fls
spec/*.log
spec/*.out
spec/*.toc

View File

@ -7,6 +7,16 @@ than improvised in code (QUICKSTART, Process notes: *"Ambiguities go into a
batch, not into code … Don't open Pass 11 until you have at least three such
items batched."*).
> **RATIFIED (Pass 11, 2026-06-21).** The bundle-layer Pass 11 candidates have
> been ratified into `core_spec.tex` — see `spec/PASS11_RATIFICATION_LOG.md`.
> Highlights: D4 adopted (ChunkKind/ProfileId/CompressionAlgorithm discriminants);
> D5 adopted (`ManifestId` preimage, with `manifest_id` excluded); D1 fixed
> (equal-generation superblock rule → `DivergentSameGeneration`); D3 fixed (blob
> hashing is bare `MUSCBLOB‖payload`, spec contradiction removed); D6 fixed
> (`ProfileConstraints` defined with the required `RetentionPolicy`,
> first-declared multi-profile precedence). D2 (Binary Format companion) stays
> Track B, with the convention baseline ratified by core item 1.8.
## Implementation decisions (QUICKSTART "Decisions you'll need to make")
1. **Replica ID entropy source** — N/A to this crate (Agent B/`epiphany-core`).

View File

@ -40,7 +40,9 @@ pub enum ChunkKind {
impl ChunkKind {
/// The stable 1-byte discriminant, assigned by the spec's declaration order.
/// Part of the hash preimage and of the chunk-reference canonical order, so
/// the assignment is normative for this format version.
/// the assignment is normative for this format version. RATIFIED by Pass 11
/// (item 1.5, P11-D4): core_spec §"Chunks",
/// Requirement `req:format:chunkkind-discriminants` (0..=8).
#[inline]
pub const fn discriminant(self) -> u8 {
match self {

View File

@ -97,7 +97,10 @@ impl ManifestId {
/// the document id, generation, and the canonical bytes of the manifest
/// body (with the `manifest_id` field itself excluded to avoid a circular
/// dependency). Deterministic — two writers encoding the same manifest
/// content derive the same id.
/// content derive the same id. RATIFIED by Pass 11 (item 1.6, P11-D5):
/// core_spec §"Manifest Encoding", Requirement `req:format:manifest-id`
/// (`trunc128(BLAKE3("MUSCMNIF" || document_id || generation || body))`,
/// body excluding `manifest_id`).
pub(crate) fn derive(document_id: DocumentId, generation: u64, body_preimage: &[u8]) -> Self {
let mut p = Preimage::new(DomainTag::MANIFEST_ID);
p.push_bytes(document_id.as_bytes());

View File

@ -290,7 +290,10 @@ impl Superblock {
/// version, same profile. The advisory `commit_timestamp` (and the physical
/// manifest offset/length, which a matching `manifest_hash` already pins) are
/// deliberately excluded — a difference in those does not make the states
/// divergent.
/// divergent. RATIFIED by Pass 11 (item 3.2, P11-D1, a spec-gap fix): core_spec
/// §"Superblock Selection" now states the equal-generation rule (this
/// load-bearing field set → equivalent, pick A; otherwise
/// `DivergentSameGeneration`, read-only).
fn selection_equivalent(a: &Superblock, b: &Superblock) -> bool {
a.manifest_hash == b.manifest_hash
&& a.manifest_schema_version == b.manifest_schema_version

View File

@ -38,6 +38,17 @@ operation so demotion is never observable.
## Pass 11 candidates (ambiguities for the spec, not resolved in code)
> **RATIFIED (Pass 11, 2026-06-21).** The spec-internal items below have been
> ratified into normative `core_spec.tex` text — see
> `spec/PASS11_RATIFICATION_LOG.md`. Disposition summary: P11-1/3/6 adopted as
> golden (TypedObjectId discriminants, promoted-voice + synthetic-pitch
> derivations); P11-2 fixed (count = 19; three construction-time MUSTs named, and
> `TupletRatio` now rejects degenerate ratios at construction); P11-4 adopted
> (RationalTime/scalar layouts + codec convention baseline the Binary Format
> companion inherits); P11-7 decided (tempo `Linear` interpolates speed). P11-5
> remains a scope boundary (Track C). The byte-layout golden tests now cite their
> ratified requirements.
### P11-1 — `TypedObjectId` discriminant values are unspecified
Chapter 5 fixes the *shape* of `TypedObjectId::canonical_bytes` ("a 16-bit

View File

@ -28,7 +28,11 @@
//!
//! This is a concrete, reversible canonical form that predates the Binary Format
//! companion specification; when that lands, reconcile the two (see
//! `DECISIONS.md`, P11-4).
//! `DECISIONS.md`, P11-4). The convention set above is RATIFIED by Pass 11
//! (item 1.8): core_spec §"Binary Format Companion",
//! Requirement `req:format:codec-conventions` blesses these conventions as the
//! companion's inherited baseline, so core/ops/bundle stay mutually consistent
//! until Agent J formalizes the full companion.
use core::num::NonZeroU16;
use std::collections::{BTreeMap, BTreeSet};
@ -1474,7 +1478,22 @@ struct_codec!(Slur {
end_event
});
struct_codec!(Beam { id, events, level });
struct_codec!(TupletRatio { actual, notated });
// TupletRatio is not a plain struct_codec!: it has private fields and a checked
// constructor that rejects degenerate ratios, so decode must validate too
// (a malformed bundle cannot inject a degenerate ratio).
impl Codec for TupletRatio {
fn enc(&self, out: &mut Vec<u8>) {
self.actual().enc(out);
self.notated().enc(out);
}
fn dec(r: &mut Reader<'_>) -> Result<Self> {
let actual = Codec::dec(r)?;
let notated = Codec::dec(r)?;
TupletRatio::new(actual, notated).ok_or(ScoreDecodeError::Reconstruct(
"TupletRatio: degenerate ratio",
))
}
}
struct_codec!(Tuplet {
id,
ratio,

View File

@ -286,10 +286,7 @@ pub fn valid_score_rich(seed: u64) -> Score {
let triplet_id: TupletId = idc.mint();
cross_cutting.tuplets.push(Tuplet {
id: triplet_id,
ratio: TupletRatio {
actual: 3,
notated: 2,
},
ratio: TupletRatio::new(3, 2).expect("3:2 is a valid tuplet ratio"),
members: triplet_members.clone(),
parent: None,
required_total: MusicalDuration(RationalTime::new(1, 4).unwrap()),
@ -678,10 +675,7 @@ pub fn violating_score(inv: GraphInvariant, seed: u64) -> Score {
let (e0, e1) = first_two_event_ids(&s);
s.cross_cutting.tuplets.push(Tuplet {
id: TupletId::new(replica, 1),
ratio: TupletRatio {
actual: 3,
notated: 2,
},
ratio: TupletRatio::new(3, 2).expect("3:2 is a valid tuplet ratio"),
members: vec![e0, e1],
parent: None,
required_total: MusicalDuration::whole(),

View File

@ -751,11 +751,43 @@ pub struct Tie {
pub class: TieClass,
}
/// The actual:notated ratio of a tuplet (Chapter 3 §"Tuplets").
/// The actual:notated ratio of a tuplet (Chapter 3 §"Tuplets"). Built only
/// through [`TupletRatio::new`], which rejects degenerate ratios at
/// construction (Chapter 3 §"Tuplets", `req:time:tuplet-ratio-construction`):
/// both terms must be nonzero and `actual != notated`, so a degenerate ratio is
/// never a representable graph state. The fields are private to keep that
/// guarantee unbypassable.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct TupletRatio {
pub actual: u32,
pub notated: u32,
actual: u32,
notated: u32,
}
impl TupletRatio {
/// Builds an `actual:notated` ratio, returning `None` for a *degenerate*
/// ratio — either term zero, or `actual == notated` (a ratio that expresses
/// no augmentation or diminution). This is the construction-time MUST of
/// Chapter 3 §"Tuplets": degeneracy is rejected here, not by a runtime
/// invariant.
pub fn new(actual: u32, notated: u32) -> Option<Self> {
if actual == 0 || notated == 0 || actual == notated {
None
} else {
Some(TupletRatio { actual, notated })
}
}
/// The `actual` term: how many notes are played.
#[inline]
pub const fn actual(&self) -> u32 {
self.actual
}
/// The `notated` term: in the time of how many.
#[inline]
pub const fn notated(&self) -> u32 {
self.notated
}
}
/// A tuplet grouping object (Chapter 3 §"Tuplets as Grouping Objects").
@ -959,8 +991,8 @@ impl NotatedComponent {
pub fn sounding_duration(&self, tuplet_ratio: Option<TupletRatio>) -> MusicalDuration {
let notated = self.notated_duration();
match tuplet_ratio {
Some(r) if r.actual != 0 && r.notated != 0 => {
let scale = crate::time::RationalTime::new(r.notated as i64, r.actual as i64)
Some(r) if r.actual() != 0 && r.notated() != 0 => {
let scale = crate::time::RationalTime::new(r.notated() as i64, r.actual() as i64)
.expect("validated nonzero");
MusicalDuration(notated.rational().mul(&scale))
}
@ -1217,9 +1249,10 @@ mod tests {
fn promoted_voice_id_byte_form_is_locked() {
// Golden: locks the MUSCSVCE 64-byte preimage layout (staff instance ||
// original voice || winning op || losing op, each 16 big-endian bytes)
// and the hash output. A change to the input order, byte layout, or
// domain tag breaks this deliberately, forcing the derivation change to
// be acknowledged (DECISIONS P11-3).
// and the hash output. RATIFIED by Pass 11 (item 1.2, P11-3 / ops C4):
// this is the spec's golden, normative in core_spec
// §"System-Promoted Voices" (`derive_promoted_voice_id`). A change to the
// input order, byte layout, or domain tag breaks this deliberately.
let id = derive_promoted_voice_id(
StaffInstanceId::new(ReplicaId(3), 1),
VoiceId::new(ReplicaId(3), 2),

View File

@ -889,9 +889,13 @@ mod tests {
#[test]
fn typed_object_id_byte_form_is_locked() {
// Golden: locks the 16-bit big-endian discriminant table (DECISIONS P11-1)
// and the payload layout. A reorder or discriminant reassignment breaks
// this deliberately — these bytes are normative (ordering/hashing/equality).
// Golden: locks the 16-bit big-endian discriminant table and the payload
// layout. RATIFIED by Pass 11 (item 1.1, P11-1): this is now the spec's
// golden, normative in core_spec §"Identifiers",
// Requirement `req:graph:typed-object-id-discriminants` (discriminants
// 0..=27, Registered=27 as disc(2)||reg(16)||raw_be(16)). A reorder or
// reassignment breaks this deliberately — these bytes drive
// ordering/hashing/equality for every object.
let r = ReplicaId(9);
// Event = discriminant 0, then the 16-byte big-endian id payload.
let event = TypedObjectId::Event(EventId::new(r, 1)).canonical_bytes();

View File

@ -1938,17 +1938,11 @@ impl<'a> GraphIndex<'a> {
// --- 16. Tuplet member durations sum to required total. -----------------
fn check_tuplet_sum(&self, out: &mut Vec<InvariantViolation>) {
for t in &self.score.cross_cutting.tuplets {
// The actual:notated ratio must be well-formed: both terms positive
// (a 0:0 or n:0 ratio is meaningless, Chapter 3 §"Tuplets").
if t.ratio.actual == 0 || t.ratio.notated == 0 {
out.push(InvariantViolation::new(
GraphInvariant::TupletSum,
format!(
"tuplet {:?} has a degenerate ratio {}:{}",
t.id, t.ratio.actual, t.ratio.notated
),
));
}
// Degenerate ratios (a zero term or actual == notated) are rejected
// at construction by `TupletRatio::new` (Chapter 3 §"Tuplets",
// `req:time:tuplet-ratio-construction`), so they are never a
// representable graph state and are not re-checked here.
//
// Sum the members' musical durations; skip members that are absent
// (invariant 10) or non-musical (cannot contribute to a rational
// total).
@ -1979,10 +1973,12 @@ impl<'a> GraphIndex<'a> {
// so a wrong ratio (e.g. 3:2 changed to 5:4) is caught. Members
// without an in-tuplet decomposition are skipped (sound but
// incomplete; the decomposition pre-pass is deferred).
if t.ratio.actual != 0 && t.ratio.notated != 0 {
let scale =
crate::time::RationalTime::new(t.ratio.notated as i64, t.ratio.actual as i64)
.expect("nonzero ratio");
if t.ratio.actual() != 0 && t.ratio.notated() != 0 {
let scale = crate::time::RationalTime::new(
t.ratio.notated() as i64,
t.ratio.actual() as i64,
)
.expect("nonzero ratio");
for &member in &t.members {
let comps: Vec<&crate::graph::NotatedComponent> = self
.score
@ -2011,7 +2007,7 @@ impl<'a> GraphIndex<'a> {
format!(
"tuplet {:?} ratio {}:{} is inconsistent with member {:?}'s notation \
(notated scaled to {:?}, sounding duration is {:?})",
t.id, t.ratio.actual, t.ratio.notated, member, sounding, sd
t.id, t.ratio.actual(), t.ratio.notated(), member, sounding, sd
),
));
}
@ -2952,10 +2948,7 @@ mod review_fix_tests_2 {
let e = s.events.ids_canonical()[0];
s.cross_cutting.tuplets.push(Tuplet {
id: TupletId::new(r, 1),
ratio: TupletRatio {
actual: 3,
notated: 2,
},
ratio: TupletRatio::new(3, 2).expect("3:2 is a valid tuplet ratio"),
members: vec![e],
parent: Some(TupletId::new(r, 7_000_002)),
required_total: MusicalDuration(RationalTime::new(1, 4).unwrap()),
@ -3168,11 +3161,11 @@ mod review_fix_tests_3 {
use crate::graph::{
AleatoricAnchoringDiscipline, AleatoricTimeModel, Instrument, Region, RegionContent,
RegionTimeModel, Spanner, StaffBasedContent, StaffExtent, StaffInstance, Tie, TieClass,
TimeExtent, Tuplet, TupletRatio, Voice,
TimeExtent, TupletRatio, Voice,
};
use crate::ids::{
EventId, InstrumentId, PitchId, RegionId, ReplicaId, SpannerId, StaffInstanceId, TieId,
TupletId, VoiceId,
VoiceId,
};
use crate::pitch::{
AcousticPitch, AcousticRealization, CmnNominal, IdentifiedPitch, Pitch, PitchSpaceId,
@ -3285,23 +3278,20 @@ mod review_fix_tests_3 {
}
#[test]
fn inv16_flags_degenerate_tuplet_ratio() {
let mut s = valid_score(91);
let r = s.identity.replica_id;
let e = s.events.ids_canonical()[0];
s.cross_cutting.tuplets.push(Tuplet {
id: TupletId::new(r, 1),
ratio: TupletRatio {
actual: 0,
notated: 2,
},
members: vec![e],
parent: None,
// Match the member's 1/4 so only the ratio is wrong.
required_total: MusicalDuration(RationalTime::new(1, 4).unwrap()),
});
let v = check_invariant(&s, GraphInvariant::TupletSum);
assert!(v.iter().any(|x| x.witness.contains("degenerate ratio")));
fn degenerate_tuplet_ratio_is_rejected_at_construction() {
// Pass 11, item 3.5 / Tuplet honesty: a degenerate ratio is rejected by
// `TupletRatio::new` at construction, so it can never enter the graph
// and is no longer a runtime invariant. A zero term or `actual ==
// notated` is refused; a well-formed ratio is accepted.
assert!(TupletRatio::new(0, 2).is_none(), "n:0-form rejected");
assert!(TupletRatio::new(2, 0).is_none(), "0:n-form rejected");
assert!(TupletRatio::new(0, 0).is_none(), "0:0 rejected");
assert!(
TupletRatio::new(4, 4).is_none(),
"actual == notated rejected"
);
let ok = TupletRatio::new(3, 2).expect("3:2 is valid");
assert_eq!((ok.actual(), ok.notated()), (3, 2));
}
#[test]
@ -3840,10 +3830,8 @@ mod review_fix_tests_4 {
// Changing the ratio to 5:4 (member notation unchanged) now has a
// validation effect: the notated eighth no longer scales to 1/12.
let mut s = valid_score_rich(241);
s.cross_cutting.tuplets[0].ratio = TupletRatio {
actual: 5,
notated: 4,
};
s.cross_cutting.tuplets[0].ratio =
TupletRatio::new(5, 4).expect("5:4 is a valid tuplet ratio");
assert!(fires(&s, GraphInvariant::TupletSum));
}
}

View File

@ -899,9 +899,12 @@ mod tests {
fn system_pitch_id_byte_form_is_locked() {
// Golden: locks the MUSCSPCH canonical-input layout (space name, scale
// position discriminant + payload, tuning, acoustic realization; strings
// length-prefixed NFC) and the hash. A change to the byte form breaks
// this deliberately, forcing the derivation change to be acknowledged
// (DECISIONS P11-6).
// length-prefixed NFC) and the hash. RATIFIED by Pass 11 (item 1.3,
// P11-6): this is the spec's golden, normative in core_spec
// §"System-Derived Pitch Identity",
// Requirement `req:graph:system-derived-pitch-id` — note the tuning
// reference (incl. the Inherit marker) is always part of intrinsic
// identity. A change to the byte form breaks this deliberately.
let id = derive_system_pitch_id(&cmn(CmnNominal::C, 0, 4));
assert_eq!(id.replica(), crate::ids::ReplicaId::SYSTEM_DERIVED);
const GOLDEN: [u8; 16] = [

View File

@ -328,8 +328,11 @@ impl Default for RationalTime {
/// numerator's sign and big-endian magnitude (length-prefixed), then the
/// positive denominator's big-endian magnitude (length-prefixed). The value is
/// always reduced first, so equal rationals encode to equal bytes (Appendix D
/// §"Canonical serialization determinism"). The full bundle wire format is the
/// Binary Format companion's (Agent D); this is the prototype's canonical form.
/// §"Canonical serialization determinism"). RATIFIED by Pass 11 (item 1.7,
/// P11-4): this primitive layout is now normative in core_spec §"Binary Format
/// Companion", Requirement `req:format:rationaltime-encoding`; the full
/// composite wire format remains the Binary Format companion's (Agent J), which
/// inherits the ratified convention baseline (`req:format:codec-conventions`).
impl CanonicalEncode for RationalTime {
fn encode_canonical(&self, out: &mut Vec<u8>) {
let big = self.to_big();

View File

@ -26,8 +26,9 @@ impl DomainTag {
/// The prefix marking a *system-derived* tag (Chapter 5:
/// "Additional domain tags introduced by registered extensions MUST begin
/// with `MUSCS` and have length exactly 8 bytes"). The two built-in system
/// tags ([`Self::SYSTEM_VOICE`], [`Self::SYSTEM_PITCH`]) also carry it.
/// with `MUSCS` and have length exactly 8 bytes"). The three built-in
/// system tags ([`Self::SYSTEM_VOICE`], [`Self::SYSTEM_PITCH`],
/// [`Self::SYSTEM_ANOMALY`]) also carry it.
const SYSTEM_PREFIX: &'static [u8] = b"MUSCS";
/// The raw 8 ASCII bytes.
@ -56,9 +57,13 @@ impl DomainTag {
pub const SYSTEM_VOICE: DomainTag = DomainTag(*b"MUSCSVCE");
/// System-derived pitch counter derivation (Chapter 5 §"System-Derived").
pub const SYSTEM_PITCH: DomainTag = DomainTag(*b"MUSCSPCH");
/// `IntegrityAnomalyId` derivation (Chapter 5 §"System-Derived Counter
/// Collisions"). Reserved built-in: anomalies are core, not an extension
/// concern (Pass 11, item 1.4).
pub const SYSTEM_ANOMALY: DomainTag = DomainTag(*b"MUSCSANM");
/// Every built-in tag, in declaration order. The closed core vocabulary.
pub const BUILTINS: [DomainTag; 9] = [
pub const BUILTINS: [DomainTag; 10] = [
Self::CHUNK,
Self::MANIFEST,
Self::BLOB,
@ -68,6 +73,7 @@ impl DomainTag {
Self::MANIFEST_ID,
Self::SYSTEM_VOICE,
Self::SYSTEM_PITCH,
Self::SYSTEM_ANOMALY,
];
/// Constructs a domain tag from raw bytes, accepting only the spec's closed
@ -114,16 +120,16 @@ impl DomainTag {
Self::BUILTINS.contains(self)
}
/// Whether this is a *system-derived* tag (begins `MUSCS`): the built-in
/// [`Self::SYSTEM_VOICE`] / [`Self::SYSTEM_PITCH`] or an extension tag
/// minted via [`SystemDomainTag::new_extension`].
/// Whether this is a *system-derived* tag (begins `MUSCS`): a built-in
/// [`Self::SYSTEM_VOICE`] / [`Self::SYSTEM_PITCH`] / [`Self::SYSTEM_ANOMALY`]
/// or an extension tag minted via [`SystemDomainTag::new_extension`].
#[inline]
pub fn is_system_derived(&self) -> bool {
self.0.starts_with(Self::SYSTEM_PREFIX)
}
/// Whether this is an *extension-introduced* system tag: system-derived and
/// not a reserved built-in. The two built-in system tags return `false`
/// not a reserved built-in. The three built-in system tags return `false`
/// here — they are reserved, not extension-introduced.
#[inline]
pub fn is_extension_system_tag(&self) -> bool {
@ -131,8 +137,9 @@ impl DomainTag {
}
}
/// A [`DomainTag`] proven to be *system-derived* (begins `MUSCS`): the built-in
/// [`DomainTag::SYSTEM_VOICE`] / [`DomainTag::SYSTEM_PITCH`], or an
/// A [`DomainTag`] proven to be *system-derived* (begins `MUSCS`): a built-in
/// [`DomainTag::SYSTEM_VOICE`] / [`DomainTag::SYSTEM_PITCH`] /
/// [`DomainTag::SYSTEM_ANOMALY`], or an
/// extension-introduced tag. Only these are admissible seeds for
/// [`crate::derive_system_counter`] (Chapter 5 §"System-Derived Identifiers").
///
@ -148,6 +155,8 @@ impl SystemDomainTag {
pub const VOICE: SystemDomainTag = SystemDomainTag(DomainTag::SYSTEM_VOICE);
/// Built-in: system-derived pitch counters (`MUSCSPCH`).
pub const PITCH: SystemDomainTag = SystemDomainTag(DomainTag::SYSTEM_PITCH);
/// Built-in: integrity-anomaly identifiers (`MUSCSANM`).
pub const ANOMALY: SystemDomainTag = SystemDomainTag(DomainTag::SYSTEM_ANOMALY);
/// Wraps a domain tag if it is system-derived; returns `None` otherwise.
#[inline]
@ -215,6 +224,7 @@ mod tests {
DomainTag::MANIFEST_ID,
DomainTag::SYSTEM_VOICE,
DomainTag::SYSTEM_PITCH,
DomainTag::SYSTEM_ANOMALY,
];
for t in tags {
assert_eq!(t.as_bytes().len(), DomainTag::LEN);
@ -235,6 +245,7 @@ mod tests {
DomainTag::MANIFEST_ID,
DomainTag::SYSTEM_VOICE,
DomainTag::SYSTEM_PITCH,
DomainTag::SYSTEM_ANOMALY,
];
for (i, a) in tags.iter().enumerate() {
for b in &tags[i + 1..] {
@ -255,6 +266,7 @@ mod tests {
assert_eq!(DomainTag::MANIFEST_ID.as_bytes(), b"MUSCMNIF");
assert_eq!(DomainTag::SYSTEM_VOICE.as_bytes(), b"MUSCSVCE");
assert_eq!(DomainTag::SYSTEM_PITCH.as_bytes(), b"MUSCSPCH");
assert_eq!(DomainTag::SYSTEM_ANOMALY.as_bytes(), b"MUSCSANM");
assert_eq!(&BUNDLE_MAGIC, b"MUSCBND\0");
assert_eq!(&SUPERBLOCK_MAGIC, b"MUSCSUPR");
}
@ -264,10 +276,13 @@ mod tests {
// They are system-derived (begin MUSCS)...
assert!(DomainTag::SYSTEM_VOICE.is_system_derived());
assert!(DomainTag::SYSTEM_PITCH.is_system_derived());
assert!(DomainTag::SYSTEM_ANOMALY.is_system_derived());
// ...but reserved built-ins, NOT extension-introduced.
assert!(DomainTag::SYSTEM_VOICE.is_builtin());
assert!(DomainTag::SYSTEM_ANOMALY.is_builtin());
assert!(!DomainTag::SYSTEM_VOICE.is_extension_system_tag());
assert!(!DomainTag::SYSTEM_PITCH.is_extension_system_tag());
assert!(!DomainTag::SYSTEM_ANOMALY.is_extension_system_tag());
// A non-system tag is neither.
assert!(!DomainTag::CHUNK.is_system_derived());
assert!(!DomainTag::CHUNK.is_extension_system_tag());
@ -301,6 +316,8 @@ mod tests {
assert!(SystemDomainTag::new_extension(*b"MUSCXXXX").is_none());
// Must not collide with a reserved built-in.
assert!(SystemDomainTag::new_extension(*b"MUSCSVCE").is_none());
// MUSCSANM is now a reserved built-in too (Pass 11): not extension-mintable.
assert!(SystemDomainTag::new_extension(*b"MUSCSANM").is_none());
// Non-ASCII rejected.
assert!(
SystemDomainTag::new_extension([b'M', b'U', b'S', b'C', b'S', 0xFF, b'A', b'B'])
@ -313,7 +330,9 @@ mod tests {
assert!(ext.tag().is_extension_system_tag());
// Built-in system tags wrap; non-system tags do not.
assert_eq!(SystemDomainTag::VOICE.tag(), DomainTag::SYSTEM_VOICE);
assert_eq!(SystemDomainTag::ANOMALY.tag(), DomainTag::SYSTEM_ANOMALY);
assert!(SystemDomainTag::new(DomainTag::SYSTEM_PITCH).is_some());
assert!(SystemDomainTag::new(DomainTag::SYSTEM_ANOMALY).is_some());
assert!(SystemDomainTag::new(DomainTag::CHUNK).is_none());
}
}

View File

@ -76,9 +76,11 @@ impl ContentHash {
pub const ZERO: ContentHash = ContentHash([0u8; 32]);
/// Hashes a blob payload: `BLAKE3(MUSCBLOB || payload)`. This is the
/// `BlobId` construction (Chapter 8: "BLAKE3 of blob content with domain
/// tag `MUSCBLOB`") — the only spec content hash that is a bare
/// `domain || payload`.
/// `BlobId` construction — the only spec content hash that is a bare
/// `domain || payload`. RATIFIED by Pass 11 (item 3.1, P11-D3, a spec-bug
/// fix): core_spec §"Blobs", Requirement `req:format:blob-hash-shape` now
/// states the bare form explicitly and deletes the contradictory
/// "identically to chunks" phrasing.
///
/// Other content hashes are *not* this shape: a chunk hash also commits to
/// kind, schema version, and uncompressed length (Chapter 8

View File

@ -7,6 +7,15 @@ rather than improvised in code (QUICKSTART, Process notes: *"Ambiguities go into
a batch, not into code … Don't open Pass 11 until you have at least three such
items batched."*).
> **RATIFIED (Pass 11, 2026-06-21).** layout P11-2 (`LayoutObjectId` derivation)
> is ratified into `core_spec.tex` §"Provenance"
> (`req:layoutir:object-id-derivation`): a `MUSCLOID`-tagged derivation keying
> multiply-manifested objects on `(source, region)` and synthesized objects on
> `(source, synthesis_kind, stable_semantic_instance_key)`. Layout ids are
> non-canonical, so this is flagged for Track A (solver/renderer). layout P11-1
> (layout→ops dependency) stays a crate-topology call for the GK re-cut. See
> `spec/PASS11_RATIFICATION_LOG.md`.
## Scope
The crate implements the Chapter 7 interface surface: all four stages, the

View File

@ -7,6 +7,16 @@ than improvised in code (QUICKSTART, Process notes: *"Ambiguities go into a
batch, not into code … Don't open Pass 11 until you have at least three such
items batched."*).
> **RATIFIED (Pass 11, 2026-06-21).** The ops-layer Pass 11 candidates have been
> ratified into `core_spec.tex` — see `spec/PASS11_RATIFICATION_LOG.md`.
> Highlights: C2 adopted (`IntegrityAnomalyId` = `derive_system_id(MUSCSANM,…)`,
> with `MUSCSANM` promoted to a reserved built-in tag); C3 decided
> (field-collision tags the *winner* `Conflicted`); C4 adopted + lifted to spec
> (the >2-way, partial-overlap promotion generalization); C7 fixed (zero-based
> DVV floor made normative); C9 decided (`TransactionCategory`/`ObjectKind` core
> vocabularies pinned); C10 decided (added `ResolutionAction::Dismiss` so
> `Dismissed` is reachable). C1/C5/C6/C8 stay deferred to their tracks.
## Implementation decisions (QUICKSTART "Decisions you'll need to make")
1. **Replica ID entropy / event-arena / chunk store** — N/A to this crate

View File

@ -32,13 +32,15 @@ use crate::support::{
IntegrityAnomalyRegistryId, ObjectKind, ReplicaAnomalyRegistryId, SerializedCanonicalInputs,
};
/// The `MUSCS`-prefixed system domain tag under which integrity-anomaly
/// identifiers are content-derived, so two replicas reducing the same operation
/// set mint byte-identical anomaly ids (Chapter 5 §"System-Derived
/// Identifiers"). A prototype choice (see `DECISIONS.md`): the spec gives
/// `IntegrityAnomaly` an `IntegrityAnomalyId` but does not pin its derivation.
/// The reserved built-in system domain tag (`MUSCSANM`) under which
/// integrity-anomaly identifiers are content-derived, so two replicas reducing
/// the same operation set mint byte-identical anomaly ids. Ratified by Pass 11
/// (item 1.4): `MUSCSANM` is a built-in reserved tag alongside `MUSCSVCE` /
/// `MUSCSPCH` (Chapter 5 §"System-Derived Counter Collisions",
/// Requirement `req:graph:integrity-anomaly-id`), not an extension tag —
/// anomalies are core.
fn anomaly_domain_tag() -> SystemDomainTag {
SystemDomainTag::new_extension(*b"MUSCSANM").expect("MUSCSANM is a valid system tag")
SystemDomainTag::ANOMALY
}
/// A range of envelopes from a single replica identified as

View File

@ -7,7 +7,11 @@
//!
//! * `vector`: for each replica the authoring replica knows, the highest
//! *contiguous* counter it has observed. `vector[r] = n` asserts that every
//! operation `(r, 0..=n)` is a causal predecessor.
//! operation `(r, 0..=n)` is a causal predecessor. The counter floor is
//! **zero-based** and normative — RATIFIED by Pass 11 (item 3.4, P11-C7):
//! core_spec §"Causal Context via Dotted Version Vectors" now pins the
//! zero-based floor so a second implementation cannot pick a one-based floor
//! and diverge on pending detection.
//! * `dots`: individual [`OperationId`]s observed but not yet contiguous in the
//! vector — "known but not yet contiguous" predecessors.
//!

View File

@ -187,6 +187,10 @@ pub enum ResolutionAction {
Override { override_operation: OperationId },
/// Re-anchor to a user-chosen target.
Reanchor { new_target: TypedObjectId },
/// Dismiss the conflict without changing the materialized graph: the user
/// acknowledges it and accepts the current (winner) state. Selects the
/// `Dismissed` resolution state (Pass 11, item 2.5; Chapter 6).
Dismiss,
/// Custom resolution for a registered conflict kind.
Registered(ResolutionRegistryId),
}
@ -198,7 +202,8 @@ impl ResolutionAction {
ResolutionAction::KeepWinner => 1,
ResolutionAction::Override { .. } => 2,
ResolutionAction::Reanchor { .. } => 3,
ResolutionAction::Registered(_) => 4,
ResolutionAction::Dismiss => 4,
ResolutionAction::Registered(_) => 5,
}
}
}
@ -207,7 +212,9 @@ impl CanonicalEncode for ResolutionAction {
fn encode_canonical(&self, out: &mut Vec<u8>) {
push_tag(out, self.discriminant());
match self {
ResolutionAction::AcceptLoser | ResolutionAction::KeepWinner => {}
ResolutionAction::AcceptLoser
| ResolutionAction::KeepWinner
| ResolutionAction::Dismiss => {}
ResolutionAction::Override { override_operation } => {
push_canon(out, override_operation)
}

View File

@ -340,7 +340,8 @@ fn resolution_action(reader: &mut Reader<'_>) -> Result<ResolutionAction> {
3 => Ok(ResolutionAction::Reanchor {
new_target: typed_object_id(reader)?,
}),
4 => Ok(ResolutionAction::Registered(registry_id(
4 => Ok(ResolutionAction::Dismiss),
5 => Ok(ResolutionAction::Registered(registry_id(
reader,
ResolutionRegistryId,
)?)),

View File

@ -42,7 +42,9 @@ use epiphany_core::{
use epiphany_determinism::{CanonicalEncode, ContentHash};
use crate::anomaly::{detect_replica_anomalies, IntegrityAnomaly, IntegrityAnomalyKind};
use crate::conflict::{ConflictKind, ConflictRecord, ConflictRegistry, FieldPath};
use crate::conflict::{
ConflictKind, ConflictRecord, ConflictRegistry, FieldPath, ResolutionAction,
};
use crate::effect::{
NoOpReason, OperationEffect, PreconditionFailureReason, ReanchorReason, RepairKind,
RepairRecord, TupletCompensationKind,
@ -1737,9 +1739,12 @@ impl<'a> Reducer<'a> {
},
Some(RS::Unresolved) => {
if let Some(rec) = self.conflicts.get_mut(op.target) {
rec.resolution_state = RS::Resolved {
by: env.id,
action: op.action,
// ResolutionAction::Dismiss selects the Dismissed state;
// every other action selects Resolved with that action
// applied (Pass 11, item 2.5).
rec.resolution_state = match op.action {
ResolutionAction::Dismiss => RS::Dismissed { by: env.id },
action => RS::Resolved { by: env.id, action },
};
}
OperationEffect::Applied
@ -2353,4 +2358,97 @@ mod tests {
.map(|(_, e)| e);
assert_eq!(kept, Some(&OperationEffect::Applied));
}
#[test]
fn resolve_conflict_with_dismiss_reaches_dismissed_state() {
// Pass 11, item 2.5: an authored ResolveConflict whose action is
// `Dismiss` must reach the `Dismissed` resolution state (previously
// `Dismissed` was representable but unreachable by any authored op).
use crate::conflict::{ConflictResolutionState, ResolutionAction};
use crate::payload::{ResolveConflictPayload, RespellPitchOp};
use epiphany_core::PitchId;
use epiphany_determinism::ContentHash;
let pitch = PitchId::new(ReplicaId(9), 500);
// An InsertEvent carrying `pitch` makes the pitch Live.
let mut insert_env = insert(1, 0, 10, 1, 100, 0);
if let OperationPayload::Primitive(OperationKind::InsertEvent(ref mut op)) =
insert_env.payload
{
op.pitches = vec![pitch];
}
// Two concurrent, differing respellings of `pitch`, both causally after
// the insert (so the pitch is Live) but concurrent with each other (so
// they collide into a StructuralFieldCollision conflict).
let respell = |replica: u64, counter: u64, physical: i64, byte: u8| {
let id = OperationId::new(ReplicaId(replica), counter);
OperationEnvelope {
id,
author: AuthorId(0),
stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(physical), 0), id),
causal_context: CausalContext::new().with_seen(ReplicaId(1), 0),
transaction: None,
payload: OperationPayload::Primitive(OperationKind::RespellPitch(RespellPitchOp {
pitch,
spelling: ContentHash([byte; 32]),
})),
}
};
let respell_a = respell(2, 0, 20, 0xAA);
let respell_b = respell(3, 0, 21, 0xBB);
// Phase 1: reduce to discover the content-derived conflict id.
let mut set = OperationSet::new();
set.accept_all(vec![
insert_env.clone(),
respell_a.clone(),
respell_b.clone(),
]);
let state = set.reduce();
assert_eq!(
state.conflicts.records().len(),
1,
"expected exactly one field-collision conflict"
);
let cid = state.conflicts.records()[0].id;
assert_eq!(
state.conflicts.records()[0].resolution_state,
ConflictResolutionState::Unresolved
);
// Phase 2: author ResolveConflict { Dismiss } against that conflict,
// causally after the colliding respells so it reduces against the
// already-created conflict record.
let resolve_id = OperationId::new(ReplicaId(4), 0);
let resolve_env = OperationEnvelope {
id: resolve_id,
author: AuthorId(0),
stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(30), 0), resolve_id),
causal_context: CausalContext::new()
.with_dot(respell_a.id)
.with_dot(respell_b.id),
transaction: None,
payload: OperationPayload::ResolveConflict(ResolveConflictPayload {
target: cid,
action: ResolutionAction::Dismiss,
}),
};
let mut set2 = OperationSet::new();
set2.accept_all(vec![insert_env, respell_a, respell_b, resolve_env]);
let state2 = set2.reduce();
let rec = state2
.conflicts
.records()
.iter()
.find(|r| r.id == cid)
.expect("conflict still present after resolution");
assert_eq!(
rec.resolution_state,
ConflictResolutionState::Dismissed { by: resolve_id },
"Dismiss action must select the Dismissed state"
);
}
}

View File

@ -0,0 +1,57 @@
# Pass 11 — Ratification Log
One line per worklist item recording the disposition. `adopt` = blessed the
implementation's golden-locked choice as normative spec text. `decided-X` = a
real fork resolved to X with rationale in the spec. `fixed` = the spec text was
contradictory or missing and changed. Spec edits are in `core_spec.tex`; every
byte-layout golden test now cites its ratified requirement.
Date: 2026-06-21. Spec revision: **Pass 11 (spec ratification)**. Architecture
unchanged. The full worklist is `PASS11_WORKLIST.md`.
## Bucket 1 — Adopt-and-pin (bytes)
| Item | P11 id | Disposition | Spec locus | Test/code anchor |
|---|---|---|---|---|
| 1.1 `TypedObjectId` discriminants | P11-1 | **adopt** — pinned the 16-bit BE discriminant table 0..=27; added the 5 missing variants (Tuplet/RepeatStructure/LyricLine/ChordSymbol/View) the code carried; `Registered`=27 = disc(2)‖reg(16)‖raw_be(16); `ObjectKindRegistryId` is 128-bit | §"Identifiers", `req:graph:typed-object-id-discriminants` | `typed_object_id_byte_form_is_locked` |
| 1.2 Promoted-voice id | P11-3 / C4 | **adopt**`MUSCSVCE` 64-byte preimage (staff_instance‖original_voice‖winning_op‖losing_op, each 16 BE); staff-instance recovered from containment; one derivation feeds both the reducer and Invariant 18 | §"System-Promoted Voices" | `promoted_voice_id_byte_form_is_locked` |
| 1.3 System-pitch id | P11-6 | **adopt + decided**`MUSCSPCH` over (scale position, acoustic realization), strings length-prefixed + NFC at the boundary; **decided** the tuning reference is *always* part of intrinsic identity, including the `Inherit` presence marker | §"System-Derived Pitch Identity", `req:graph:system-derived-pitch-id` | `system_pitch_id_byte_form_is_locked` |
| 1.4 `IntegrityAnomalyId` + tag | P11-C2 | **adopt + decided**`derive_system_id(MUSCSANM, kind.canonical_bytes())`; **decided** `MUSCSANM` is a reserved *built-in* tag (anomalies are core), moved from `new_extension` to `DomainTag::SYSTEM_ANOMALY`. No byte change | §"System-Derived Counter Collisions", `req:graph:integrity-anomaly-id`; tag added to §"System-Derived Identifier Namespace" | `domain.rs` builtins; `anomaly.rs::anomaly_domain_tag` |
| 1.5 ChunkKind/Profile/Compression | P11-D4 | **adopt** — ChunkKind 0..=8, ProfileId 0..=3, CompressionAlgorithm 0..=2, all single declaration-order discriminant bytes; ChunkKind byte is in the chunk hash preimage | §"Chunks" `req:format:chunkkind-discriminants`; §"Format Profiles" `req:format:profileid-discriminants` | `chunk.rs::discriminant`, `chunk_kind_discriminants_round_trip` |
| 1.6 `ManifestId` preimage | P11-D5 | **adopt**`trunc128(BLAKE3("MUSCMNIF" ‖ document_id ‖ generation_le ‖ manifest_body))`, body excludes `manifest_id` (self-reference exclusion is normative) | §"Manifest Encoding", `req:format:manifest-id`; Appendix D entry updated | `bundle/ids.rs::derive` |
| 1.7 `RationalTime` + scalars | P11-4 | **adopt** — sign byte + u32-LE-length-prefixed BE numerator + u32-LE-length-prefixed BE denominator, always reduced; wall-clock integers little-endian | §"Binary Format Companion", `req:format:rationaltime-encoding` | `time.rs::CanonicalEncode for RationalTime` |
| 1.8 Codec conventions | P11-4 / D2 | **adopt as companion baseline** — LE ints; single discriminant byte per tagged union; u32 counts/length-prefixes on every variable-width leaf; raw UTF-8 free-text, NFC only for catalog ids at construction. Companion *inherits* this | §"Binary Format Companion", `req:format:codec-conventions` | `core/codec.rs` module doc |
## Bucket 2 — Decide-and-pin
| Item | P11 id | Disposition | Spec locus | Test/code anchor |
|---|---|---|---|---|
| 2.1 Tempo `Linear` parameter | P11-7 | **decided: speed-linear** — interpolates whole-notes-per-second (beat-unit-agnostic), not bpm/period; `Exponential` interpolates speed geometrically. Rationale: a tempo map may change beat unit across segments, so only speed gives a beat-unit-independent wall-clock schedule | §"Conversion", `req:time:linear-interpolates-speed` + rationale | `tempo.rs::SpeedModel` |
| 2.2 Field-collision effect tag | P11-C3 | **decided: winner-carries-`Conflicted`** — the later op (which materializes and noticed the collision) reads `Conflicted`; the earlier op keeps `Applied`. Chosen for order-independence; a UI reads the record's `loser` field for "your edit was overridden" | `req:semops:field-collision-effect` + rationale; RespellPitch reduction rule | `reduce.rs::respell_pitch` |
| 2.3 `>2`-way promotion | P11-C4 | **adopt + lifted to normative** — order-independent pre-pass: bucket by voice, walk by OperationId, retain a non-overlapping set, promote each overlapping loser (lowest-id retained survivor wins); applies to **partial** interval overlaps, not just identical onsets | §"System-Promoted Voices", `req:graph:promotion-generalization` | `reduce.rs::compute_promotions` |
| 2.4 Open-vocab enums | P11-C9 | **decided: pinned core sets, kept `Registered`**`TransactionCategory ∈ {NoteEntry, Structural, Layout, Import, Registered}`; `ObjectKind ∈ {Voice, Pitch, Registered}` (narrower than the 28 object kinds: only kinds minted into the system namespace) | `req:semops:transaction-category`, `req:graph:object-kind-vocab` | `payload.rs`, `support.rs` |
| 2.5 `ResolveConflict` Dismissed | P11-C10 | **decided: added `ResolutionAction::Dismiss`** (code + spec) — closes the half-unreachable state machine; the Dismiss action selects the `Dismissed` state, every other action selects `Resolved` | §"Conflict Resolution Operations" | `conflict.rs`, `reduce.rs::resolve_conflict`, `resolve_conflict_with_dismiss_reaches_dismissed_state` |
| 2.6 Layout-object id | layout P11-2 | **decided + registered tag (Track A)**`MUSCLOID`-tagged derivation; keys multiply-manifested objects on `(source, region)`, synthesized objects on `(source, synthesis_kind, stable_semantic_instance_key)`. Non-canonical (not document state); consumed by the solver/renderer | §"Provenance", `req:layoutir:object-id-derivation` | `layout-ir` provenance |
## Bucket 3 — Fixes (spec was contradictory or silent)
| Item | P11 id | Disposition | Spec locus | Test/code anchor |
|---|---|---|---|---|
| 3.1 Blob hashing shape | P11-D3 | **fixed (spec bug)** — bare `BLAKE3("MUSCBLOB" ‖ payload)`; deleted the contradictory "identically to chunks" phrasing so the structured-vs-bare contradiction cannot recur | §"Blobs", `req:format:blob-hash-shape` | `determinism/hash.rs::of_blob` |
| 3.2 Equal-generation tie-break | P11-D1 | **fixed (gap) — adopted code rule** — equal generation + equal load-bearing fields {manifest_hash, manifest_schema_version, reduction_algorithm_version, profile_id} → equivalent, pick A; any divergence → `DivergentSameGeneration`, read-only. Advisory fields (commit_timestamp, offset/length) excluded | §"Superblock Selection" + rationale | `superblock.rs::selection_equivalent` |
| 3.3 RetentionPolicy placement | P11-D6 | **fixed (silent) — defined `ProfileConstraints`** (was a dangling forward reference) and placed the required `retention_policy` in it; **decided** multi-profile precedence = first-declared profile | §"Format Profiles", `req:format:profile-constraints` | `manifest.rs::ProfileConstraints` |
| 3.4 DVV zero-based floor | P11-C7 | **fixed (promote to normative)**`vector[r]=n``(r,0..=n)` are predecessors; zero-based floor is normative so a one-based implementation can't diverge on pending detection. No behavior change | §"Causal Context via Dotted Version Vectors" | `causal.rs` |
| 3.5 Invariant count 18→19 | P11-2 | **fixed** — QUICKSTART 18→19; spec body states the count is 19 and names the three *construction-time* MUSTs (time-signature beat-group sum, ordering-DAG acyclicity, non-degenerate TupletRatio). **Tuplet honesty:** added `TupletRatio::new` rejecting degenerate ratios at construction (zero term or `actual==notated`) + codec decode validation; removed the now-redundant runtime sub-check | QUICKSTART; §"Graph Invariants" note; §"Tuplets" `req:time:tuplet-ratio-construction` | `graph.rs::TupletRatio::new`, `degenerate_tuplet_ratio_is_rejected_at_construction` |
## No-spec-change items (recorded resolved, no Pass 11 action)
`P11-C1` operation payload schemas → Track B (Operation Catalog). `P11-C5`
nearest-anchor stand-in → resolves with the graph-mutation phase. `P11-C6`
time-model compatibility → Track B. `P11-C8` forward undo → Track B. `P11-5`
Chapter 4 tuning catalog → Track C. `P11-D2` Binary Format companion → Track B
(item 1.8 baselines it). `layout P11-1` layout→ops dependency → crate-topology
call for the GK re-cut (recommend blessing the dependency).
**Tally:** 8 adopt-and-pin, 6 decide-then-pin, 5 fix, 6 no-spec-change-defer =
25 candidate items. All 19 ratifiable items are ratified; the 6 deferrals are
recorded with their owning track.

211
spec/PASS11_WORKLIST.md Normal file
View File

@ -0,0 +1,211 @@
# Pass 11 — Spec Ratification Worklist
*Purpose: convert the v0 implementation's provisional choices into ratified spec text, so the durable byte layouts are fixed before the next-phase build-outs (visible slice, interchange core) start writing real documents on top of them.*
*Scope: this is a **spec revision pass**, not an architecture pass. The architecture stays frozen. Every item below is either (a) a value the spec must pin, (b) a place the spec must show a field/derivation it implied but never wrote, or (c) a genuine spec-internal contradiction to fix. No item requires reopening a Pass 110 design decision.*
*Working rule (unchanged from QUICKSTART): the spec is the contract. Where this worklist says "adopt," the disposition is to bless the implementation's existing choice in normative spec text. Where it says "decide," there is a real fork the implementer should resolve and record. Where it says "fix," the spec text is self-contradictory or missing and must change regardless of the code.*
---
## How to use this document
The 25 items are grouped into three buckets by disposition difficulty:
- **Bucket 1 — Adopt-and-pin (mechanical).** The implementation made a deterministic, golden-locked choice; the spec just needs to write it down. Low judgment, high volume. An agent can draft all of these from the code directly. **Start here** — it unblocks the most downstream work for the least deliberation.
- **Bucket 2 — Decide-then-pin (judgment).** There is a real fork the spec left open; the implementation picked one branch but the choice has semantic consequences worth a deliberate call.
- **Bucket 3 — Fix (spec is wrong or silent).** The spec text contradicts itself or omits a required field. These change the spec independent of the code.
Each item carries: the source P11 id, the spec location to edit, the implementation's current choice (with the code anchor), a recommended disposition, and the **blast radius** (what downstream work is unblocked by ratifying it).
A golden-bytes test already locks every byte-layout item. **Ratification protocol per item:** (1) write the normative spec text; (2) if the spec adopts the code's layout, confirm the existing golden test is now the *spec's* golden, not just the crate's proposal — annotate the test to cite the ratified spec section; (3) if the spec overrides the code, update both the code and the golden in the same commit and add a migration note. Either way, no provisional byte layout survives this pass unannotated.
---
## Bucket 1 — Adopt-and-pin (mechanical, do first)
These are the items gating durable bytes. Every one is a discriminant table, derivation preimage, or encoding layout that already exists in code, is golden-locked, and just needs blessing. Ratifying this whole bucket is what converts "provisional encoding" to "stable format" — the precondition for the interchange track (J/K) and for any document a user saves and reopens later.
### 1.1 — `TypedObjectId` discriminant table `[P11-1]`
- **Spec edit:** Chapter 5 §"Identifier Family" / wherever `TypedObjectId::canonical_bytes` is defined. The spec fixes the *shape* (16-bit big-endian discriminant + variant payload) but the variant list ends "…and so on for every named object kind."
- **Code choice:** declaration-order discriminants `Event = 0 … AnalysisLayer = 21`, then the kinds beyond the spec's explicit list: `Tuplet = 22, RepeatStructure = 23, LyricLine = 24, ChordSymbol = 25, View = 26, Registered = 27`. `Registered(reg, raw)` encodes `discriminant(2) || reg.canonical_bytes(16) || raw_be(16)`. Locked by `typed_object_id_byte_form_is_locked`.
- **Disposition:** **Adopt.** The table is reasonable and complete against the current graph. Two confirmations needed: (a) the full object-kind set is closed at these 27 — confirm no graph object kind is missing; (b) `ObjectKindRegistryId` is a 128-bit value.
- **Blast radius:** every content hash, every canonical ordering, every equality over objects. This is the single highest-leverage ratification in the pass — most other byte layouts transitively reference object ids.
### 1.2 — Promoted-voice id derivation `[P11-3]`
- **Spec edit:** Chapter 5 §"System-Promoted Voices" / the semantic-operations companion's `derive_promoted_voice_id`.
- **Code choice:** 64-byte `MUSCSVCE` preimage = `staff_instance || original_voice || winning_op || losing_op`, each 16 big-endian bytes. Locked by `promoted_voice_id_byte_form_is_locked`. (Note the cross-crate dependency: this is **also** ops `P11-C4` — Agent C's reducer computes the same derivation Agent B's Invariant 18 verifies. Ratify once; both crates consume it.)
- **Disposition:** **Adopt.** Confirm field order and that staff-instance-from-containment is an acceptable recovery (the op does not store it redundantly).
- **Blast radius:** unblocks Invariant 18 verification and the ops promotion pre-pass from "provisional." Required before the Operation Catalog (K) fills in the real promotion payload.
### 1.3 — System-derived (synthetic) pitch id derivation `[P11-6]`
- **Spec edit:** Chapter 5 §"System-Derived Identifiers" (the `MUSCSPCH` tag is reserved but the derivation function is deferred).
- **Code choice:** `derive_system_pitch_id` content-addresses the pitch from a fixed canonical byte form of intrinsic identity (scale position + acoustic realization; strings length-prefixed and NFC-normalized at the derivation boundary). Locked by `system_pitch_id_byte_form_is_locked`.
- **Disposition:** **Adopt**, but **decide one sub-point**: the exact field set that constitutes "intrinsic identity." The code uses scale position + acoustic realization. Confirm that's the complete identity (e.g., does a synthetic pitch's identity include or exclude its tuning reference when tuning is `Inherit`?). This touches Invariant 11.
- **Blast radius:** Invariant 11 enforcement; any future synthetic-pitch minting.
### 1.4 — `IntegrityAnomalyId` derivation + anomaly domain tag `[P11-C2]`
- **Spec edit:** Chapter 5 (`IntegrityAnomaly` id) + the system-tag registry.
- **Code choice:** `derive_system_id(MUSCSANM, kind.canonical_bytes())` in the `SYSTEM_DERIVED` namespace, introducing a new `MUSCSANM` extension system tag.
- **Disposition:** **Adopt + decide:** confirm `MUSCSANM` should be a *built-in* reserved system tag alongside `MUSCSVCE`/`MUSCSPCH` (recommended — anomalies are core, not an extension), rather than an extension tag. If built-in, add it to the spec's closed tag vocabulary.
- **Blast radius:** cross-replica agreement on anomaly identity. Needed before equivocation/anomaly handling is part of any conformance claim.
### 1.5 — `ChunkKind` / `ProfileId` / `CompressionAlgorithm` discriminants `[P11-D4]`
- **Spec edit:** Chapter 8 §"Domain-Separated Preimages" — `ChunkKind::canonical_bytes()` is in the chunk hash preimage, so its discriminants are normative.
- **Code choice:** declaration-order single-byte discriminants: `ChunkKind` `OperationEnvelopeBlock = 0 … Manifest = 8`; `ProfileId` `03`; `CompressionAlgorithm` `02`.
- **Disposition:** **Adopt.** Same situation as 1.1 for the bundle layer.
- **Blast radius:** every chunk hash. `ChunkKind` in particular changes content addresses, so this gates the bundle format's stability.
### 1.6 — `ManifestId` derivation preimage `[P11-D5]`
- **Spec edit:** Chapter 8 §"The Manifest" (says "each commit produces a new `ManifestId`" + assigns `MUSCMNIF`, but gives no preimage).
- **Code choice:** `trunc128(BLAKE3("MUSCMNIF" || document_id || generation || manifest_body))`, where `manifest_body` is the canonical manifest encoding with the `manifest_id` field excluded (avoids self-reference).
- **Disposition:** **Adopt.** Confirm the self-reference exclusion is stated normatively (a conforming writer must zero/omit `manifest_id` when computing it).
- **Blast radius:** two conforming writers must derive identical manifest ids; gates multi-writer interop.
### 1.7 — `RationalTime` + scalar canonical encodings `[P11-4]`
- **Spec edit:** Appendix D / Chapter 8 — explicitly deferred to the Binary Format companion, but the primitive layouts can be ratified now independent of the full companion.
- **Code choice:** `RationalTime` = sign + length-prefixed big-endian numerator and denominator magnitudes, always reduced; wall-clock integers little-endian (matching `QuantizedCoord`).
- **Disposition:** **Adopt the primitive layouts now**, leave the *composite* whole-Score codec (1.8) flagged for the companion. The primitives are stable, small, and referenced everywhere; pinning them de-risks the companion.
- **Blast radius:** the foundation for every higher composite encoding.
### 1.8 — Whole-`Score` canonical codec conventions `[P11-4 / P11-C "Provisional encoding" / P11-D2]`
- **Spec edit:** This is the Binary Format companion's job in full, but the *conventions* the three crates already share should be ratified as the companion's baseline so they don't drift apart before it's written.
- **Code choice (uniform across core/ops/bundle):** little-endian integers; single discriminant byte per tagged union; `u32` counts/length-prefixes; every variable-width leaf length-prefixed; raw (non-NFC) UTF-8 for free-text fields, NFC only for catalog ids at construction.
- **Disposition:** **Adopt as the companion's baseline convention set.** Do not attempt to write the full companion in Pass 11 — that's Track B/Agent J. Pass 11's job is to bless the conventions so core/ops/bundle stay mutually consistent until J formalizes them. Record explicitly that the companion *inherits* these conventions rather than re-deriving them.
- **Blast radius:** this is the seam between Pass 11 and the interchange track. Getting the convention baseline ratified means Agent J writes a companion that matches three crates instead of reconciling three divergent codecs.
---
## Bucket 2 — Decide-then-pin (judgment calls)
Each has a real fork. The implementation picked a branch; the choice has semantic weight. Resolve deliberately and record the rationale in the spec, not just the verdict.
### 2.1 — Tempo "Linear" interpolation parameter `[P11-7]`
- **Fork:** Chapter 3 says a `Linear` segment is "linear interpolation from `start_tempo` to `end_tempo`" without saying *what* interpolates linearly: bpm, period, or speed.
- **Code choice:** interpolates **speed** (whole notes per second) linearly; `Exponential` interpolates speed geometrically. Speed is beat-unit-agnostic and coincides with linear-bpm when both tempos share a beat unit.
- **Recommendation:** **adopt speed-linear**, and state the rationale in the spec (beat-unit-agnosticism is the right invariant for a format that supports tempo changes across beat-unit changes). But this is a genuine musical-semantics call — a conductor's "accelerando" intuition is arguably linear-in-bpm. Worth one deliberate confirmation because it changes the derived wall-clock schedule and therefore playback timing forever.
- **Blast radius:** every wall-clock schedule derived from a tempo map; playback, and any `wallclock_to_musical` inverse.
### 2.2 — Per-operation effect tag in a field collision `[P11-C3]`
- **Fork:** for concurrent differing `RespellPitch`es the spec pins the *conflict record* (kind `StructuralFieldCollision`, winner/loser) and says the later-in-canonical-order op wins and materializes. It does not pin which participant's `OperationEffect` reads `Conflicted`.
- **Code choice:** tags the **winner** (later op, which materializes and whose processing created the record) `Conflicted`; leaves the earlier op's `Applied` in place.
- **Recommendation:** **decide deliberately.** There's a defensible alternative: tag the *loser* `Conflicted` (or `NoOp{SupersededByLaterOperation}`) since it's the one whose intent didn't survive, and leave the winner `Applied`. The code's choice (winner carries the flag because it's the one that *noticed* the collision) is order-independent and defensible, but the loser-tagged alternative is arguably more intuitive for a UI surfacing "your edit was overridden." Pick one, state why. Sub-point to resolve in the same breath: whether the superseded loser retroactively reads `NoOp{SupersededByLaterOperation}`.
- **Blast radius:** any UI or analytics consuming per-op effects; conflict-resolution UX.
### 2.3 — `>2`-way voice-promotion collision `[P11-C4]`
- **Fork:** the spec describes a *pairwise* promotion rule. Three or more concurrent overlapping inserts into the same voice need a generalization the spec doesn't give.
- **Code choice:** order-independent pre-pass — bucket inserts by voice, walk by `OperationId`, keep a non-overlapping set in the original voice, promote each concurrent overlapping loser; the first lower-id overlapping op retained in the original voice is the winner for each promotion. Also applies the rule to partial interval overlaps, not just identical start positions.
- **Recommendation:** **adopt**, and **lift the generalization into the spec normatively** (it's currently only in code). The "lowest-id retained survivor wins" rule is deterministic and matches the pairwise rule's spirit. Confirm the partial-overlap extension is intended (it's stricter than the spec's identical-start-position language — arguably correct, but it's a widening of the rule).
- **Blast radius:** any score with 3+ concurrent inserts in one voice; the Operation Catalog's promotion payload.
### 2.4 — Open-vocabulary enums: `TransactionCategory`, `ObjectKind` `[P11-C9]`
- **Fork:** the spec calls these open vocabularies. The code gives minimal core sets with a `Registered` escape: `TransactionCategory ∈ {NoteEntry, Structural, Layout, Import, Registered}`; `ObjectKind ∈ {Voice, Pitch, Registered}` (only the kinds actually derived into the system namespace).
- **Recommendation:** **decide the core set, keep `Registered`.** For `TransactionCategory`, confirm the four core categories are the right minimal set (UIs/analytics consume it; under-specifying is cheap to extend, over-specifying is not). For `ObjectKind`, note it's intentionally narrower than `TypedObjectId`'s 27 kinds because only Voice/Pitch are minted into the system namespace today — confirm that's the intended scoping rather than an oversight.
- **Blast radius:** `SystemIdentifierCollision` payloads (`ObjectKind`); UI/analytics (`TransactionCategory`).
### 2.5 — `ResolveConflict` Dismissed selection `[P11-C10]`
- **Fork:** the spec distinguishes `Resolved` from `Dismissed` resolution states but provides one `ResolveConflictPayload { target, action }`. The code maps every applied resolve to `Resolved { action }`; `Dismissed` is a reachable state but no representative op authors it.
- **Recommendation:** **decide:** either add a distinct `action` variant that selects Dismissed, or a separate payload. Recommend the former (a `ResolutionAction::Dismiss` variant) — it's the smaller change and keeps one payload type. This is a small but real gap: without it, half the resolution-state machine is unreachable by authored operations.
- **Blast radius:** conflict-resolution UX; the Operation Catalog's `ResolveConflict` schema.
### 2.6 — Layout-object id derivation `[layout P11-2]`
- **Fork:** Chapter 7 declares `LayoutObjectId(pub u128)` and requires stability across relayouts but specifies neither the derivation, whether it's domain-separated, how a multiply-manifested object (a staff in two regions) is keyed, nor how synthesized objects are keyed.
- **Code choice:** keys multiply-manifested objects on `(source, region)`; synthesized objects on `(source, synthesis_kind, stable_semantic_instance_key)`. No domain tag registered.
- **Recommendation:** **decide + register a tag.** Pin the derivation, the manifestation-context key, and the synthesized-object key. Appendix D's domain-separation discipline suggests registering a dedicated `MUSC*` layout tag — recommend doing so for consistency even though layout ids are non-canonical (they don't enter document state, but stability-across-relayout is easier to reason about with a fixed derivation). This one is lower-stakes than the Bucket 1 ids because layout ids are not canonical document state — flag it for Track A (the visible slice) rather than blocking on it.
- **Blast radius:** incremental relayout correctness; provenance back-references. Consumed by Track A (solver + renderer), not by the interchange track.
---
## Bucket 3 — Fix (spec is contradictory or silent)
These change the spec regardless of the code, because the spec text is wrong or missing.
### 3.1 — Blob hashing shape contradiction `[P11-D3]` ⚠️ spec bug
- **The contradiction:** Chapter 8 §"Blobs" says blobs are "content-addressed identically to chunks (BLAKE3 of uncompressed payload, with the `MUSCBLOB` domain tag)." "Identically to chunks" implies the **structured** preimage (committing to kind, schema version, and length); "BLAKE3 of uncompressed payload with the domain tag" implies a **bare** `MUSCBLOB || payload`. These disagree.
- **Code choice:** follows Agent A's `ContentHash::of_blob` = bare `MUSCBLOB || payload` (the only spec content hash documented as a bare `domain || payload`).
- **Disposition:** **Fix the spec text** to state exactly one. Recommend **bare** (matches the existing `of_blob` and the intuition that a blob is opaque payload with no schema), and **delete the "identically to chunks" phrasing** so the contradiction can't recur. If the structured form is wanted instead, the code and golden change with it.
- **Blast radius:** every `BlobId`. Must be resolved before blobs are stored in any durable bundle.
### 3.2 — Superblock equal-generation tie-break `[P11-D1]`
- **The gap:** Chapter 8 §"Superblock Selection" says "the slot with the higher generation is active" but gives no rule for two valid slots at the *same* generation — which the QUICKSTART nonetheless lists as a harness scenario.
- **Code choice:** equal generation + equivalent load-bearing fields (`manifest_hash`, `manifest_schema_version`, `reduction_algorithm_version`, `profile_id`; advisory `commit_timestamp` and physical offset/length excluded) → equivalent, pick A. Equal generation differing in any load-bearing field → `IntegrityAnomaly::DivergentSameGeneration`, opened read-only.
- **Disposition:** **Fix (add the missing rule).** Adopt the code's rule into Chapter 8 normatively. It's the conservative correct call: two genuinely different committed states cannot share a generation under a conforming writer, so divergence is an integrity anomaly, not a silent pick. State the exact load-bearing field set (the exclusion of advisory fields is the subtle part).
- **Blast radius:** crash-recovery determinism; the manifest-selection harness's correctness depends on this being specified.
### 3.3 — `RetentionPolicy` field placement `[P11-D6]`
- **The gap:** Chapter 8 §"Garbage Collection and Retention" requires "the active conformance profile MUST declare a `RetentionPolicy`," but the `ProfileDeclaration` / `ProfileConstraints` structs shown in §"Format Profiles" don't include the field.
- **Code choice:** `retention_policy` placed inside `ProfileConstraints`; a bundle declaring multiple profiles resolves retention from the first declared profile.
- **Disposition:** **Fix (show the field).** Add `retention_policy` to the `ProfileConstraints` struct in the spec, and **decide** the multi-profile resolution rule (the code uses first-declared; confirm or specify a precedence). This is mostly mechanical but the multi-profile precedence is a small real decision.
- **Blast radius:** GC correctness; any bundle with a non-trivial retention policy.
### 3.4 — DVV zero-based counter floor (make normative) `[P11-C7]`
- **The gap:** the DVV's contiguous `vector[r] = n` asserts predecessors `(r, 0..=n)` exist. The code relies on this zero-based floor; the spec documents it in prose but should retain it explicitly in the *normative* DVV definition (it's load-bearing for the missing-causal-predecessor pending logic).
- **Code choice:** zero-based per-replica counter floor; range check walks known ids rather than expanding `0..=n` (so a sparse high-counter context doesn't cause proportional work).
- **Disposition:** **Fix (promote to normative).** No behavior change — just ensure the normative DVV definition states the zero-based floor so a second implementation can't choose a one-based floor and silently diverge on pending detection.
- **Blast radius:** cross-implementation agreement on which operations are pending vs. ready.
### 3.5 — Invariant count: 18 vs 19 `[P11-2]`
- **The gap:** QUICKSTART says "18 graph invariants"; Chapter 5 body enumerates 19; the code implements 19.
- **Disposition:** **Fix the QUICKSTART** (it's the stale doc; the spec body is authoritative). Trivial, but do it so the count is consistent across all three artifacts. While here, confirm the three Chapter-3 "reject at construction" rules (`TimeSignature` beat-group sum, `EventOrderingDAG` acyclicity, `Tuplet` degenerate ratios) are correctly *outside* the 19-invariant enumeration — the code enforces them at construction, which is faithful, but the spec should be explicit that they're construction-time MUSTs, not runtime invariants.
- **Blast radius:** documentation consistency only; no bytes.
---
## Items that need no spec change (record as resolved)
These appear in the DECISIONS files as P11 candidates but, on review, require no ratification — they resolve when later phases land, and the code already records the approximation honestly. List them so the agent doesn't spend time on them:
- **`P11-C1` (operation payload schemas carry identifiers + fingerprints):** resolves when the Operation Catalog (Track B/Agent K) lands. No Pass 11 action — the payload *schemas* are that companion's deliverable, not a ratification item. The provisional projection is honest.
- **`P11-C5` ("nearest surviving anchor" stand-in):** resolves when the graph-mutation phase tracks resolved positions. The rule *structure* is faithful; only the metric is approximated. No spec change.
- **`P11-C6` (time-model compatibility computed when graph available):** the rich migration payload belongs to the Operation Catalog. No Pass 11 action.
- **`P11-C8` (forward undo via minted-object compensation):** faithful to the spec's content-equivalence definition for insert-shaped transactions; per-primitive inverses are the Operation Catalog's job. No spec change.
- **`P11-5` (Chapter 4 tuning catalog is a separate subsystem):** a scope boundary, not an ambiguity. Becomes Track-C work (the tuning catalog) when scheduled. No Pass 11 action.
- **`P11-D2` (Binary Format companion not yet written):** the companion is Track B/Agent J. Pass 11 ratifies the *convention baseline* (item 1.8) so the crates stay consistent until then, but does not write the companion.
- **`layout P11-1` (Agent E dependency set vs. edit-barrier types):** a build-organization question (does layout depend on ops for `OperationKindTag`, or do the edit-barrier types relocate?). Decide this when re-cutting the agents for GK — it's a crate-topology call, not a spec-byte call. Recommend blessing the layout→ops dependency (the discriminator type is small and stable) rather than relocating types.
---
## Suggested execution order for the agent
1. **Bucket 1 in id-dependency order:** 1.1 (`TypedObjectId`) first — most things reference object ids — then 1.5 (`ChunkKind`), then the derivations that build on them (1.2, 1.3, 1.4, 1.6), then the encodings (1.7, 1.8). Draft each as normative spec text + annotate the existing golden test to cite the ratified section. This is the bulk of the value and the least deliberation.
2. **Bucket 3 fixes:** 3.1 (blob contradiction) is the only true spec *bug* and gates durable blobs — do it early. 3.23.5 are mechanical-with-one-decision-each.
3. **Bucket 2 judgment calls:** these benefit from a short written rationale each. 2.1 (tempo-linear) and 2.2 (effect tag) are the two with lasting semantic weight; give them the most thought. 2.6 (layout ids) can defer to Track A.
**Deliverable:** a spec revision (call it the Pass 11 revision, consistent with the revision-history convention) plus a one-line-per-item ratification log recording the disposition (adopt / decided-X-because-Y / fixed) for each of the 25. The log is what lets the next-phase agents trust that "provisional" is now "ratified."
**Do not:** open any architectural question, rewrite any Pass 110 decision, or attempt the Binary Format companion or Operation Catalog here. Those are Track B. Pass 11's boundary is exactly: pin what exists, fix what contradicts, and ratify the convention baseline the companions will inherit.
---
## One-line inventory (for tracking)
| Item | P11 id | Bucket | Disposition | Gates |
|---|---|---|---|---|
| 1.1 TypedObjectId discriminants | P11-1 | 1 | Adopt | all object hashing/ordering |
| 1.2 Promoted-voice id | P11-3 / C4 | 1 | Adopt | Inv 18; promotion payload |
| 1.3 Synthetic-pitch id | P11-6 | 1 | Adopt + 1 decision | Inv 11 |
| 1.4 IntegrityAnomalyId + tag | P11-C2 | 1 | Adopt + tag call | anomaly agreement |
| 1.5 ChunkKind/Profile/Compression discriminants | P11-D4 | 1 | Adopt | all chunk hashing |
| 1.6 ManifestId preimage | P11-D5 | 1 | Adopt | multi-writer interop |
| 1.7 RationalTime + scalars | P11-4 | 1 | Adopt | all composite encoding |
| 1.8 Whole-Score codec conventions | P11-4/D2 | 1 | Adopt as baseline | interchange track seam |
| 2.1 Tempo Linear parameter | P11-7 | 2 | Recommend speed-linear | playback timing |
| 2.2 Field-collision effect tag | P11-C3 | 2 | Decide winner vs loser | conflict UX |
| 2.3 >2-way promotion | P11-C4 | 2 | Adopt + lift to spec | 3+ concurrent inserts |
| 2.4 Open-vocab enums | P11-C9 | 2 | Decide core sets | collision payloads, UI |
| 2.5 ResolveConflict Dismissed | P11-C10 | 2 | Add Dismiss action | conflict state machine |
| 2.6 Layout-object id | layout P11-2 | 2 | Decide + tag (defer to Track A) | relayout stability |
| 3.1 Blob hashing shape | P11-D3 | 3 | **Fix** (recommend bare) | every BlobId |
| 3.2 Equal-gen tie-break | P11-D1 | 3 | Fix (adopt code rule) | crash recovery |
| 3.3 RetentionPolicy placement | P11-D6 | 3 | Fix + multi-profile call | GC |
| 3.4 DVV zero-based floor | P11-C7 | 3 | Fix (make normative) | pending detection |
| 3.5 Invariant count 18→19 | P11-2 | 3 | Fix QUICKSTART | docs |
| — payload schemas | P11-C1 | none | defer to Track B | — |
| — nearest-anchor stand-in | P11-C5 | none | resolves with graph phase | — |
| — time-model compat | P11-C6 | none | defer to Track B | — |
| — forward undo | P11-C8 | none | defer to Track B | — |
| — tuning catalog scope | P11-5 | none | defer to Track C | — |
| — Binary Format companion | P11-D2 | none | Track B (1.8 baselines it) | — |
| — layout/ops dependency | layout P11-1 | none | crate-topology call for GK | — |
25 candidate items: **8 adopt-and-pin, 6 decide-then-pin, 5 fix, 6 no-spec-change-defer.** (1.2/C4 and the layout dependency note are counted once each at their primary home.)

View File

@ -53,7 +53,7 @@ Spec sections: Appendix D in full, Chapter 4 §4.6 (frequency units), Chapter 7
### Agent B — `epiphany-core`
Owns the score graph: the identifier family (`EventId`, `PitchId`, `VoiceId`, ..., `TypedObjectId`), the `ReplicaId::SYSTEM_DERIVED` reserved value and counter derivation (`trunc64(BLAKE3(domain_tag || canonical_inputs))`), `IdentifiedPitch`, `PitchSpelling`, `ScalePosition`, the duration union (`EventDuration` / `ConcreteDuration`), `MusicalPosition`, `WallClockTime`, `TimeAnchor`, `AnchorOffset`, the event arena, `Voice`, `Staff` and `StaffInstance` (these are distinct types — the spec is explicit), `Region`, `BarlineAlignmentGroup`, and the 18 graph invariants enumerated in Chapter 5.
Owns the score graph: the identifier family (`EventId`, `PitchId`, `VoiceId`, ..., `TypedObjectId`), the `ReplicaId::SYSTEM_DERIVED` reserved value and counter derivation (`trunc64(BLAKE3(domain_tag || canonical_inputs))`), `IdentifiedPitch`, `PitchSpelling`, `ScalePosition`, the duration union (`EventDuration` / `ConcreteDuration`), `MusicalPosition`, `WallClockTime`, `TimeAnchor`, `AnchorOffset`, the event arena, `Voice`, `Staff` and `StaffInstance` (these are distinct types — the spec is explicit), `Region`, `BarlineAlignmentGroup`, and the 19 graph invariants enumerated in Chapter 5.
**Critical**: graph invariants are property tests in CI, not runtime assertions in release builds. The testkit provides arbitrary-instance generators. For each invariant, you must have a positive generator that produces valid graphs and a negative shrinker that minimizes invariant violations to a small witness for debugging.

Binary file not shown.

View File

@ -2011,6 +2011,43 @@ For constant segments, conversion is multiplication. For linear and
exponential segments, conversion has a closed-form solution. For curve
segments, conversion is numerical.
\begin{requirement}
\label{req:time:linear-interpolates-speed}
For a \texttt{TempoShape::Linear} segment, the quantity that varies
linearly across the segment is \emph{speed} --- whole notes per
second --- as a function of fractional position within the segment,
\emph{not} beats-per-minute and \emph{not} beat period. Speed is
derived from a \texttt{Tempo} as
$\text{speed} = \text{bpm} \times w(\text{beat\_unit}) / 60$, where
$w(\cdot)$ is the beat unit's value in whole notes. Writing
$s_0, s_1$ for the segment's start and end speeds and
$u \in [0,1]$ for fractional position, $s(u) = s_0 + (s_1 - s_0)\,u$,
and the wall-clock duration of the segment of musical length
$\ell$ is the closed form
$\int_0^1 \ell\,\mathrm{d}u / s(u) = (\ell / (s_1 - s_0))\,
\ln(s_1 / s_0)$ (and $\ell / s_0$ when $s_1 = s_0$).
A \texttt{TempoShape::Exponential} segment interpolates speed
\emph{geometrically}: $s(u) = s_0\,(s_1/s_0)^{u}$, i.e. a constant
continuous rate of change of speed.
\end{requirement}
\begin{rationale}
``Linear interpolation from \texttt{start\_tempo} to
\texttt{end\_tempo}'' is ambiguous about which quantity is linear:
bpm, period, or speed. We pin \emph{speed} because it is the only
beat-unit-agnostic choice. A tempo map may change beat unit across
segments (a \texttt{quarter = 120} segment abutting a
\texttt{dotted-quarter = 80} segment); interpolating bpm would make
the wall-clock schedule depend on the arbitrary beat unit chosen to
notate each endpoint, whereas interpolating speed depends only on
the sounding rate. When both endpoints share a beat unit, speed-linear
and bpm-linear coincide, so the common case matches a conductor's
``linear accelerando'' intuition. Because this choice fixes the
derived wall-clock schedule for every playback engine, it is stated
normatively rather than left to the implementation.
\end{rationale}
\begin{requirement}
The tempo map \MUST{} expose two conversion functions:
\texttt{musical\_to\_wallclock} and \texttt{wallclock\_to\_musical}.
@ -2178,6 +2215,20 @@ pub struct TupletRatio {
}
\end{lstlisting}
\begin{requirement}
\label{req:time:tuplet-ratio-construction}
A \texttt{TupletRatio} is \emph{degenerate} if either term is zero
or if \texttt{actual == notated} (a ratio that expresses no
augmentation or diminution). Degenerate ratios \MUST{} be rejected
at construction: a conforming constructor of \texttt{TupletRatio}
(and therefore of any \texttt{Tuplet}) \MUST{} fail rather than
produce a value with \texttt{actual == 0}, \texttt{notated == 0}, or
\texttt{actual == notated}. This is a construction-time rejection,
not a runtime graph invariant (Section~\ref{sec:graph:invariants}):
a degenerate ratio is never a representable graph state, so no
invariant restores it after the fact.
\end{requirement}
\subsection{Tuplet Nesting}
Tuplets \MAY{} nest to arbitrary depth. The sounding duration of an
@ -2367,6 +2418,7 @@ encoding admits both as cases.
\end{requirement}
\begin{requirement}
\label{req:time:ordering-dag-acyclic}
The ordering DAG \MUST{} be acyclic. Cycles \MUST{} be rejected at
construction. Two events with no path between them in the DAG are
unordered.
@ -3394,6 +3446,11 @@ pub enum TypedObjectId {
GraphicGesture(GraphicGestureId),
TimeSignature(TimeSignatureId),
AnalysisLayer(AnalysisLayerId),
Tuplet(TupletId),
RepeatStructure(RepeatStructureId),
LyricLine(LyricLineId),
ChordSymbol(ChordSymbolId),
View(ViewId),
/// Extension-defined object kind, identified by registry id
/// plus the extension's own 128-bit identifier.
Registered(ObjectKindRegistryId, u128),
@ -3407,6 +3464,51 @@ impl TypedObjectId {
}
\end{lstlisting}
\begin{requirement}
\label{req:graph:typed-object-id-discriminants}
The \texttt{TypedObjectId} variant tag \MUST{} be encoded as a
16-bit big-endian discriminant followed by the variant payload's
canonical bytes. The discriminant of each variant is its
declaration-order index in the following closed table; this
assignment is normative and stable, and \MUSTNOT{} be reordered:
\begin{center}
\begin{tabular}{r l @{\qquad} r l}
\toprule
\textbf{Disc.} & \textbf{Variant} &
\textbf{Disc.} & \textbf{Variant} \\
\midrule
0 & \texttt{Event} & 14 & \texttt{Spanner} \\
1 & \texttt{Pitch} & 15 & \texttt{Marker} \\
2 & \texttt{Voice} & 16 & \texttt{AnalyticalAnnotation} \\
3 & \texttt{Staff} & 17 & \texttt{Comment} \\
4 & \texttt{StaffInstance} & 18 & \texttt{GraphicObject} \\
5 & \texttt{StaffGroup} & 19 & \texttt{GraphicGesture} \\
6 & \texttt{Region} & 20 & \texttt{TimeSignature} \\
7 & \texttt{Instrument} & 21 & \texttt{AnalysisLayer} \\
8 & \texttt{PartDefinition} & 22 & \texttt{Tuplet} \\
9 & \texttt{Measure} & 23 & \texttt{RepeatStructure} \\
10 & \texttt{BarlineAlignmentGroup} & 24 & \texttt{LyricLine} \\
11 & \texttt{Slur} & 25 & \texttt{ChordSymbol} \\
12 & \texttt{Tie} & 26 & \texttt{View} \\
13 & \texttt{Beam} & 27 & \texttt{Registered} \\
\bottomrule
\end{tabular}
\end{center}
The object-kind set is closed at these 28 discriminants
(\texttt{0}--\texttt{27}). Every named object kind in the score
graph has a dedicated discriminant; kinds introduced by registered
extensions \MUST{} use discriminant \texttt{27}
(\texttt{Registered}) and are distinguished by their
\texttt{ObjectKindRegistryId}, never by extending this table. The
\texttt{Registered(reg, raw)} payload is encoded as the 128-bit
\texttt{reg.canonical\_bytes()} (16 big-endian bytes) followed by
the 16 big-endian bytes of the \texttt{raw} \texttt{u128}, for a
total canonical form of $2 + 16 + 16 = 34$ bytes.
\texttt{ObjectKindRegistryId} is a 128-bit value.
\end{requirement}
\subsection{Identifier Generation}
\begin{requirement}
@ -3488,14 +3590,22 @@ fn derive_system_counter(
}
\end{lstlisting}
Domain tags for system-derived identifiers:
The built-in (reserved) domain tags for system-derived identifiers
are a closed set of three:
\begin{itemize}
\item \texttt{"MUSCSVCE"} for system-promoted voices.
\item \texttt{"MUSCSVCE"} for system-promoted voices
(Section~\ref{sec:graph:promoted-voices}).
\item \texttt{"MUSCSPCH"} for system-derived pitches (rare;
only when promotion logic introduces a synthetic pitch).
only when promotion logic introduces a synthetic pitch;
Section~\ref{sec:graph:system-derived-pitch}).
\item \texttt{"MUSCSANM"} for integrity-anomaly identifiers
(Section~\ref{sec:graph:system-collisions}). Anomalies are
core, not an extension concern, so their tag is built in
alongside the other two.
\item Additional domain tags introduced by registered
extensions \MUST{} begin with \texttt{"MUSCS"} and have
length exactly 8 bytes.
extensions \MUST{} begin with \texttt{"MUSCS"}, have length
exactly 8 bytes, and \MUSTNOT{} collide with the three
reserved tags above.
\end{itemize}
Two replicas reducing the same operation set \MUST{} derive
@ -3550,8 +3660,48 @@ pub enum IntegrityAnomalyKind {
/// transport.
Registered(IntegrityAnomalyRegistryId),
}
/// The kind of object whose system-derived identifier collided.
/// Open vocabulary, but intentionally narrower than TypedObjectId:
/// only the kinds actually minted into the system-derived namespace
/// appear here.
pub enum ObjectKind {
Voice, // system-promoted voices (MUSCSVCE)
Pitch, // content-derived synthetic pitches (MUSCSPCH)
Registered(OperationKindRegistryId),
}
\end{lstlisting}
\begin{requirement}
\label{req:graph:integrity-anomaly-id}
An \texttt{IntegrityAnomalyId} \MUST{} be derived by the
system-derived identifier function of
Section~\ref{sec:graph:system-derived} with the reserved domain tag
\texttt{"MUSCSANM"} over the anomaly kind's canonical bytes:
\texttt{derive\_system\_id::<IntegrityAnomalyId>(b"MUSCSANM",
\&kind.canonical\_bytes())}. Because the identity is content-derived
from the kind, two replicas observing the same structural failure
derive the same anomaly identifier and therefore agree on anomaly
identity across the network.
\end{requirement}
\begin{requirement}
\label{req:graph:object-kind-vocab}
\texttt{ObjectKind} (the kind field of
\texttt{SystemIdentifierCollision}) is an \emph{open} vocabulary
whose normative core set is exactly \texttt{Voice} and
\texttt{Pitch}, with a \texttt{Registered} escape for
extension-introduced system-derived kinds. It is deliberately
narrower than \texttt{TypedObjectId}'s 28-kind table
(Requirement~\ref{req:graph:typed-object-id-discriminants}):
\texttt{ObjectKind} enumerates only the kinds that are actually
minted into the \texttt{ReplicaId::SYSTEM\_DERIVED} namespace ---
today, promoted voices and synthetic pitches --- because those are
the only kinds for which a system-derived counter collision is
possible. A new core variant is added here only when a new object
kind begins being minted into the system-derived namespace.
\end{requirement}
\begin{requirement}
Implementations \MUST{} perform a collision check during
reduction whenever a new system-derived identifier is minted.
@ -3605,6 +3755,37 @@ pub enum IntegrityAnomalyKind {
growth until external recovery resolves the structural issue.
\end{rationale}
\subsection{System-Derived Pitch Identity}
\label{sec:graph:system-derived-pitch}
When promotion logic introduces a synthetic pitch (rare), its
\texttt{PitchId} is content-addressed from the pitch's intrinsic
identity rather than allocated from a replica counter, so that all
replicas mint the same identifier for the same synthetic pitch.
\begin{requirement}
\label{req:graph:system-derived-pitch-id}
A system-derived \texttt{PitchId} \MUST{} be derived by the
function of Section~\ref{sec:graph:system-derived} with domain tag
\texttt{"MUSCSPCH"} over a fixed canonical byte form of the pitch's
\emph{intrinsic identity}, defined as the pair (\emph{scale
position}, \emph{acoustic realization}). All variable-width string
components of this preimage (nominal names, accidental and tuning
catalog references) \MUST{} be length-prefixed and \MUST{} be
NFC-normalized at the derivation boundary, so that
canonically-equivalent Unicode spellings derive the same identifier.
The acoustic realization's \emph{tuning reference} is always part of
intrinsic identity, including the \texttt{TuningReference::Inherit}
case: \texttt{Inherit} is encoded as a distinct presence marker, not
as the absence of a field. Consequently a synthetic pitch that
inherits its tuning and one that names an explicit tuning are
\emph{distinct} identities even when they would resolve to the same
frequency in context. This makes synthetic-pitch identity stable
under later changes to the inherited tuning environment, and aligns
Invariant~11's uniqueness check with a fixed, context-free preimage.
\end{requirement}
\subsection{Identifier Stability}
\begin{requirement}
@ -4389,12 +4570,67 @@ preserves the non-overlap invariant without rejecting user intent.
\item The losing operation's \texttt{OperationId}.
\end{enumerate}
The exact derivation function (a hash of the concatenated
identifiers, normalized to the replica-plus-counter format
reserved for system-derived identifiers) is specified in the
semantic-operations companion document. Determinism is required so
that all replicas independently arrive at the same promoted
voice identity.
The derivation is the system-derived identifier function of
Section~\ref{sec:graph:system-derived} with domain tag
\texttt{"MUSCSVCE"} over a fixed 64-byte preimage formed by
concatenating the four identifiers in the order listed above, each
as its 16-byte big-endian canonical form:
\begin{lstlisting}[language=Rust]
fn derive_promoted_voice_id(
staff_instance: StaffInstanceId,
original_voice: VoiceId,
winning_op: OperationId,
losing_op: OperationId,
) -> VoiceId {
// 4 * 16 = 64 bytes, each component big-endian.
let mut inputs = Vec::with_capacity(64);
inputs.extend_from_slice(&staff_instance.canonical_bytes());
inputs.extend_from_slice(&original_voice.canonical_bytes());
inputs.extend_from_slice(&winning_op.canonical_bytes());
inputs.extend_from_slice(&losing_op.canonical_bytes());
derive_system_id::<VoiceId>(b"MUSCSVCE", &inputs)
}
\end{lstlisting}
The enclosing staff instance is recovered from graph containment at
reduction time; the operation does not store it redundantly.
Determinism is required so that all replicas independently arrive at
the same promoted voice identity. This derivation is the same one
whose result Invariant~18 (Section~\ref{sec:graph:invariants})
verifies; the reducer that mints the voice and the verifier that
checks it consume one derivation.
\end{requirement}
\begin{requirement}
\label{req:graph:promotion-generalization}
The promotion rule generalizes from the pairwise case to any number
of concurrent overlapping inserts into the same voice by an
order-independent pre-pass:
\begin{enumerate}
\item Bucket the concurrent \texttt{InsertEvent} operations by
their target \texttt{(staff\_instance, original\_voice)}.
\item Within a bucket, walk the operations in ascending
\texttt{OperationId} order, maintaining a \emph{retained set} of
mutually non-overlapping inserts that stay in the original voice.
\item An operation whose inserted interval does not overlap any
member of the retained set joins the retained set. An operation
whose interval overlaps some retained member is a \emph{loser}
and is promoted; the lowest-\texttt{OperationId} retained member
it overlaps is its \texttt{winning\_operation}.
\end{enumerate}
Because the walk is by \texttt{OperationId} and the retained set is
built deterministically, every replica promotes the same operations
and derives the same promoted-voice identities regardless of
delivery order. ``Overlap'' is interval overlap of the half-open
inserted spans \texttt{[start,\,end)}: the rule applies to
\emph{partial} overlaps, not only to inserts that share an identical
start position. This is a deliberate widening of the pairwise rule's
identical-start-position language; it is the correct reading because
any positive-duration overlap breaks the voice non-overlap invariant
(Invariant~3), not just a shared onset.
\end{requirement}
\begin{requirement}
@ -5099,6 +5335,29 @@ The score graph maintains a set of structural invariants. Implementations
changes that restore them within the same operation.
\end{requirement}
This enumeration contains exactly \textbf{19} invariants. (Earlier
summary material, including the QUICKSTART, referred to ``18 graph
invariants''; the authoritative count is 19, matching this
enumeration and the reference implementation.) These 19 are
\emph{runtime} invariants: they hold over every well-formed graph and
are restored by compensating changes when an edit would break them.
Distinct from them are three \emph{construction-time} rejections
defined in Chapter~\ref{ch:time}, which a conforming constructor
\MUST{} enforce before a value ever enters the graph, and which no
runtime invariant restores because the offending value is never a
representable graph state:
\begin{enumerate}
\item A \texttt{TimeSignature}'s beat groups \MUST{} sum to the
measure duration (Section~\ref{sec:time:meter}).
\item An \texttt{EventOrderingDAG} \MUST{} be acyclic
(Requirement~\ref{req:time:ordering-dag-acyclic}).
\item A \texttt{TupletRatio} \MUSTNOT{} be degenerate
(Section~\ref{sec:time:tuplets},
Requirement~\ref{req:time:tuplet-ratio-construction}).
\end{enumerate}
\section{Indexes and Auxiliary Structures}
\label{sec:graph:indexes}
@ -5290,6 +5549,20 @@ pub struct CausalContext {
output or in the operation log's expanded form, but the wire and
storage representation of causal context is the DVV.
The per-replica counter floor is \textbf{zero-based} and
normative: \texttt{vector[r] == n} asserts that \emph{every}
operation \texttt{(r,\,0..=n)} is a causal predecessor --- the
contiguous range begins at counter \texttt{0}, not \texttt{1}. A
conforming implementation \MUSTNOT{} adopt a one-based floor: doing
so would shift the contiguous/dot boundary and make two
implementations disagree about which operations are pending versus
ready. The membership check (``is operation \texttt{(r,\,c)}
covered?'') is \texttt{c <= vector[r]} or \texttt{(r,\,c) in dots};
an implementation \MAY{} evaluate it by walking the known operation
ids rather than materializing the full \texttt{0..=n} range, so a
sparse high-counter context does not force work proportional to the
counter value.
Interval tree clocks and other compact causal representations
\MAY{} appear in future revisions as performance optimizations but
are non-normative in this specification.
@ -6135,6 +6408,10 @@ pub enum ConflictResolutionState {
Dismissed { by: OperationId },
}
// See Requirement (field-collision effect tag): for a
// StructuralFieldCollision the winner (later op) reads Conflicted;
// the earlier op retains Applied.
pub enum ResolutionAction {
/// Accept the losing operation's effect (replacing the winner).
AcceptLoser,
@ -6145,11 +6422,44 @@ pub enum ResolutionAction {
Override { override_operation: OperationId },
/// Re-anchor to a user-chosen target.
Reanchor { new_target: TypedObjectId },
/// Dismiss the conflict without changing the materialized graph:
/// the user acknowledges it and accepts the current (winner)
/// state. Selects the Dismissed resolution state.
Dismiss,
/// Custom resolution for registered conflict kinds.
Registered(ResolutionRegistryId),
}
\end{lstlisting}
\begin{requirement}
\label{req:semops:field-collision-effect}
When two concurrent operations write the same non-LWW field and
reduction records a \texttt{StructuralFieldCollision}, the
per-operation \texttt{OperationEffect} is assigned as follows: the
\emph{winner} --- the operation later in canonical order, whose
choice materializes and whose processing created the conflict record
--- reads \texttt{Conflicted\{conflict\}}; the \emph{earlier}
(losing) operation retains the \texttt{Applied} effect it received
when it was first reduced. The winner carries the flag because it is
the single operation that both materializes the field and observes
the collision, which makes the assignment independent of the order
in which envelopes arrive at any replica.
\end{requirement}
\begin{rationale}
The alternative --- tagging the loser \texttt{Conflicted} (or
\texttt{NoOp\{SupersededByLaterOperation\}}) because its intent did
not survive --- is defensible and arguably more intuitive for a UI
that surfaces ``your edit was overridden.'' We pin
\emph{winner-carries} instead because it is computed at the moment
the collision is detected (when the winning operation is reduced
against already-present state), needs no second pass to retroactively
re-tag an earlier operation, and is manifestly order-independent. A
UI that wishes to tell the losing author their edit was overridden
reads the \texttt{StructuralFieldCollision} record's \texttt{loser}
field rather than the loser operation's effect.
\end{rationale}
\subsection{The Conflict Registry}
The score graph carries a top-level conflict registry alongside the
@ -6255,10 +6565,17 @@ pub struct ResolveConflictPayload {
\begin{requirement}
A \texttt{ResolveConflict} operation transitions a conflict's
state from \texttt{Unresolved} to \texttt{Resolved} (with the
declared action applied) or \texttt{Dismissed} (with no semantic
change to the materialized graph beyond the conflict record's
state).
state from \texttt{Unresolved} to one of two states selected by its
\texttt{action}. The \texttt{ResolutionAction::Dismiss} action
transitions the conflict to \texttt{Dismissed} (no semantic change
to the materialized graph beyond the conflict record's state: the
user acknowledges the conflict and accepts the current winner
state). Every other \texttt{action} transitions the conflict to
\texttt{Resolved} with that action applied. Both resolution states
are thus reachable by an authored operation; \texttt{Dismiss} is the
authored selector for \texttt{Dismissed}, closing the prior gap in
which \texttt{Dismissed} was a representable state with no operation
to reach it.
Concurrent \texttt{ResolveConflict} operations on the same
conflict are themselves subject to canonical reduction: the
@ -6288,8 +6605,32 @@ pub struct TransactionDescriptor {
}
pub struct TransactionId(pub u128);
/// Open vocabulary: a minimal core set plus a Registered escape for
/// extension-defined categories.
pub enum TransactionCategory {
NoteEntry,
Structural,
Layout,
Import,
Registered(OperationKindRegistryId),
}
\end{lstlisting}
\begin{requirement}
\label{req:semops:transaction-category}
\texttt{TransactionCategory} is an \emph{open} vocabulary. Its
normative core set is exactly \texttt{NoteEntry}, \texttt{Structural},
\texttt{Layout}, and \texttt{Import}; any other category \MUST{} be
expressed as \texttt{Registered(OperationKindRegistryId)} rather than
by extending the core set. The field is advisory (consumed by UIs and
analytics, never by canonical reduction), so the minimal core set is
the right tradeoff: under-specifying is cheap to extend through
\texttt{Registered}, whereas baking speculative categories into the
core vocabulary is not. The discriminants follow declaration order
(\texttt{NoteEntry} $= 0$ through \texttt{Registered} $= 4$).
\end{requirement}
Each primitive operation belonging to a transaction carries the
transaction's identifier in its envelope's \texttt{transaction} field.
The transaction descriptor itself is a separate operation envelope
@ -7128,9 +7469,14 @@ that operation's choice. The losing respelling is recorded in a
\texttt{ConflictRecord} of kind
\texttt{StructuralFieldCollision} if the two respellings represent
different spellings (i.e., differ in nominal, accidental stack, or
octave). Identical concurrent respellings reduce idempotently: the
later operation produces \texttt{NoOp\{reason: AlreadyApplied\}}
with no conflict recorded.
octave). The per-operation effect assignment follows the general
field-collision rule
(Requirement~\ref{req:semops:field-collision-effect}): the later
operation --- the winner, which materializes --- reads
\texttt{Conflicted}, and the earlier operation retains
\texttt{Applied}. Identical concurrent respellings reduce
idempotently: the later operation produces
\texttt{NoOp\{reason: AlreadyApplied\}} with no conflict recorded.
\subsection{ChangeRegionTimeModel}
@ -7538,6 +7884,42 @@ pub struct LayoutObjectId(pub u128);
whose change should invalidate this layout object.
\end{requirement}
\begin{requirement}
\label{req:layoutir:object-id-derivation}
A \texttt{LayoutObjectId} \MUST{} be stable across re-layouts whose
underlying source is unchanged. It is derived by domain-separated
BLAKE3 truncation with the reserved layout domain tag
\texttt{"MUSCLOID"} over a key that depends on how the object is
manifested:
\begin{itemize}
\item A layout object that manifests a single score-graph object
exactly once is keyed on \texttt{source.canonical\_bytes()}.
\item A multiply-manifested object --- one score-graph object that
appears in more than one layout context, e.g. a staff manifested
in two regions --- is keyed on the pair
\texttt{(source,\,region)}, so each manifestation receives a
distinct, stable id.
\item A synthesized object (no direct score-graph
\texttt{source}; \texttt{synthesis} is \texttt{Some}) is keyed
on the triple \texttt{(source,\,synthesis\_kind,\,
stable\_semantic\_instance\_key)}, where the
\texttt{stable\_semantic\_instance\_key} distinguishes multiple
synthesized objects sharing a source and synthesis kind by a
semantically stable discriminator (not a layout-position
ordinal, which would not survive relayout).
\end{itemize}
\texttt{LayoutObjectId}s are \emph{non-canonical}: they are not part
of document state and do not enter any content hash, so the
\texttt{"MUSCLOID"} tag lives in the layout namespace and is not one
of the reserved \emph{canonical} system tags of
Section~\ref{sec:graph:system-derived}. The fixed derivation is
pinned for incremental-relayout correctness and provenance
back-reference stability; its consumers are the solver and renderer
(Track~A), not the interchange track.
\end{requirement}
\section{The Stage Pipeline}
\label{sec:layoutir:pipeline}
@ -8779,7 +9161,28 @@ pub enum CommitState {
highest-generation valid slot; it \MUSTNOT{} silently treat
the file as normal.
\item If both slots are valid and their generations differ by
at most one, the slot with the higher generation is active.
exactly one, the slot with the higher generation is active.
\item If both slots are valid and their generations are
\emph{equal}, the reader \MUST{} compare the slots'
\emph{load-bearing} selection fields:
\texttt{manifest\_hash}, \texttt{manifest\_schema\_version},
\texttt{reduction\_algorithm\_version}, and
\texttt{profile\_id}. The advisory fields
\texttt{commit\_timestamp} and the physical chunk
offset/length are \emph{excluded} from this comparison.
\begin{itemize}
\item If every load-bearing field is equal, the two slots
select equivalent canonical state; the reader \MUST{}
deterministically choose slot~A and open normally.
\item If any load-bearing field differs, the reader \MUST{}
report a format-layer \texttt{IntegrityAnomaly} of kind
\texttt{DivergentSameGeneration} and open the bundle
read-only. Two genuinely different committed states cannot
share a generation under a conforming writer (the
atomic-write protocol increments the generation on every
commit), so the divergence is a structural failure, not a
state the reader may silently resolve by picking one.
\end{itemize}
\item If exactly one slot is valid, that slot is active.
\item If neither slot is valid, the file is corrupt; readers
\MUST{} surface this as a hard error and \MUSTNOT{}
@ -8787,6 +9190,20 @@ pub enum CommitState {
\end{enumerate}
\end{requirement}
\begin{rationale}
The advisory fields are excluded from the equal-generation
comparison because they do not determine which canonical state a
slot selects: \texttt{commit\_timestamp} is wall-clock metadata,
and the physical offset/length merely locate the manifest chunk in
the file. Two slots that agree on the four load-bearing fields name
the same canonical document even if they were written at different
times or to different offsets, so treating an advisory-only
difference as divergence would raise false anomalies on benign
repacks. Conversely, a difference in any load-bearing field means
the two slots disagree about canonical state itself, which is the
precise condition that must not be silently resolved.
\end{rationale}
\section{The Manifest}
\label{sec:format:manifest}
@ -9020,6 +9437,26 @@ manifest before it has any information from the chunk store.
the few hundred bytes of compression headroom.
\end{rationale}
\begin{requirement}
\label{req:format:manifest-id}
The \texttt{manifest\_id} field \MUST{} be derived as
\[
\texttt{ManifestId} = \mathrm{trunc128}\!\left(
\mathrm{BLAKE3}(\texttt{"MUSCMNIF"} \Vert \texttt{document\_id}
\Vert \texttt{generation} \Vert \texttt{manifest\_body})\right),
\]
where \texttt{document\_id} is its 16-byte canonical form,
\texttt{generation} is the 8-byte little-endian \texttt{u64}, and
\texttt{manifest\_body} is the canonical manifest encoding with the
\texttt{manifest\_id} field itself \emph{excluded}. The exclusion is
normative: a conforming writer \MUST{} omit (equivalently, zero) the
\texttt{manifest\_id} field when computing the preimage, so that the
identifier does not reference itself. $\mathrm{trunc128}$ takes the
leading 16 bytes of the 32-byte BLAKE3 output. Two conforming
writers committing the same manifest body at the same generation of
the same document therefore derive identical \texttt{ManifestId}s.
\end{requirement}
\section{Content Hashing}
\label{sec:format:hashing}
@ -9182,6 +9619,42 @@ pub struct ContentHash(pub [u8; 32]);
pub struct ChunkId(pub ContentHash);
\end{lstlisting}
\begin{requirement}
\label{req:format:chunkkind-discriminants}
\texttt{ChunkKind::canonical\_bytes()} is a single byte: the
variant's declaration-order discriminant. Because that byte is part
of the chunk hash preimage (Section~\ref{sec:format:chunks},
``Domain-Separated Preimages''), this assignment is normative and
stable and \MUSTNOT{} be reordered:
\begin{center}
\begin{tabular}{r l @{\qquad} r l}
\toprule
\textbf{Disc.} & \textbf{Variant} &
\textbf{Disc.} & \textbf{Variant} \\
\midrule
0 & \texttt{OperationEnvelopeBlock} & 5 & \texttt{TextProjection} \\
1 & \texttt{OperationIndex} & 6 & \texttt{LayoutCache} \\
2 & \texttt{Snapshot} & 7 & \texttt{IntegrityIndex} \\
3 & \texttt{Blob} & 8 & \texttt{Manifest} \\
4 & \texttt{ExtensionData} & & \\
\bottomrule
\end{tabular}
\end{center}
\texttt{CompressionAlgorithm} likewise encodes as a single
declaration-order discriminant byte followed by any variant payload:
\texttt{None} $= 0$ (no payload); \texttt{Zstd\{level\}} $= 1$
followed by the \texttt{level} byte; \texttt{Reserved(u8)} $= 2$
followed by the reserved byte. Compression is \emph{not} part of a
chunk's content identity (it is metadata on the \texttt{ChunkRef}),
so these discriminants are stable but do not affect chunk hashes.
\texttt{ProfileId}'s discriminants are pinned in
Section~\ref{sec:format:profiles}
(Requirement~\ref{req:format:profileid-discriminants}).
\end{requirement}
\begin{requirement}
Chunks \MUST{} be immutable. Once a chunk is written and its bytes
durably flushed, those bytes \MUSTNOT{} be modified. New state
@ -9393,9 +9866,23 @@ pub struct SnapshotRef {
\label{sec:format:blobs}
The blob store holds large opaque content: audio reference tracks,
embedded images, custom fonts, ML model checkpoints. Blobs are
content-addressed identically to chunks (BLAKE3 of uncompressed
payload, with the \texttt{"MUSCBLOB"} domain tag).
embedded images, custom fonts, ML model checkpoints.
\begin{requirement}
\label{req:format:blob-hash-shape}
A \texttt{BlobId} \MUST{} be BLAKE3 over the \emph{bare} preimage
\texttt{"MUSCBLOB"} $\Vert$ \texttt{uncompressed\_payload}: the
8-byte domain tag immediately followed by the uncompressed payload
bytes, with \emph{no} chunk-kind, schema-version, or length fields.
This is deliberately \emph{unlike} the structured chunk preimage of
Section~\ref{sec:format:chunks} (which commits to kind, schema
version, and length): a blob is opaque payload with no schema, so
its identity commits only to the domain tag and the bytes. As with
chunks, the compression algorithm is metadata on the
\texttt{BlobRef} and is \emph{not} part of the blob's identity, so
identical payloads stored under different compression share a
\texttt{BlobId}.
\end{requirement}
\begin{lstlisting}[language=Rust]
pub struct BlobRef {
@ -9872,6 +10359,45 @@ pub enum ProfileId {
}
\end{lstlisting}
The constraints a profile imposes are carried in
\texttt{ProfileConstraints}, which the GC and retention rules
(Section~\ref{sec:format:gc}) reference for the mandatory
\texttt{RetentionPolicy}:
\begin{lstlisting}[language=Rust]
pub struct ProfileConstraints {
/// Maximum uncompressed operation-envelope block size a reader
/// must accept under this profile (default 64 MiB).
pub max_uncompressed_block_size: u64,
/// The RetentionPolicy this profile declares. Required: the GC
/// and retention rules read it from the active profile.
pub retention_policy: RetentionPolicy,
// Additional constraint fields (permitted compression sets,
// required-extension lists) are reserved for a later revision;
// a reader MUST tolerate their future addition under the
// minor-version compatibility rule.
}
\end{lstlisting}
\begin{requirement}
\label{req:format:profile-constraints}
Every \texttt{ProfileDeclaration} \MUST{} carry a
\texttt{ProfileConstraints}, and every \texttt{ProfileConstraints}
\MUST{} include a \texttt{retention\_policy} field. This satisfies
the requirement (Section~\ref{sec:format:gc}) that the active
conformance profile declare a \texttt{RetentionPolicy}: the policy
lives inside the active profile's constraints, not as a free-standing
manifest field.
When a bundle declares more than one profile, the
\texttt{RetentionPolicy} in force is the one declared by the
\emph{first} profile in the manifest's \texttt{profile\_declarations}
list (declaration order is canonical). A bundle that declares no
profile constraints carrying a policy uses the Full profile's
default retention policy.
\end{requirement}
\begin{requirement}
Every bundle \MUST{} declare at least one profile in its manifest's
\texttt{profile\_declarations}. An implementation \MAY{} open a
@ -9880,6 +10406,18 @@ pub enum ProfileId {
(Section~\ref{sec:format:fwdcompat}).
\end{requirement}
\begin{requirement}
\label{req:format:profileid-discriminants}
\texttt{ProfileId} encodes as a single declaration-order
discriminant followed by any variant payload: \texttt{Full} $= 0$,
\texttt{ReadOnly} $= 1$, \texttt{Lite} $= 2$,
\texttt{Custom(ProfileRegistryId)} $= 3$ followed by the registry
id's canonical bytes. This assignment is normative and stable.
\texttt{ProfileId} participates in superblock selection
(Section~\ref{sec:format:bundle}, the load-bearing field set), so
its discriminants are part of that comparison.
\end{requirement}
\section{Compression}
\label{sec:format:compression}
@ -9997,9 +10535,72 @@ Format document. The Binary Format document is normative; an
implementation cannot conform to the file format specification
without conforming to the Binary Format specification.
\subsection{Ratified Convention Baseline}
\label{sec:format:codec-baseline}
Although the full Binary Format companion is still to be written, the
canonical encoding conventions shared by the core, operations, and
bundle layers are ratified now, so those three layers do not drift
apart before the companion formalizes them. The companion
\emph{inherits} this baseline rather than re-deriving it.
\begin{requirement}
\label{req:format:codec-conventions}
The canonical encoding of every composite structure (whole
\texttt{Score}, operation envelopes, manifest) \MUST{} follow these
conventions:
\begin{itemize}
\item Integers are little-endian. A boolean is a single
\texttt{0}/\texttt{1} byte.
\item Counts and the length prefix of every variable-width
\emph{leaf} (an identifier, a \texttt{RationalTime}, a string)
are \texttt{u32} little-endian. Length-prefixing every leaf
makes the decoder width-agnostic: it never infers a boundary
from context.
\item A tagged union is encoded as a single discriminant byte
followed by the variant payload.
\item Free-text string fields are length-prefixed UTF-8 and are
\emph{not} NFC-folded by the codec: the in-memory value is
preserved byte-exact, so \texttt{decode(encode(x)) == x}.
Catalog identifiers are NFC-normalized at construction, not in
the codec.
\end{itemize}
Certain fixed hashing preimages deliberately depart from the
single-discriminant-byte convention where a wider or order-bearing
tag is required --- notably \texttt{TypedObjectId}'s 16-bit
big-endian discriminant
(Requirement~\ref{req:graph:typed-object-id-discriminants}). Such
departures are pinned at their definitions and are not overridden by
this baseline.
\end{requirement}
\begin{requirement}
\label{req:format:rationaltime-encoding}
The primitive scalar layouts referenced throughout the canonical
encodings are ratified as follows:
\begin{itemize}
\item \texttt{RationalTime} encodes as a sign byte
(\texttt{0} = zero, \texttt{1} = positive, \texttt{2} =
negative), then the \texttt{u32}-little-endian-length-prefixed
big-endian magnitude of the numerator, then the
\texttt{u32}-little-endian-length-prefixed big-endian magnitude
of the denominator. The value \MUST{} always be stored reduced
(lowest terms, positive denominator), so the encoding is
canonical: equal rationals encode to equal bytes.
\item Wall-clock integers (\texttt{WallClockTime},
\texttt{WallClockDuration}) are little-endian fixed-width
integers, matching \texttt{QuantizedCoord}.
\end{itemize}
\end{requirement}
\begin{openquestion}
The Binary Format companion is under development as a separate
specification. Its development follows this chapter; the byte-level
The remainder of the Binary Format companion --- the full composite
struct layouts, schema-version wire evolution, and varint details
beyond the baseline above --- is under development as a separate
specification. Its development follows this chapter and inherits the
ratified baseline (Sections~\ref{req:format:codec-conventions}
and~\ref{req:format:rationaltime-encoding}); the remaining byte-level
decisions depend on the structural decisions specified here.
\end{openquestion}
@ -11625,10 +12226,12 @@ rules are normative here.
\texttt{ManifestId} &
Content-derived from manifest payload &
Unique per manifest version. Implementations \MAY{} use
trunc128(BLAKE3 of canonical manifest preimage) with domain
tag \texttt{"MUSCMNIF"}; the Binary Format companion defines
the exact derivation. \\
Unique per manifest version. Derived as
\texttt{trunc128(BLAKE3("MUSCMNIF" || document\_id ||
generation || manifest\_body))} with the \texttt{manifest\_id}
field excluded from \texttt{manifest\_body}; the derivation is
ratified normatively in
Section~\ref{req:format:manifest-id}. \\
\texttt{SnapshotId} &
Content-derived from snapshot payload and frontier &
@ -13345,6 +13948,43 @@ state.
Chapter~\ref{ch:format}'' wording, reflecting that the
Pass 3 architectural shift made causal context part of
operation semantics rather than only file storage. \\
\today & Pass 11 revision (spec ratification) &
Converted the v0 implementation's provisional, golden-locked byte
choices into ratified normative spec text; the architecture is
unchanged. \emph{Adopt-and-pin (bytes):} pinned the
\texttt{TypedObjectId} 16-bit big-endian discriminant table
(\texttt{0}--\texttt{27}, \texttt{Registered}~=~27; added the
\texttt{Tuplet}/\texttt{RepeatStructure}/\texttt{LyricLine}/%
\texttt{ChordSymbol}/\texttt{View} variants the implementation
carried); the promoted-voice (\texttt{MUSCSVCE}), system-pitch
(\texttt{MUSCSPCH}, tuning always part of identity), and
integrity-anomaly (\texttt{MUSCSANM}, promoted to a reserved
built-in tag) derivations; the \texttt{ChunkKind} /
\texttt{ProfileId} / \texttt{CompressionAlgorithm} discriminants;
the \texttt{ManifestId} preimage (with \texttt{manifest\_id}
excluded); and the \texttt{RationalTime}/scalar primitive layouts
plus the shared codec convention baseline the Binary Format
companion inherits. \emph{Decide-and-pin:} \texttt{TempoShape::Linear}
interpolates \emph{speed} (whole notes/second), not bpm; a
\texttt{StructuralFieldCollision} tags the \emph{winner}
\texttt{Conflicted}; lifted the $>$2-way voice-promotion
generalization (partial-overlap, lowest-id-survivor) to normative;
pinned the \texttt{TransactionCategory} and \texttt{ObjectKind} core
vocabularies; added \texttt{ResolutionAction::Dismiss} so the
\texttt{Dismissed} resolution state is reachable by an authored
operation; and pinned the (non-canonical) \texttt{LayoutObjectId}
derivation with a \texttt{MUSCLOID} tag. \emph{Fixes:} blob hashing
is the bare \texttt{"MUSCBLOB" || payload} (deleted the
contradictory ``identically to chunks'' phrasing); added the
equal-generation superblock rule (\texttt{DivergentSameGeneration}
on load-bearing-field divergence); defined \texttt{ProfileConstraints}
and placed the required \texttt{RetentionPolicy} in it with
first-declared multi-profile precedence; made the DVV zero-based
counter floor normative; reconciled the graph-invariant count to 19
(was 18 in QUICKSTART) and named the three construction-time MUSTs
(time-signature beat-group sum, ordering-DAG acyclicity, and
non-degenerate \texttt{TupletRatio}, now enforced at construction).
\\
\bottomrule
\end{longtable}