diff --git a/crates/epiphany-bundle/src/ids.rs b/crates/epiphany-bundle/src/ids.rs index 7a83e26..a17eff3 100644 --- a/crates/epiphany-bundle/src/ids.rs +++ b/crates/epiphany-bundle/src/ids.rs @@ -101,6 +101,11 @@ impl ManifestId { /// core_spec §"Manifest Encoding", Requirement `req:format:manifest-id` /// (`trunc128(BLAKE3("MUSCMNIF" || document_id || generation || body))`, /// body excluding `manifest_id`). + /// + /// Note: `document_id` and `generation` are committed twice — explicitly + /// here and again inside `body_preimage` (the canonical manifest body opens + /// with them). This duplication is intentional and golden-locked, not an + /// oversight: the preimage shape above is the ratified format. 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()); diff --git a/crates/epiphany-core/DECISIONS.md b/crates/epiphany-core/DECISIONS.md index 6c98fae..f9397f1 100644 --- a/crates/epiphany-core/DECISIONS.md +++ b/crates/epiphany-core/DECISIONS.md @@ -208,5 +208,8 @@ which is both faithful and removes the need for a runtime pass: - `TimeSignature::new` rejects beat groups that do not sum to the measure duration. - `EventOrderingDAG::try_new` rejects a cyclic aleatoric ordering. -- `Tuplet` degenerate ratios (`0:n`/`n:0`) are caught by invariant 16 - (`check_invariants`), since a `Tuplet` is a plain struct. +- `TupletRatio::new` rejects degenerate ratios (either term zero, or + `actual == notated`); its fields are private, so a degenerate `TupletRatio` + is never representable, and codec decode re-validates through the same + constructor. (Pass 11 item 3.5 moved this from a runtime invariant-16 + sub-check to a construction-time MUST.) diff --git a/crates/epiphany-core/src/codec.rs b/crates/epiphany-core/src/codec.rs index 1e82438..440566a 100644 --- a/crates/epiphany-core/src/codec.rs +++ b/crates/epiphany-core/src/codec.rs @@ -2065,6 +2065,30 @@ mod tests { ); } + #[test] + fn degenerate_tuplet_ratio_is_rejected_on_decode() { + // Guards the TupletRatio::dec re-validation (Pass 11 item 3.5): a + // hand-crafted byte stream must not be able to inject a degenerate ratio + // that TupletRatio::new would reject at construction. Without the + // `.ok_or(Reconstruct)` in dec, these would decode into an + // unconstructible-by-API value. + let decode = |actual: u32, notated: u32| { + let mut bytes = Vec::new(); + actual.enc(&mut bytes); + notated.enc(&mut bytes); + TupletRatio::dec(&mut Reader::new(&bytes)) + }; + for (a, n) in [(0u32, 0u32), (2, 0), (0, 2), (4, 4)] { + assert!( + matches!(decode(a, n), Err(ScoreDecodeError::Reconstruct(_))), + "degenerate ratio {a}:{n} must be rejected on decode" + ); + } + // A well-formed ratio still decodes. + let ok = decode(3, 2).expect("non-degenerate ratio decodes"); + assert_eq!((ok.actual(), ok.notated()), (3, 2)); + } + #[test] fn exotic_event_and_pitch_variants_round_trip() { // Round-trip is structural, so this need not satisfy graph invariants — diff --git a/crates/epiphany-layout-ir/DECISIONS.md b/crates/epiphany-layout-ir/DECISIONS.md index 1dc5c79..2046632 100644 --- a/crates/epiphany-layout-ir/DECISIONS.md +++ b/crates/epiphany-layout-ir/DECISIONS.md @@ -9,11 +9,15 @@ 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 +> (`req:layoutir:object-id-derivation`): the spec **pins** 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 the `MUSCLOID` tag is **flagged for Track A +> and not yet wired in code**: this crate still mints provisional ids (untagged; +> synthesized borrows `MUSCCONF`) because the frozen determinism crate exposes no +> `MUSCLOID` tag (see the `stable_layout_id` bullet below). Adopting the spec'd +> derivation is Track A (solver/renderer) work. layout P11-1 (layout→ops +> dependency) stays a crate-topology call for the G–K re-cut. See > `spec/PASS11_RATIFICATION_LOG.md`. ## Scope @@ -88,7 +92,11 @@ object is covered); the provenance-preservation contract itself is unchanged. reordering of other objects (Chapter 7 §"Provenance"). It is not domain- separated, and the engraving-decision id borrows the `MUSCCONF` tag with a literal `engraving-decision` type prefix, because the frozen determinism crate - (Agent A) defines no layout-object domain tag. See Pass 11 candidate 3. + (Agent A) defines no layout-object domain tag. Pass 11 ratified the **target** + derivation — a `MUSCLOID`-tagged hash (`req:layoutir:object-id-derivation`) — + but adopting it is Track A work (the determinism crate must first expose the + layout-namespace tag); these ids stay provisional until then. See the header + note. - **Repeated manifestations get per-`(source, region)` ids.** A score-graph object manifested within a region is laid out **per manifestation**: its stable diff --git a/crates/epiphany-layout-ir/src/provenance.rs b/crates/epiphany-layout-ir/src/provenance.rs index b92fa81..002014e 100644 --- a/crates/epiphany-layout-ir/src/provenance.rs +++ b/crates/epiphany-layout-ir/src/provenance.rs @@ -136,8 +136,9 @@ impl Provenance { /// stable across re-layouts where the source is unchanged): inserting, /// removing, or reordering other objects cannot change any object's stable id, /// because each depends solely on its own source's canonical bytes. v0 derives -/// it as `trunc128(BLAKE3(source.canonical_bytes()))`; see `DECISIONS.md` for -/// why this is not (yet) domain-separated. +/// it as `trunc128(BLAKE3(source.canonical_bytes()))` — a provisional, untagged +/// stand-in. The spec (`req:layoutir:object-id-derivation`) pins a +/// `MUSCLOID`-tagged derivation as the Track A target; see `DECISIONS.md`. pub fn stable_layout_id(source: &TypedObjectId) -> LayoutObjectId { LayoutObjectId(trunc128(&blake3_256(&source.canonical_bytes()))) } @@ -155,9 +156,11 @@ pub fn manifestation_layout_id(source: &TypedObjectId, region: RegionId) -> Layo /// Derives the stable layout id of an **engraver-synthesized** object from its /// `source` and its [`SynthesisKind`], so distinct synthesis kinds from one -/// source do not collide (Chapter 7 §"Provenance"). Domain-tagged via the -/// borrowed `MUSCCONF` tag with a `synthesized` discriminator prefix (the -/// determinism crate defines no layout tag — see `DECISIONS.md`). +/// source do not collide (Chapter 7 §"Provenance"). Provisionally domain-tagged +/// via the borrowed `MUSCCONF` tag with a `synthesized` discriminator prefix; the +/// determinism crate defines no `MUSCLOID` layout tag yet, and the spec'd +/// `MUSCLOID` derivation (`req:layoutir:object-id-derivation`) is the Track A +/// target — see `DECISIONS.md`. pub fn synthesized_layout_id( source: &TypedObjectId, kind: SynthesisKind, diff --git a/crates/epiphany-ops/src/reduce.rs b/crates/epiphany-ops/src/reduce.rs index ceab209..c12ea96 100644 --- a/crates/epiphany-ops/src/reduce.rs +++ b/crates/epiphany-ops/src/reduce.rs @@ -731,8 +731,11 @@ impl<'a> Reducer<'a> { // --- Voice promotion pre-pass (Chapter 6 §6.10 InsertEvent). ------------ fn compute_promotions(&mut self, active: &[&OperationEnvelope]) { - // Bucket inserts by target voice. Promotion applies only to concurrent - // operations whose half-open duration intervals overlap. + // Bucket inserts by target voice. Bucketing by `op.voice` alone realizes + // the spec's `(staff_instance, original_voice)` key: a VoiceId is + // globally unique and (Invariant 5) belongs to exactly one staff + // instance, so the voice id alone determines the pair. Promotion applies + // only to concurrent operations whose half-open duration intervals overlap. let mut buckets: BTreeMap> = BTreeMap::new(); for env in active { if let OperationPayload::Primitive(OperationKind::InsertEvent(op)) = &env.payload { diff --git a/spec/PASS11_RATIFICATION_LOG.md b/spec/PASS11_RATIFICATION_LOG.md index d939d72..a24563e 100644 --- a/spec/PASS11_RATIFICATION_LOG.md +++ b/spec/PASS11_RATIFICATION_LOG.md @@ -31,7 +31,7 @@ unchanged. The full worklist is `PASS11_WORKLIST.md`. | 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 | +| 2.6 Layout-object id | layout P11-2 | **decided: spec pins `MUSCLOID` tag; code adoption is Track A** — the spec specifies a `MUSCLOID`-tagged derivation keying multiply-manifested objects on `(source, region)`, synthesized objects on `(source, synthesis_kind, stable_semantic_instance_key)`. Non-canonical (not document state). The v0 `layout-ir` crate still mints **provisional** ids (untagged; synthesized borrows `MUSCCONF`) because the frozen determinism crate exposes no `MUSCLOID` tag — realizing the spec'd derivation is Track A work, not done in this pass | §"Provenance", `req:layoutir:object-id-derivation` | `layout-ir` provenance (provisional) | ## Bucket 3 — Fixes (spec was contradictory or silent) diff --git a/spec/core_spec.pdf b/spec/core_spec.pdf index ef4d8ed..1540909 100644 Binary files a/spec/core_spec.pdf and b/spec/core_spec.pdf differ diff --git a/spec/core_spec.tex b/spec/core_spec.tex index c3886e8..330bcd1 100644 --- a/spec/core_spec.tex +++ b/spec/core_spec.tex @@ -3690,7 +3690,7 @@ pub enum ObjectKind { Section~\ref{sec:graph:system-derived} with the reserved domain tag \texttt{"MUSCSANM"} over the anomaly kind's canonical bytes: \texttt{derive\_system\_id::(b"MUSCSANM", - \&kind.canonical\_bytes())}. Because the identity is content-derived + \&kind.to\_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. @@ -7898,9 +7898,9 @@ pub struct LayoutObjectId(pub u128); \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 + underlying source is unchanged. It \MUST{} be 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} @@ -7928,7 +7928,13 @@ pub struct LayoutObjectId(pub u128); 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. + (Track~A), not the interchange track. Because these ids never enter + document state, no stored or interchanged artifact depends on this + derivation; the v0 reference crate accordingly mints layout ids with + a provisional, untagged stand-in (synthesized objects borrowing the + \texttt{"MUSCCONF"} tag), and wiring the \texttt{"MUSCLOID"} + derivation above is Track~A work rather than a property the prototype + yet realizes. \end{requirement} \section{The Stage Pipeline} @@ -13991,8 +13997,10 @@ state. 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 + operation; and specified the (non-canonical) \texttt{LayoutObjectId} + derivation with a \texttt{MUSCLOID} tag as the Track~A target (the v0 + prototype still mints provisional layout ids; wiring \texttt{MUSCLOID} + is Track~A work). \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}