Pass 11 audit follow-up: honest LayoutObjectId status + doc/test gaps

Independent audit of b2f2e20 / a7adbdc. The canonical, document-state byte
layouts were already correct and golden-locked; this closes the one real
spec-vs-code gap (in the non-canonical layout namespace) and the smaller
doc/test gaps around it.

LayoutObjectId (item 2.6): the spec normatively stated the id "is derived ...
with MUSCLOID" and the ratification log / layout DECISIONS header called it
"pinned" -- but no code uses MUSCLOID (stable_layout_id and
manifestation_layout_id are untagged; synthesized_layout_id borrows MUSCCONF),
and MUSCLOID cannot even be constructed: DomainTag::from_bytes accepts only
built-ins or MUSCS-prefixed tags, so wiring it requires changing the frozen
determinism crate (out of scope for a spec pass, and the spec itself says
MUSCLOID is not a canonical system tag). Kept MUSCLOID as the pinned Track-A
target and made every artifact honest that the v0 code is provisional:
  - spec: "is derived" -> "MUST be derived" (forward contract) + a note that
    the prototype mints provisional ids; changelog "pinned" -> "specified ...
    as the Track-A target".
  - records: ratification-log line 2.6 and layout DECISIONS header/body now
    state spec-pinned-but-code-provisional; stale "Pass 11 candidate 3"
    pointer now cites the ratified requirement.
  - provenance.rs comments name the MUSCLOID target and label the current
    derivation provisional.

Other fixes:
  - epiphany-core/DECISIONS.md: the Tuplet bullet still claimed degenerate
    ratios are caught by runtime invariant 16 "since a Tuplet is a plain
    struct" -- stale after the construction-time TupletRatio change. Rewritten.
  - codec.rs: added degenerate_tuplet_ratio_is_rejected_on_decode, guarding
    the TupletRatio::dec re-validation branch that no test exercised (the
    constructor was tested, the decode path was not).
  - spec: integrity-anomaly snippet kind.canonical_bytes() ->
    to_canonical_bytes() (the actual method).
  - ids.rs / reduce.rs: clarifying comments (ManifestId's intentional,
    golden-locked document_id/generation duplication; compute_promotions
    bucketing by voice == (staff_instance, original_voice) via Invariant 5).

Verification: cargo test --workspace (434 pass, +1), clippy --all-targets clean
(0 warnings), fmt clean; spec rebuilds (lualatex, 0 undefined refs, 254pp).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-06-22 10:02:05 -04:00
parent a7adbdc5a1
commit 0d8ec61a3c
9 changed files with 77 additions and 23 deletions

View File

@ -101,6 +101,11 @@ impl ManifestId {
/// core_spec §"Manifest Encoding", Requirement `req:format:manifest-id` /// core_spec §"Manifest Encoding", Requirement `req:format:manifest-id`
/// (`trunc128(BLAKE3("MUSCMNIF" || document_id || generation || body))`, /// (`trunc128(BLAKE3("MUSCMNIF" || document_id || generation || body))`,
/// body excluding `manifest_id`). /// 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 { pub(crate) fn derive(document_id: DocumentId, generation: u64, body_preimage: &[u8]) -> Self {
let mut p = Preimage::new(DomainTag::MANIFEST_ID); let mut p = Preimage::new(DomainTag::MANIFEST_ID);
p.push_bytes(document_id.as_bytes()); p.push_bytes(document_id.as_bytes());

View File

@ -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 - `TimeSignature::new` rejects beat groups that do not sum to the measure
duration. duration.
- `EventOrderingDAG::try_new` rejects a cyclic aleatoric ordering. - `EventOrderingDAG::try_new` rejects a cyclic aleatoric ordering.
- `Tuplet` degenerate ratios (`0:n`/`n:0`) are caught by invariant 16 - `TupletRatio::new` rejects degenerate ratios (either term zero, or
(`check_invariants`), since a `Tuplet` is a plain struct. `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.)

View File

@ -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] #[test]
fn exotic_event_and_pitch_variants_round_trip() { fn exotic_event_and_pitch_variants_round_trip() {
// Round-trip is structural, so this need not satisfy graph invariants — // Round-trip is structural, so this need not satisfy graph invariants —

View File

@ -9,11 +9,15 @@ items batched."*).
> **RATIFIED (Pass 11, 2026-06-21).** layout P11-2 (`LayoutObjectId` derivation) > **RATIFIED (Pass 11, 2026-06-21).** layout P11-2 (`LayoutObjectId` derivation)
> is ratified into `core_spec.tex` §"Provenance" > is ratified into `core_spec.tex` §"Provenance"
> (`req:layoutir:object-id-derivation`): a `MUSCLOID`-tagged derivation keying > (`req:layoutir:object-id-derivation`): the spec **pins** a `MUSCLOID`-tagged
> multiply-manifested objects on `(source, region)` and synthesized objects on > derivation keying multiply-manifested objects on `(source, region)` and
> `(source, synthesis_kind, stable_semantic_instance_key)`. Layout ids are > synthesized objects on `(source, synthesis_kind, stable_semantic_instance_key)`.
> non-canonical, so this is flagged for Track A (solver/renderer). layout P11-1 > Layout ids are non-canonical, so the `MUSCLOID` tag is **flagged for Track A
> (layout→ops dependency) stays a crate-topology call for the GK re-cut. See > 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 GK re-cut. See
> `spec/PASS11_RATIFICATION_LOG.md`. > `spec/PASS11_RATIFICATION_LOG.md`.
## Scope ## 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- reordering of other objects (Chapter 7 §"Provenance"). It is not domain-
separated, and the engraving-decision id borrows the `MUSCCONF` tag with a separated, and the engraving-decision id borrows the `MUSCCONF` tag with a
literal `engraving-decision` type prefix, because the frozen determinism crate 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 - **Repeated manifestations get per-`(source, region)` ids.** A score-graph
object manifested within a region is laid out **per manifestation**: its stable object manifested within a region is laid out **per manifestation**: its stable

View File

@ -136,8 +136,9 @@ impl Provenance {
/// stable across re-layouts where the source is unchanged): inserting, /// stable across re-layouts where the source is unchanged): inserting,
/// removing, or reordering other objects cannot change any object's stable id, /// 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 /// because each depends solely on its own source's canonical bytes. v0 derives
/// it as `trunc128(BLAKE3(source.canonical_bytes()))`; see `DECISIONS.md` for /// it as `trunc128(BLAKE3(source.canonical_bytes()))` — a provisional, untagged
/// why this is not (yet) domain-separated. /// 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 { pub fn stable_layout_id(source: &TypedObjectId) -> LayoutObjectId {
LayoutObjectId(trunc128(&blake3_256(&source.canonical_bytes()))) 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 /// Derives the stable layout id of an **engraver-synthesized** object from its
/// `source` and its [`SynthesisKind`], so distinct synthesis kinds from one /// `source` and its [`SynthesisKind`], so distinct synthesis kinds from one
/// source do not collide (Chapter 7 §"Provenance"). Domain-tagged via the /// source do not collide (Chapter 7 §"Provenance"). Provisionally domain-tagged
/// borrowed `MUSCCONF` tag with a `synthesized` discriminator prefix (the /// via the borrowed `MUSCCONF` tag with a `synthesized` discriminator prefix; the
/// determinism crate defines no layout tag — see `DECISIONS.md`). /// 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( pub fn synthesized_layout_id(
source: &TypedObjectId, source: &TypedObjectId,
kind: SynthesisKind, kind: SynthesisKind,

View File

@ -731,8 +731,11 @@ impl<'a> Reducer<'a> {
// --- Voice promotion pre-pass (Chapter 6 §6.10 InsertEvent). ------------ // --- Voice promotion pre-pass (Chapter 6 §6.10 InsertEvent). ------------
fn compute_promotions(&mut self, active: &[&OperationEnvelope]) { fn compute_promotions(&mut self, active: &[&OperationEnvelope]) {
// Bucket inserts by target voice. Promotion applies only to concurrent // Bucket inserts by target voice. Bucketing by `op.voice` alone realizes
// operations whose half-open duration intervals overlap. // 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<VoiceId, Vec<&OperationEnvelope>> = BTreeMap::new(); let mut buckets: BTreeMap<VoiceId, Vec<&OperationEnvelope>> = BTreeMap::new();
for env in active { for env in active {
if let OperationPayload::Primitive(OperationKind::InsertEvent(op)) = &env.payload { if let OperationPayload::Primitive(OperationKind::InsertEvent(op)) = &env.payload {

View File

@ -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.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.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.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) ## Bucket 3 — Fixes (spec was contradictory or silent)

Binary file not shown.

View File

@ -3690,7 +3690,7 @@ pub enum ObjectKind {
Section~\ref{sec:graph:system-derived} with the reserved domain tag Section~\ref{sec:graph:system-derived} with the reserved domain tag
\texttt{"MUSCSANM"} over the anomaly kind's canonical bytes: \texttt{"MUSCSANM"} over the anomaly kind's canonical bytes:
\texttt{derive\_system\_id::<IntegrityAnomalyId>(b"MUSCSANM", \texttt{derive\_system\_id::<IntegrityAnomalyId>(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 from the kind, two replicas observing the same structural failure
derive the same anomaly identifier and therefore agree on anomaly derive the same anomaly identifier and therefore agree on anomaly
identity across the network. identity across the network.
@ -7898,9 +7898,9 @@ pub struct LayoutObjectId(pub u128);
\begin{requirement} \begin{requirement}
\label{req:layoutir:object-id-derivation} \label{req:layoutir:object-id-derivation}
A \texttt{LayoutObjectId} \MUST{} be stable across re-layouts whose A \texttt{LayoutObjectId} \MUST{} be stable across re-layouts whose
underlying source is unchanged. It is derived by domain-separated underlying source is unchanged. It \MUST{} be derived by
BLAKE3 truncation with the reserved layout domain tag domain-separated BLAKE3 truncation with the reserved layout domain
\texttt{"MUSCLOID"} over a key that depends on how the object is tag \texttt{"MUSCLOID"} over a key that depends on how the object is
manifested: manifested:
\begin{itemize} \begin{itemize}
@ -7928,7 +7928,13 @@ pub struct LayoutObjectId(pub u128);
Section~\ref{sec:graph:system-derived}. The fixed derivation is Section~\ref{sec:graph:system-derived}. The fixed derivation is
pinned for incremental-relayout correctness and provenance pinned for incremental-relayout correctness and provenance
back-reference stability; its consumers are the solver and renderer 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} \end{requirement}
\section{The Stage Pipeline} \section{The Stage Pipeline}
@ -13991,8 +13997,10 @@ state.
pinned the \texttt{TransactionCategory} and \texttt{ObjectKind} core pinned the \texttt{TransactionCategory} and \texttt{ObjectKind} core
vocabularies; added \texttt{ResolutionAction::Dismiss} so the vocabularies; added \texttt{ResolutionAction::Dismiss} so the
\texttt{Dismissed} resolution state is reachable by an authored \texttt{Dismissed} resolution state is reachable by an authored
operation; and pinned the (non-canonical) \texttt{LayoutObjectId} operation; and specified the (non-canonical) \texttt{LayoutObjectId}
derivation with a \texttt{MUSCLOID} tag. \emph{Fixes:} blob hashing 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 is the bare \texttt{"MUSCBLOB" || payload} (deleted the
contradictory ``identically to chunks'' phrasing); added the contradictory ``identically to chunks'' phrasing); added the
equal-generation superblock rule (\texttt{DivergentSameGeneration} equal-generation superblock rule (\texttt{DivergentSameGeneration}