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:
parent
691f527e07
commit
b2f2e204a7
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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] = [
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 G–K re-cut. See
|
||||
> `spec/PASS11_RATIFICATION_LOG.md`.
|
||||
|
||||
## Scope
|
||||
|
||||
The crate implements the Chapter 7 interface surface: all four stages, the
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
//!
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)?)),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 G–K 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.
|
||||
|
|
@ -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 1–10 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` `0–3`; `CompressionAlgorithm` `0–2`.
|
||||
- **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 G–K — 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.2–3.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 1–10 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 G–K | — |
|
||||
|
||||
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.)
|
||||
|
|
@ -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.
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue