diff --git a/crates/epiphany-core/DECISIONS.md b/crates/epiphany-core/DECISIONS.md index 5645589..d88cbda 100644 --- a/crates/epiphany-core/DECISIONS.md +++ b/crates/epiphany-core/DECISIONS.md @@ -1491,3 +1491,38 @@ not `Option`-hidden, so there is no lower-major layout for this payload to fall back to. This is the sole surface among the nine genesis-tranche settings/creates that drags `OperationEnvelopeBlock`'s accept-set from 2 to 3 (`epiphany-bundle`'s `DECISIONS.md`). + +## Genesis tranche G3a — the four root-level entity mints join +## `canonical_value!`, and `Staff.group`/`StaffGroup.members` gain their +## authority doc comments (2026-07-29) + +`spec/CONTRACT_GENESIS_G3A_ENTITIES.md` adds `CreateStaffGroup`, +`CreatePartDefinition`, `CreateAnalysisLayer`, and `CreateView` to +`epiphany-ops`, closing G3a of the genesis ladder (`spec/PLAN_GENESIS_OPS.md` +§4). All four carried types — `StaffGroup`, `PartDefinition`, `AnalysisLayer`, +`ViewDefinition` — already had a `Codec` **and** a `TextValue` (both generated +by the one `struct_codec!` macro, `codec.rs:510`/`:522`), so this crate's one +required *code* change is four more `canonical_value!` lines. No new byte +layout, same as G1/G2a/G2b's entries above; `textvalue_graph.rs` needed no +change at all — the cheapest surface this tranche touches. + +All four are schema major 0 as standalone payloads: neither `decode_v0_score` +nor the live `Codec` walk has ever versioned any of the four leaf types, so +none gains a `schema_major()` arm in `epiphany-ops` — all four fall into the +existing `_ => 0` catch-all, exactly G2a's shape, not G1/G2b's. Consequently no +`epiphany-bundle` change of any kind: the op-block accept-set stays at 3 where +G2b left it. + +**Pin 4b.** `spec/CONTRACT_GENESIS_G3A_ENTITIES.md` §1.1 (disposition B, +ratified 2026-07-29) rules that `Staff.group` is the sole authority for group +membership and `StaffGroup.members` is a non-authoritative denormalized +projection that G3a stores but neither maintains nor trusts; both the missing- +member and the spurious-member stale forms are permitted. Both fields gained a +doc comment stating this — neither had one before — since a ruling that lives +only in a contract and a candidate ledger (filed as P13-S16) is not normative +until the fields themselves say so. `invariants.rs`'s invariant-10 doc comment +was also repaired (pin 6, doc-only — no enum entry, no discriminant, no +behaviour change): its body already resolved a staff's group, a group's +members, a part's staves, a view's active layers, and measure/grid +time-signature references; the doc comment previously named only cross-cutting +structures and event-internal references. diff --git a/crates/epiphany-core/src/codec.rs b/crates/epiphany-core/src/codec.rs index 3d555f9..971b985 100644 --- a/crates/epiphany-core/src/codec.rs +++ b/crates/epiphany-core/src/codec.rs @@ -3585,6 +3585,19 @@ canonical_value! { // `ScoreTuningContext`'s five-field walk; this makes that layout // reachable per-value, exactly as `ScoreTuningContext` above is. TuningContextSettings, + // Genesis tranche G3a (`CONTRACT_GENESIS_G3A_ENTITIES.md` pin 3) — the + // four root-level mints (`CreateStaffGroup`, `CreatePartDefinition`, + // `CreateAnalysisLayer`, `CreateView`) embed the full value, mirroring + // `CreateStaff`'s `Staff` / `CreateInstrument`'s `Instrument`. All four + // already have a `Codec` (`struct_codec!` at `:1790`–`:1796` and `:2329`) + // and already ship inside `Score`; this makes those existing layouts + // reachable per-value, same as every other entry here. No new byte + // layout, and no `textvalue_graph.rs` work — `struct_codec!` already + // generated each type's `TextValue` impl alongside its `Codec`. + StaffGroup, + PartDefinition, + AnalysisLayer, + ViewDefinition, } #[cfg(test)] diff --git a/crates/epiphany-core/src/graph.rs b/crates/epiphany-core/src/graph.rs index 0982835..dbe7199 100644 --- a/crates/epiphany-core/src/graph.rs +++ b/crates/epiphany-core/src/graph.rs @@ -816,6 +816,12 @@ pub struct Staff { pub abbreviation: Option, pub instrument: InstrumentId, pub default_staff_lines: StaffLineConfiguration, + /// Which staff group (if any) this staff belongs to. **The sole authority + /// for group membership** (genesis tranche G3a, + /// `spec/CONTRACT_GENESIS_G3A_ENTITIES.md` §1.1, disposition B, filed as + /// P13-S16): every consumer MUST read membership from this field, not + /// from [`StaffGroup::members`], which is a non-authoritative denormalized + /// projection that may disagree with this field in either direction. pub group: Option, /// Default clef for new instances of this staff (schema major 2, /// appended last per the wire rule; migration default treble). @@ -1611,6 +1617,13 @@ pub struct StaffGroup { pub id: StaffGroupId, pub name: Option, pub kind: StaffGroupKind, + /// A **non-authoritative denormalized projection** of group membership + /// (genesis tranche G3a, `spec/CONTRACT_GENESIS_G3A_ENTITIES.md` §1.1, + /// disposition B, filed as P13-S16). [`Staff::group`] is the sole + /// authority: this field MUST NOT be read to decide whether a staff is in + /// a group, and MAY be stale in **both** directions — a member missing + /// here while `Staff.group` names this group, or a staff listed here + /// while its own `Staff.group` is `None` or names a different group. pub members: Vec, } @@ -2066,3 +2079,82 @@ mod tests { assert!(EventOrderingDAG::default().is_acyclic()); } } + +/// Genesis tranche G3a (`spec/CONTRACT_GENESIS_G3A_ENTITIES.md` pin 4b): the +/// authority-rule doc-comment guards for `Staff.group` and +/// `StaffGroup.members`. +#[cfg(test)] +mod g3a_tests { + const SOURCE: &str = include_str!("graph.rs"); + + /// The production portion of this file only, ending right before the + /// first `#[cfg(test)]` module. Every needle this module searches for is + /// itself written, as a string literal, inside *this* test module — so + /// searching the whole file (including this module) risks the exact + /// self-matching trap `spec/CONTRACT_GENESIS_G3A_ENTITIES.md` §4 warns + /// about: a needle that matches the guard's own source cannot fail. + /// Restricting the haystack to the code *above* any test module removes + /// that risk structurally, rather than relying on needle length alone. + fn production_source() -> &'static str { + SOURCE + .split_once("#[cfg(test)]") + .map(|(before, _)| before) + .expect("this file contains at least one #[cfg(test)] module") + } + + /// (t14) `Staff.group`'s doc comment states it is authoritative for + /// group membership. Grep-assert, **sliced to that field's doc block + /// only** — `graph.rs` mentions `group` throughout, so a file-wide + /// search cannot fail. + /// + /// **Mutation:** delete the doc comment (revert to no doc comment on + /// this field, its pre-G3a state); must fail. + #[test] + fn t14_staff_group_field_doc_comment_states_sole_authority() { + let source = production_source(); + let start = source + .find(" /// Which staff group (if any) this staff belongs to.") + .expect("Staff.group's doc comment is present"); + let end = source[start..] + .find("pub group: Option,") + .map(|offset| start + offset) + .expect("the `group` field declaration follows its doc comment"); + let doc_block = &source[start..end]; + assert!( + doc_block.contains("sole authority"), + "Staff.group's doc comment must state it is the sole authority; block was:\n{doc_block}" + ); + } + + /// (t14) `StaffGroup.members`'s doc comment states it is a + /// non-authoritative projection that may be stale in **both** + /// directions. Grep-assert, **sliced to that field's doc block only** — + /// same discipline as the `Staff.group` guard above. + /// + /// **Mutation:** delete the doc comment (revert to no doc comment on + /// this field, its pre-G3a state); must fail. + #[test] + fn t14_staff_group_members_field_doc_comment_states_non_authoritative_projection() { + let source = production_source(); + let start = source + .find(" /// A **non-authoritative denormalized projection** of group") + .expect("StaffGroup.members's doc comment is present"); + let end = source[start..] + .find("pub members: Vec,") + .map(|offset| start + offset) + .expect("the `members` field declaration follows its doc comment"); + let doc_block = &source[start..end]; + assert!( + doc_block.contains("non-authoritative"), + "StaffGroup.members's doc comment must state it is non-authoritative; block was:\n{doc_block}" + ); + assert!( + doc_block.contains("MUST NOT be read"), + "StaffGroup.members's doc comment must forbid reading it to decide membership; block was:\n{doc_block}" + ); + assert!( + doc_block.contains("both** directions"), + "StaffGroup.members's doc comment must permit staleness in both directions; block was:\n{doc_block}" + ); + } +} diff --git a/crates/epiphany-core/src/invariants.rs b/crates/epiphany-core/src/invariants.rs index 4bf383f..75eb524 100644 --- a/crates/epiphany-core/src/invariants.rs +++ b/crates/epiphany-core/src/invariants.rs @@ -59,7 +59,15 @@ pub enum GraphInvariant { /// 10. Every graph reference resolves to an extant object: cross-cutting /// structures (incl. anchor targets, annotation layers, tuplet parents, /// graphic objects) and event-internal references (indeterminate - /// alternatives, trajectory event-pitches, graphic objects, cue sources). + /// alternatives, trajectory event-pitches, graphic objects, cue sources); + /// structural top-level references (a staff's declared instrument, a + /// staff's group, a staff group's members, a part's staves, a view's + /// active layers — genesis tranche G3a repairs this prose to name what + /// the check body already enforced); and meter/time-signature + /// references at every level a `MeterChange` can appear (a region's + /// time-model meter changes, a region's default metric grid, a + /// measure's declared time signature, a staff instance's local metric + /// grid). CrossCuttingRefsResolve, /// 11. Identifiers are unique within their kind (every id kind), with /// reserved-namespace (`SYSTEM_DERIVED`) misuse, tombstone/live @@ -4074,3 +4082,60 @@ mod accidental_compatibility_tests { .all(|v| !v.witness.contains("accidental-modification-compatibility"))); } } + +/// Genesis tranche G3a (`spec/CONTRACT_GENESIS_G3A_ENTITIES.md` pin 6): the +/// invariant-10 doc-comment reconciliation guard. +#[cfg(test)] +mod g3a_tests { + const SOURCE: &str = include_str!("invariants.rs"); + + /// The production portion of this file only, ending right before the + /// first `#[cfg(test)]` module. Every needle this module searches for is + /// itself written, as a string literal, inside a test module — so + /// searching the whole file risks the exact self-matching trap + /// `spec/CONTRACT_GENESIS_G3A_ENTITIES.md` §4 warns about (G2b hit it + /// twice: once via a needle matching the guard's own source, once via an + /// assertion message). Restricting the haystack structurally removes + /// that risk rather than relying on needle length alone. + fn production_source() -> &'static str { + SOURCE + .split_once("#[cfg(test)]") + .map(|(before, _)| before) + .expect("this file contains at least one #[cfg(test)] module") + } + + /// (t12) Invariant 10's doc comment names the four reference classes its + /// body checks (pin 6). Grep-assert the repaired prose is present + /// **within the invariant-10 doc block only** — slice the source from + /// the `/// 10.` line to the `CrossCuttingRefsResolve,` line and search + /// *that*, since searching the whole file would pass on the + /// implementation body, which contains the same identifiers the doc + /// comment is supposed to gain. + /// + /// **Mutation:** revert the doc comment to its pre-G3a text (naming only + /// cross-cutting structures and event-internal references); must fail. + #[test] + fn t12_invariant_10_doc_comment_names_the_four_reference_classes() { + let source = production_source(); + let start = source + .find(" /// 10. Every graph reference resolves") + .expect("invariant 10's doc comment is present"); + let end = source[start..] + .find("CrossCuttingRefsResolve,") + .map(|offset| start + offset) + .expect("the CrossCuttingRefsResolve variant follows its doc comment"); + let doc_block = &source[start..end]; + + for needle in [ + "staff's group", + "group's members", + "part's staves", + "active layers", + ] { + assert!( + doc_block.contains(needle), + "invariant 10's doc comment must name `{needle}`; block was:\n{doc_block}" + ); + } + } +} diff --git a/crates/epiphany-editor-core/src/barriers.rs b/crates/epiphany-editor-core/src/barriers.rs index 2ee2c8a..10a5283 100644 --- a/crates/epiphany-editor-core/src/barriers.rs +++ b/crates/epiphany-editor-core/src/barriers.rs @@ -445,6 +445,27 @@ pub(crate) fn subjects_of(kind: &OperationKind, score: &Score) -> BarrierSubject TypedObjectId::Instrument(op.instrument_id()), EditContext::default(), ), + // Genesis tranche G3a: four more root-level `Score` entity mints, + // exactly like `CreateStaff`/`CreateInstrument` above — each names + // only the object it mints, with no resolvable region or + // staff-instance context, regardless of whether the carried value + // holds an outbound reference (`CreateStaffGroup.members`, + // `CreatePartDefinition.staves`, `CreateView.active_layers`). + OperationKind::CreateStaffGroup(op) => one( + TypedObjectId::StaffGroup(op.staff_group_id()), + EditContext::default(), + ), + OperationKind::CreatePartDefinition(op) => one( + TypedObjectId::PartDefinition(op.part_definition_id()), + EditContext::default(), + ), + OperationKind::CreateAnalysisLayer(op) => one( + TypedObjectId::AnalysisLayer(op.analysis_layer_id()), + EditContext::default(), + ), + OperationKind::CreateView(op) => { + one(TypedObjectId::View(op.view_id()), EditContext::default()) + } OperationKind::SetTimeSignature(op) => { let mut objects = vec![(TypedObjectId::Region(op.region), ctx(Some(op.region), None))]; if let Some(signature) = &op.time_signature { diff --git a/crates/epiphany-layout-ir/src/barrier.rs b/crates/epiphany-layout-ir/src/barrier.rs index f18e85b..a3248c7 100644 --- a/crates/epiphany-layout-ir/src/barrier.rs +++ b/crates/epiphany-layout-ir/src/barrier.rs @@ -1153,28 +1153,30 @@ mod tests { tag: 7 }) ); - // Operation-kind tag 35 is one past the vocabulary (the Phase-3 ops + // Operation-kind tag 39 is one past the vocabulary (the Phase-3 ops // tranche appended 24..=27, the repeat pair 28/29, `TransposeInterval` // 30, genesis G1's `CreateInstrument` 31, genesis G2a's // `SetCanvasLayoutDefaults` 32 and `SetSpellingPrecedence` 33, - // genesis G2b's `SetTuningContext` 34; encodings are append-only). + // genesis G2b's `SetTuningContext` 34, genesis G3a's + // `CreateStaffGroup`/`CreatePartDefinition`/`CreateAnalysisLayer`/ + // `CreateView` 35..=38; encodings are append-only). // // This assertion named 30 until Push 5 / P4, 31 until genesis G1, 32 - // until genesis G2a, and 34 until genesis G2b — each time, by then, - // the number had become a real kind, so the test was pinning a bug: a - // barrier that prohibited the new operation encoded fine and would - // not read back. It must be bumped by every tranche that appends a - // tag, and it is deliberately a literal rather than - // `PAYLOAD_FREE.len()` so the bump is a conscious act. + // until genesis G2a, 34 until genesis G2b, and 35 until genesis G3a — + // each time, by then, the number had become a real kind, so the test + // was pinning a bug: a barrier that prohibited the new operation + // encoded fine and would not read back. It must be bumped by every + // tranche that appends a tag, and it is deliberately a literal rather + // than `PAYLOAD_FREE.len()` so the bump is a conscious act. let mut bytes = vec![0u8]; bytes.extend(set_blob(&[])); - bytes.extend(set_blob(&[vec![35u8]])); + bytes.extend(set_blob(&[vec![39u8]])); bytes.push(0); assert_eq!( EditBarrier::decode_canonical_bytes(&bytes), Err(BarrierDecodeError::InvalidTag { kind: "OperationKindTag", - tag: 35 + tag: 39 }) ); } diff --git a/crates/epiphany-ops/DECISIONS.md b/crates/epiphany-ops/DECISIONS.md index 3f0d48b..79073c3 100644 --- a/crates/epiphany-ops/DECISIONS.md +++ b/crates/epiphany-ops/DECISIONS.md @@ -1957,3 +1957,106 @@ inferred. tuning context (the canonical base embeds no graph value for any `Score` field), which is exactly why a future prune must stay blocked on disposition C; this packet adds no `fn prune` and no scaffolding toward one. + +## Genesis tranche G3a — `CreateStaffGroup`/`CreatePartDefinition`/ +## `CreateAnalysisLayer`/`CreateView`, kinds/tags 35–38 (2026-07-29) + +`spec/CONTRACT_GENESIS_G3A_ENTITIES.md` lands the fourth rung: the four +remaining root-level `Score` entity mints, closing the from-empty +satisfiability gap G1 opened (`CreateStaff`'s `group` precondition was +unsatisfiable under from-empty reduction until a `StaffGroup` could be +minted). All four ride `create_staff`'s set-union mint discipline exactly +(fresh id mints; byte-identical re-carry is idempotent; a differing value +under a live id is `RecreateContentMismatch`; a tombstoned id is +`TargetTombstoned`), all at schema major 0 (pin 2: **no** `schema_major()` +arm — the catch-all `_ => 0` is correct, G2a's shape not G1/G2b's), all at +epoch 11. + +**Pin 4a: four more carried-value maps, 28 touch points.** +`staff_group_values`, `part_definition_values`, `analysis_layer_values`, +`view_values` join `staff_values`/`time_signature_values`/`instrument_values` +at the same seven sites each: reducer state declaration, `WorkingSnapshot` +declaration, initialization, base seeding (`seed_from_graph` — the site that +fails silently without it, per G1's documented `instrument_values` hazard), +mint insertion, snapshot, restore. Site 4 is load-bearing and separately +mutation-tested (t13, four sub-mutations, one per family) against a dedicated +base-recarry test (t5b) that a from-empty-only re-carry test (t5) structurally +cannot exercise. + +**The tombstoned branch is implemented but, like `create_staff`'s and +`create_instrument`'s, not exercised by any test.** No delete exists for any +of these six mint-only families (`CreateStaff`, `CreateInstrument`, and now +the four G3a kinds), so `ObjectState::Tombstoned` is unreachable for any of +them through the public operation API today. This is the pre-existing +project convention, not a new gap this rung introduces. + +**§1.1, disposition B — the `Staff.group`/`StaffGroup.members` authority +ruling, and why `create_staff_group` does the least possible thing.** +`CreateStaffGroup` carries `members` **exactly as given** — no normalization, +no validation beyond the graph-aware liveness loop (pin 4) — and reduction +never writes `StaffGroup.members` from any other operation. `Staff.group` is +the sole authority; `StaffGroup.members` is a stored, non-authoritative, +denormalized projection that both the missing-member and spurious-member +stale forms are permitted to reach (t8b, two independent mutations: one in +`create_staff` proving disposition A's maintenance rule is *not* implemented, +one in `create_staff_group` proving non-empty `members` is *not* normalized +away). Filed as P13-S16; disposition A (the maintenance/enforcement fix) is +sequenced after G3b. + +**Referential preconditions, graph-aware only, mirroring `create_staff` +exactly.** `CreateStaffGroup.members` and `CreatePartDefinition.staves` each +precondition every named id resolves to a live `Staff`; `CreateView. +active_layers` preconditions every named id resolves to a live +`AnalysisLayer`; `CreateAnalysisLayer` has no outbound reference and needs no +precondition at all — the `CreateInstrument` shape, not the `CreateStaff` +shape. All three loops are independently mutation-tested (t6, three +sub-mutations) and independently shown unenforced base-free (t7). Pin 5: +these preconditions were verified against invariant 10's existing check body +(`core/src/invariants.rs:1120`–`:1159`), which already resolved all three +reference classes before this rung — G3a's own contribution to invariant 10 +is the doc-comment reconciliation (pin 6, doc-only, no enum entry, no +discriminant, no behaviour change), asserted structurally by a grep guard +(t12) rather than by inspection. + +**t8, narrowed per §1.1.** From empty, `CreateInstrument` → +`CreateStaffGroup` → `CreateStaff(group: Some(g))` reaches a note with the +staff's group precondition satisfied — this closes the *satisfiability* half +only. It does **not** claim a bidirectionally consistent group is authorable; +t8b pins both permitted stale forms as the honest complement. + +**Decode vectors pinned to literal bytes** (`vectors.rs`, four new tests +mirroring the G1/G2a/G2b precedent), not merely round-tripped. One genuine +finding during construction: `valuegen::analysis_layer`'s original name +format (`"analysis-layer-{n}"`) happened to be exactly 16 bytes for small +counters — the same width as `AnalysisLayerId`'s canonical encoding — which +made a `struct_codec!` field swap on the two-field `AnalysisLayer` type +byte-invisible (decode-under-swap re-encodes to the identical original bytes, +since both fields are equal-width length-prefixed leaves). Fixed by +shortening the name format (`"layer-{n}"`); documented in both the helper and +the corresponding literal-byte test so the hazard does not recur silently. +All four field-swap mutations (`StaffGroup`, `PartDefinition`, +`AnalysisLayer`, `ViewDefinition`) were then observed to kill their own +vector, independently, and only their own. + +**Boundary crossings, budgeted as the contract's one-time authorization.** +`editor-core/src/barriers.rs::subjects_of` gains four arms — each names only +the object it mints with `EditContext::default()`, exactly `CreateStaff`'s +and `CreateInstrument`'s shape, regardless of whether the carried value holds +an outbound reference. `layout-ir/src/barrier.rs`'s "one past the vocabulary" +literal moves 35→39. `testkit/src/generators.rs`'s `rng.below` bound moves +35→39 with four new arms and a widened `operation_payload_emits_every_ +appended_kind` coverage assertion. `testkit/tests/text_projection_grammar.rs`'s +kind count moves 35→39. `testkit/src/layout_stub.rs`'s prose range comment +moves 30..=34→30..=38 (the derived `PAYLOAD_FREE`-based draw itself needed no +edit, same as G2b). + +**`the_canonical_base_is_byte_identical_across_data_model_majors` re-pinned +again**, same reasoning as every prior tranche: `gen_payload` gained four +arms and its bounded draw widened, reshuffling the whole seeded RNG stream. +Nothing leaked — all four kinds are schema major 0 unconditionally and +`MaterializedState` embeds no `Score` field value for any of the four carried +types. + +**No pruning or compaction is implemented, enabled, or prepared here** — pin +9's explicit non-goal, restated: G3a adds four more authored families to the +surface a future prune must not discard. diff --git a/crates/epiphany-ops/src/envdecode.rs b/crates/epiphany-ops/src/envdecode.rs index c1796b5..01fe23b 100644 --- a/crates/epiphany-ops/src/envdecode.rs +++ b/crates/epiphany-ops/src/envdecode.rs @@ -31,17 +31,18 @@ use std::collections::BTreeSet; +use epiphany_core::{ + AnalysisLayer, CanvasLayoutDefaults, EventId, Instrument, InstrumentId, MetricGrid, + MusicalPosition, OperationId, PartDefinition, PitchId, PitchSpelling, RegionId, + RegionTimeModel, RepeatStructureId, ReplicaId, ScoreMetadata, SpellingPrecedence, Staff, + StaffGroup, StaffInstance, StaffInstanceId, StaffLineConfiguration, TimeAnchor, TimeSignature, + TranspositionInterval, TuningContextSettings, TupletId, TypedObjectId, ViewDefinition, Voice, + VoiceId, WallClockTime, +}; use epiphany_core::{ Beam, Event, IdentifiedPitch, Pitch, Region, RepeatStructure, Rest, Slur, Spanner, Tie, }; use epiphany_core::{CanonicalValue, TempoSegment}; -use epiphany_core::{ - CanvasLayoutDefaults, EventId, Instrument, InstrumentId, MetricGrid, MusicalPosition, - OperationId, PitchId, PitchSpelling, RegionId, RegionTimeModel, RepeatStructureId, ReplicaId, - ScoreMetadata, SpellingPrecedence, Staff, StaffInstance, StaffInstanceId, - StaffLineConfiguration, TimeAnchor, TimeSignature, TranspositionInterval, - TuningContextSettings, TupletId, TypedObjectId, Voice, VoiceId, WallClockTime, -}; use epiphany_determinism::{CanonicalDecode, CanonicalEncode}; use crate::causal::CausalContext; @@ -599,6 +600,18 @@ fn operation_kind(r: &mut Reader<'_>) -> Result { 34 => OperationKind::SetTuningContext(SetTuningContextOp { settings: value::(r, "TuningContextSettings")?, }), + 35 => OperationKind::CreateStaffGroup(CreateStaffGroupOp { + group: value::(r, "StaffGroup")?, + }), + 36 => OperationKind::CreatePartDefinition(CreatePartDefinitionOp { + part: value::(r, "PartDefinition")?, + }), + 37 => OperationKind::CreateAnalysisLayer(CreateAnalysisLayerOp { + layer: value::(r, "AnalysisLayer")?, + }), + 38 => OperationKind::CreateView(CreateViewOp { + view: value::(r, "ViewDefinition")?, + }), tag => { return Err(EnvelopeDecodeError::InvalidTag { kind: "OperationKind", @@ -903,6 +916,38 @@ pub(crate) mod tests { settings: valuegen::tuning_context_settings(1), }) } + OperationKindTag::CreateStaffGroup => { + OperationKind::CreateStaffGroup(crate::payload::CreateStaffGroupOp { + group: valuegen::staff_group( + epiphany_core::StaffGroupId::new(ReplicaId(7), 1), + vec![StaffId::new(ReplicaId(7), 1)], + ), + }) + } + OperationKindTag::CreatePartDefinition => { + OperationKind::CreatePartDefinition(crate::payload::CreatePartDefinitionOp { + part: valuegen::part_definition( + epiphany_core::PartDefinitionId::new(ReplicaId(7), 1), + vec![StaffId::new(ReplicaId(7), 1)], + ), + }) + } + OperationKindTag::CreateAnalysisLayer => { + OperationKind::CreateAnalysisLayer(crate::payload::CreateAnalysisLayerOp { + layer: valuegen::analysis_layer(epiphany_core::AnalysisLayerId::new( + ReplicaId(7), + 1, + )), + }) + } + OperationKindTag::CreateView => { + OperationKind::CreateView(crate::payload::CreateViewOp { + view: valuegen::view( + epiphany_core::ViewId::new(ReplicaId(7), 1), + vec![epiphany_core::AnalysisLayerId::new(ReplicaId(7), 1)], + ), + }) + } } } diff --git a/crates/epiphany-ops/src/fuzz.rs b/crates/epiphany-ops/src/fuzz.rs index 13d73a6..fa4b9a2 100644 --- a/crates/epiphany-ops/src/fuzz.rs +++ b/crates/epiphany-ops/src/fuzz.rs @@ -21,9 +21,9 @@ //! those paths are exercised for permutation-invariance too. use epiphany_core::{ - EventId, MusicalDuration, MusicalPosition, OperationId, PitchId, RationalTime, RegionId, - RepeatStructureId, ReplicaId, SlurId, StaffId, StaffInstanceId, TranspositionInterval, - TypedObjectId, VoiceId, + AnalysisLayerId, EventId, MusicalDuration, MusicalPosition, OperationId, PartDefinitionId, + PitchId, RationalTime, RegionId, RepeatStructureId, ReplicaId, SlurId, StaffGroupId, StaffId, + StaffInstanceId, TranspositionInterval, TypedObjectId, ViewId, VoiceId, }; use epiphany_determinism::fuzz::SplitMix64; @@ -32,8 +32,9 @@ use crate::envelope::OperationEnvelope; use crate::opset::OperationSet; use crate::payload::OperationKindTag; use crate::payload::{ - CreateCrossCuttingOp, CreateInstrumentOp, CreateRegionOp, CreateRepeatStructureOp, - CreateStaffInstanceOp, CreateStaffOp, CreateVoiceOp, CrossCuttingValue, DeleteCrossCuttingOp, + CreateAnalysisLayerOp, CreateCrossCuttingOp, CreateInstrumentOp, CreatePartDefinitionOp, + CreateRegionOp, CreateRepeatStructureOp, CreateStaffGroupOp, CreateStaffInstanceOp, + CreateStaffOp, CreateViewOp, CreateVoiceOp, CrossCuttingValue, DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp, DeleteRegionOp, DeleteRepeatStructureOp, DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, InsertIdentifiedPitchOp, ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, OperationKind, OperationPayload, @@ -90,7 +91,7 @@ fn pitch(n: u64) -> PitchId { /// Generates a random payload over the shared id space. fn gen_payload(rng: &mut SplitMix64) -> OperationPayload { - let kind = match rng.below(32) { + let kind = match rng.below(36) { 0 => { let voice = VoiceId::new(ReplicaId(7), rng.below(3)); let position = MusicalPosition(RationalTime::from_int(rng.below(4) as i32)); @@ -304,6 +305,30 @@ fn gen_payload(rng: &mut SplitMix64) -> OperationPayload { 31 => OperationKind::SetTuningContext(crate::payload::SetTuningContextOp { settings: valuegen::tuning_context_settings(rng.below(3) as u8), }), + // Genesis tranche G3a: the four remaining root-level mints, over the + // shared staff/analysis-layer id spaces so mints/re-carries genuinely + // interact with them (mirroring arm 21's `CreateStaff`). + 32 => OperationKind::CreateStaffGroup(CreateStaffGroupOp { + group: valuegen::staff_group( + StaffGroupId::new(ReplicaId(7), rng.below(2)), + vec![StaffId::new(ReplicaId(7), rng.below(2))], + ), + }), + 33 => OperationKind::CreatePartDefinition(CreatePartDefinitionOp { + part: valuegen::part_definition( + PartDefinitionId::new(ReplicaId(7), rng.below(2)), + vec![StaffId::new(ReplicaId(7), rng.below(2))], + ), + }), + 34 => OperationKind::CreateAnalysisLayer(CreateAnalysisLayerOp { + layer: valuegen::analysis_layer(AnalysisLayerId::new(ReplicaId(7), rng.below(2))), + }), + 35 => OperationKind::CreateView(CreateViewOp { + view: valuegen::view( + ViewId::new(ReplicaId(7), rng.below(2)), + vec![AnalysisLayerId::new(ReplicaId(7), rng.below(2))], + ), + }), _ => OperationKind::SetStaffLayout(SetStaffLayoutOp { staff_instance: StaffInstanceId::new(ReplicaId(7), rng.below(3)), instrument_override: None, diff --git a/crates/epiphany-ops/src/lib.rs b/crates/epiphany-ops/src/lib.rs index d9debdf..9320123 100644 --- a/crates/epiphany-ops/src/lib.rs +++ b/crates/epiphany-ops/src/lib.rs @@ -124,9 +124,10 @@ pub use envelope::{ pub use migrate::{migrate_v0_envelope, project_v1_to_v0, MigrationError}; pub use opset::{AcceptOutcome, OperationSet}; pub use payload::{ - operation_block_introduced_minor, ChangeRegionTimeModelOp, CreateCrossCuttingOp, - CreateInstrumentOp, CreateRegionOp, CreateRepeatStructureOp, CreateStaffInstanceOp, - CreateStaffOp, CreateVoiceOp, CrossCuttingValue, DeleteCrossCuttingOp, DeleteEventOp, + operation_block_introduced_minor, ChangeRegionTimeModelOp, CreateAnalysisLayerOp, + CreateCrossCuttingOp, CreateInstrumentOp, CreatePartDefinitionOp, CreateRegionOp, + CreateRepeatStructureOp, CreateStaffGroupOp, CreateStaffInstanceOp, CreateStaffOp, + CreateViewOp, CreateVoiceOp, CrossCuttingValue, DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp, DeleteRegionOp, DeleteRepeatStructureOp, DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, InsertIdentifiedPitchOp, ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, OperationKind, OperationKindTag, OperationPayload, PositionRemapping, diff --git a/crates/epiphany-ops/src/migrate.rs b/crates/epiphany-ops/src/migrate.rs index e8cca1f..42c796f 100644 --- a/crates/epiphany-ops/src/migrate.rs +++ b/crates/epiphany-ops/src/migrate.rs @@ -190,6 +190,13 @@ fn project_kind(kind: &OperationKind) -> V0OperationKind { } // Genesis tranche G2b: born past v0; projected verbatim. OperationKind::SetTuningContext(op) => V0OperationKind::SetTuningContext(op.clone()), + // Genesis tranche G3a: born past v0; projected verbatim. + OperationKind::CreateStaffGroup(op) => V0OperationKind::CreateStaffGroup(op.clone()), + OperationKind::CreatePartDefinition(op) => { + V0OperationKind::CreatePartDefinition(op.clone()) + } + OperationKind::CreateAnalysisLayer(op) => V0OperationKind::CreateAnalysisLayer(op.clone()), + OperationKind::CreateView(op) => V0OperationKind::CreateView(op.clone()), } } @@ -354,6 +361,13 @@ fn migrate_kind(kind: &V0OperationKind, context: &Score) -> Result OperationKind::SetTuningContext(op.clone()), + // Genesis tranche G3a: identity round-trip (no lossy v0 form). + V0OperationKind::CreateStaffGroup(op) => OperationKind::CreateStaffGroup(op.clone()), + V0OperationKind::CreatePartDefinition(op) => { + OperationKind::CreatePartDefinition(op.clone()) + } + V0OperationKind::CreateAnalysisLayer(op) => OperationKind::CreateAnalysisLayer(op.clone()), + V0OperationKind::CreateView(op) => OperationKind::CreateView(op.clone()), }) } diff --git a/crates/epiphany-ops/src/payload.rs b/crates/epiphany-ops/src/payload.rs index 85412c9..8a06c54 100644 --- a/crates/epiphany-ops/src/payload.rs +++ b/crates/epiphany-ops/src/payload.rs @@ -32,13 +32,14 @@ //! payloads. use epiphany_core::{ - Beam, CanonicalValue, CanvasLayoutDefaults, Event, EventDuration, EventId, EventPosition, - IdentifiedPitch, Instrument, InstrumentId, MetricGrid, MusicalDuration, MusicalPosition, - OperationId, Pitch, PitchId, PitchSpelling, Region, RegionId, RegionTimeModel, RepeatStructure, - RepeatStructureId, Rest, ScoreMetadata, Slur, Spanner, SpellingPrecedence, Staff, StaffId, - StaffInstance, StaffInstanceId, StaffLineConfiguration, TempoSegment, Tie, TimeAnchor, + AnalysisLayer, AnalysisLayerId, Beam, CanonicalValue, CanvasLayoutDefaults, Event, + EventDuration, EventId, EventPosition, IdentifiedPitch, Instrument, InstrumentId, MetricGrid, + MusicalDuration, MusicalPosition, OperationId, PartDefinition, PartDefinitionId, Pitch, + PitchId, PitchSpelling, Region, RegionId, RegionTimeModel, RepeatStructure, RepeatStructureId, + Rest, ScoreMetadata, Slur, Spanner, SpellingPrecedence, Staff, StaffGroup, StaffGroupId, + StaffId, StaffInstance, StaffInstanceId, StaffLineConfiguration, TempoSegment, Tie, TimeAnchor, TimeSignature, TransactionId, TranspositionInterval, TuningContextSettings, TupletId, - TypedObjectId, Voice, VoiceId, + TypedObjectId, ViewDefinition, ViewId, Voice, VoiceId, }; use epiphany_determinism::{ sorted_canonical, CanonicalDecode, CanonicalEncode, CanonicalSet, DecodeError, @@ -273,6 +274,26 @@ pub enum OperationKind { /// `ScoreTuningContext` — `accidental_extensions` is not on the wire and /// is left untouched by reduction (contract §1). SetTuningContext(SetTuningContextOp), + // --- Genesis tranche G3a (`spec/CONTRACT_GENESIS_G3A_ENTITIES.md`): the + // four remaining root-level `Score` entity mints. Discriminant extends + // additively past 34. --- + /// Mint a staff group on the score root (set-union creation). Graph-aware + /// reduction preconditions every carried member resolves to a live + /// `Staff`; the mint stores `members` as given and neither maintains nor + /// trusts it thereafter (contract §1.1, disposition B). + CreateStaffGroup(CreateStaffGroupOp), + /// Mint a part-extraction view definition on the score root (set-union + /// creation). Graph-aware reduction preconditions every carried staff + /// resolves to a live `Staff`. + CreatePartDefinition(CreatePartDefinitionOp), + /// Mint a first-class analysis layer on the score root (set-union + /// creation). Holds no outbound entity reference, so needs no referential + /// precondition — only mint and idempotence, exactly `CreateInstrument`. + CreateAnalysisLayer(CreateAnalysisLayerOp), + /// Mint a view recipe on the score root (set-union creation). Graph-aware + /// reduction preconditions every carried active layer resolves to a live + /// `AnalysisLayer`. + CreateView(CreateViewOp), } impl OperationKind { @@ -388,6 +409,12 @@ impl OperationKind { // Genesis tranche G2b; appended past 33. Coincides with tag 34 by // the same accident as G1/G2a, not by rule. OperationKind::SetTuningContext(_) => 34, + // Genesis tranche G3a; appended past 34. Coincide with tags + // 35–38 by the same accident as G1/G2a/G2b, not by rule. + OperationKind::CreateStaffGroup(_) => 35, + OperationKind::CreatePartDefinition(_) => 36, + OperationKind::CreateAnalysisLayer(_) => 37, + OperationKind::CreateView(_) => 38, } } @@ -447,6 +474,13 @@ impl OperationKind { // Minor 10 (Genesis tranche G2b), ratified // `spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4. OperationKind::SetTuningContext(_) => Some(10), + // Minor 11 (Genesis tranche G3a), ratified + // `spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4: one epoch for one + // additive event, the G2a precedent (two kinds at epoch 9). + OperationKind::CreateStaffGroup(_) + | OperationKind::CreatePartDefinition(_) + | OperationKind::CreateAnalysisLayer(_) + | OperationKind::CreateView(_) => Some(11), } } @@ -495,6 +529,13 @@ impl OperationKind { OperationKind::SetCanvasLayoutDefaults(_) => OperationKindTag::SetCanvasLayoutDefaults, OperationKind::SetSpellingPrecedence(_) => OperationKindTag::SetSpellingPrecedence, OperationKind::SetTuningContext(_) => OperationKindTag::SetTuningContext, + // Genesis tranche G3a. Name-verbatim, as the four most recent + // additions are (contract pin 3's precedent — not the tag + // layer's older Create→Insert convention). + OperationKind::CreateStaffGroup(_) => OperationKindTag::CreateStaffGroup, + OperationKind::CreatePartDefinition(_) => OperationKindTag::CreatePartDefinition, + OperationKind::CreateAnalysisLayer(_) => OperationKindTag::CreateAnalysisLayer, + OperationKind::CreateView(_) => OperationKindTag::CreateView, } } } @@ -541,6 +582,10 @@ impl CanonicalEncode for OperationKind { OperationKind::SetCanvasLayoutDefaults(op) => op.encode_canonical(out), OperationKind::SetSpellingPrecedence(op) => op.encode_canonical(out), OperationKind::SetTuningContext(op) => op.encode_canonical(out), + OperationKind::CreateStaffGroup(op) => op.encode_canonical(out), + OperationKind::CreatePartDefinition(op) => op.encode_canonical(out), + OperationKind::CreateAnalysisLayer(op) => op.encode_canonical(out), + OperationKind::CreateView(op) => op.encode_canonical(out), } } } @@ -597,6 +642,14 @@ pub enum OperationKindTag { SetSpellingPrecedence, /// Genesis tranche G2b. SetTuningContext, + /// Genesis tranche G3a. + CreateStaffGroup, + /// Genesis tranche G3a. + CreatePartDefinition, + /// Genesis tranche G3a. + CreateAnalysisLayer, + /// Genesis tranche G3a. + CreateView, } /// The discriminant of [`OperationKindTag::Registered`], the one tag that @@ -712,6 +765,10 @@ operation_kind_tag_vocabulary! { SetCanvasLayoutDefaults = 32 => "set-canvas-layout-defaults" @ Some(9), SetSpellingPrecedence = 33 => "set-spelling-precedence" @ Some(9), SetTuningContext = 34 => "set-tuning-context" @ Some(10), + CreateStaffGroup = 35 => "create-staff-group" @ Some(11), + CreatePartDefinition = 36 => "create-part-definition" @ Some(11), + CreateAnalysisLayer = 37 => "create-analysis-layer" @ Some(11), + CreateView = 38 => "create-view" @ Some(11), } impl CanonicalEncode for OperationKindTag { @@ -1702,6 +1759,103 @@ impl CanonicalEncode for SetTuningContextOp { } } +// --- Genesis tranche G3a (`spec/CONTRACT_GENESIS_G3A_ENTITIES.md`): the four +// remaining root-level `Score` entity mints. All four ride the `CreateStaff` +// set-union mint pattern (`reduce.rs:4075`) with byte-identical re-carry +// idempotence. --- + +/// Mint a global [`StaffGroup`] on the score root (operation_catalog +/// §CreateStaffGroup). Carries the full staff-group value: identity, optional +/// name, kind, and the carried `members` list. Graph-aware reduction +/// preconditions every carried member resolves to a live `Staff`; per §1.1 +/// (disposition B), the mint stores `members` exactly as given and neither +/// maintains nor trusts it thereafter — `Staff.group` is the sole authority +/// for membership. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct CreateStaffGroupOp { + pub group: StaffGroup, +} + +impl CreateStaffGroupOp { + /// The minted staff group's identifier. + pub fn staff_group_id(&self) -> StaffGroupId { + self.group.id + } +} + +impl CanonicalEncode for CreateStaffGroupOp { + fn encode_canonical(&self, out: &mut Vec) { + push_lp_bytes(out, &self.group.canonical_bytes()); + } +} + +/// Mint a [`PartDefinition`] on the score root (operation_catalog +/// §CreatePartDefinition). Carries the full part-extraction value: identity, +/// name, and the carried `staves` list. Graph-aware reduction preconditions +/// every carried staff resolves to a live `Staff`. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct CreatePartDefinitionOp { + pub part: PartDefinition, +} + +impl CreatePartDefinitionOp { + /// The minted part definition's identifier. + pub fn part_definition_id(&self) -> PartDefinitionId { + self.part.id + } +} + +impl CanonicalEncode for CreatePartDefinitionOp { + fn encode_canonical(&self, out: &mut Vec) { + push_lp_bytes(out, &self.part.canonical_bytes()); + } +} + +/// Mint an [`AnalysisLayer`] on the score root (operation_catalog +/// §CreateAnalysisLayer). Carries the full value: identity and name. +/// `AnalysisLayer` holds no outbound entity reference, so this operation +/// needs no referential precondition at all — only mint and idempotence, +/// exactly the `CreateInstrument` discipline. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct CreateAnalysisLayerOp { + pub layer: AnalysisLayer, +} + +impl CreateAnalysisLayerOp { + /// The minted analysis layer's identifier. + pub fn analysis_layer_id(&self) -> AnalysisLayerId { + self.layer.id + } +} + +impl CanonicalEncode for CreateAnalysisLayerOp { + fn encode_canonical(&self, out: &mut Vec) { + push_lp_bytes(out, &self.layer.canonical_bytes()); + } +} + +/// Mint a [`ViewDefinition`] on the score root (operation_catalog +/// §CreateView). Carries the full view-recipe value: identity, name, and the +/// carried `active_layers` list. Graph-aware reduction preconditions every +/// carried active layer resolves to a live `AnalysisLayer`. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct CreateViewOp { + pub view: ViewDefinition, +} + +impl CreateViewOp { + /// The minted view's identifier. + pub fn view_id(&self) -> ViewId { + self.view.id + } +} + +impl CanonicalEncode for CreateViewOp { + fn encode_canonical(&self, out: &mut Vec) { + push_lp_bytes(out, &self.view.canonical_bytes()); + } +} + /// Set, replace, or (`None`) remove the single meter change at the anchor's /// resolved musical position in a region's default metric grid /// (operation_catalog §"Meter and Tempo Overwrites"). Carries the full @@ -2473,6 +2627,12 @@ mod tests { // Genesis G2b. Kept in step with pin 1's table; an epoch omitted // here is an epoch this test cannot see go wrong. (OperationKindTag::SetTuningContext, 10), + // Genesis G3a. Same rationale: omitting any of the four here is + // an epoch this test cannot see go wrong. + (OperationKindTag::CreateStaffGroup, 11), + (OperationKindTag::CreatePartDefinition, 11), + (OperationKindTag::CreateAnalysisLayer, 11), + (OperationKindTag::CreateView, 11), ] { assert_eq!( tag.introduced_minor(), diff --git a/crates/epiphany-ops/src/reduce.rs b/crates/epiphany-ops/src/reduce.rs index bc63db5..e95b2dc 100644 --- a/crates/epiphany-ops/src/reduce.rs +++ b/crates/epiphany-ops/src/reduce.rs @@ -32,15 +32,16 @@ use std::cmp::Reverse; use std::collections::{BTreeMap, BTreeSet, BinaryHeap}; use epiphany_core::{ - canonical_pitch_bytes, derive_promoted_voice_id, simplest_spelling, AnchorOffset, - AnnotationAnchor, CanonicalValue, CanvasLayoutDefaults, Event, EventDuration, EventId, - EventPosition, GestureAnchoring, Instrument, InstrumentId, MeterChange, MetricGrid, - MusicalDuration, MusicalPosition, OperationId, Pitch, PitchId, PitchSpelling, RationalTime, - RegionEdge, RegionId, RegionTimeModel, ReplicaId, Score, ScoreMetadata, SpellingAttachment, - SpellingDirective, SpellingPrecedence, SpellingScope, SpellingSource, Staff, StaffId, - StaffInstance, StaffInstanceId, StaffLineConfiguration, TempoMap, TempoSegment, TempoShape, - TimeAnchor, TimeSignature, TimeSignatureId, TransactionId, TransposeRefusal, - TranspositionInterval, TuningContextSettings, TypedObjectId, Voice, VoiceId, VoiceOrigin, + canonical_pitch_bytes, derive_promoted_voice_id, simplest_spelling, AnalysisLayer, + AnalysisLayerId, AnchorOffset, AnnotationAnchor, CanonicalValue, CanvasLayoutDefaults, Event, + EventDuration, EventId, EventPosition, GestureAnchoring, Instrument, InstrumentId, MeterChange, + MetricGrid, MusicalDuration, MusicalPosition, OperationId, PartDefinition, PartDefinitionId, + Pitch, PitchId, PitchSpelling, RationalTime, RegionEdge, RegionId, RegionTimeModel, ReplicaId, + Score, ScoreMetadata, SpellingAttachment, SpellingDirective, SpellingPrecedence, SpellingScope, + SpellingSource, Staff, StaffGroup, StaffGroupId, StaffId, StaffInstance, StaffInstanceId, + StaffLineConfiguration, TempoMap, TempoSegment, TempoShape, TimeAnchor, TimeSignature, + TimeSignatureId, TransactionId, TransposeRefusal, TranspositionInterval, TuningContextSettings, + TypedObjectId, ViewDefinition, ViewId, Voice, VoiceId, VoiceOrigin, }; use epiphany_determinism::CanonicalEncode; @@ -56,10 +57,11 @@ use crate::encode::{push_canon, push_len, push_lp_bytes, push_u8_bool}; use crate::envelope::OperationEnvelope; use crate::opset::OperationSet; use crate::payload::{ - resolved_anchor_position, CreateCrossCuttingOp, CreateInstrumentOp, CreateRegionOp, - CreateRepeatStructureOp, CreateStaffInstanceOp, CreateStaffOp, CreateVoiceOp, - CrossCuttingValue, DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp, - DeleteRegionOp, DeleteRepeatStructureOp, DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, + resolved_anchor_position, CreateAnalysisLayerOp, CreateCrossCuttingOp, CreateInstrumentOp, + CreatePartDefinitionOp, CreateRegionOp, CreateRepeatStructureOp, CreateStaffGroupOp, + CreateStaffInstanceOp, CreateStaffOp, CreateViewOp, CreateVoiceOp, CrossCuttingValue, + DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp, DeleteRegionOp, + DeleteRepeatStructureOp, DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, InsertIdentifiedPitchOp, ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, OperationKind, OperationPayload, RespellPitchOp, SetCanvasLayoutDefaultsOp, SetMetadataOp, SetMetricGridOp, SetSpellingPrecedenceOp, SetStaffLayoutOp, SetTempoSegmentOp, @@ -1005,6 +1007,15 @@ struct Reducer<'a> { // — `TypedObjectId::Instrument` liveness is seeded from the base // (`seed_from_graph`), but the base seed left no value map before this. instrument_values: BTreeMap, + // Carried values of the four genesis tranche G3a root-level mints + // (`spec/CONTRACT_GENESIS_G3A_ENTITIES.md` pin 4a), mirroring + // `staff_values`/`instrument_values`: the byte-identical-re-carry + // idempotence check against a *base*-seeded entity has nothing to compare + // without these. + staff_group_values: BTreeMap, + part_definition_values: BTreeMap, + analysis_layer_values: BTreeMap, + view_values: BTreeMap, structures: BTreeMap>, // Live child sets for the structural-container empty-only delete (Group 3): // a region's live staff instances, and a staff instance's live voices. (A @@ -1106,6 +1117,10 @@ struct WorkingSnapshot { staff_values: BTreeMap, time_signature_values: BTreeMap, instrument_values: BTreeMap, + staff_group_values: BTreeMap, + part_definition_values: BTreeMap, + analysis_layer_values: BTreeMap, + view_values: BTreeMap, structures: BTreeMap>, region_instances: BTreeMap>, instance_voices: BTreeMap>, @@ -1388,6 +1403,10 @@ impl<'a> Reducer<'a> { staff_values: BTreeMap::new(), time_signature_values: BTreeMap::new(), instrument_values: BTreeMap::new(), + staff_group_values: BTreeMap::new(), + part_definition_values: BTreeMap::new(), + analysis_layer_values: BTreeMap::new(), + view_values: BTreeMap::new(), structures: BTreeMap::new(), region_instances: BTreeMap::new(), instance_voices: BTreeMap::new(), @@ -1447,10 +1466,17 @@ impl<'a> Reducer<'a> { for group in &score.staff_groups { self.objects .insert(TypedObjectId::StaffGroup(group.id), ObjectState::Live); + // Genesis tranche G3a (contract pin 4a, site 4): without this seed, + // a byte-identical re-carry against a *base* staff group finds no + // retained value and misclassifies (the G1 `instrument_values` + // hazard, `reduce.rs:13264`–`:13268`, copied here for all four + // families). + self.staff_group_values.insert(group.id, group.clone()); } for part in &score.parts { self.objects .insert(TypedObjectId::PartDefinition(part.id), ObjectState::Live); + self.part_definition_values.insert(part.id, part.clone()); } for signature in &score.time_signatures { self.objects.insert( @@ -1463,10 +1489,12 @@ impl<'a> Reducer<'a> { for layer in &score.analysis_layers { self.objects .insert(TypedObjectId::AnalysisLayer(layer.id), ObjectState::Live); + self.analysis_layer_values.insert(layer.id, layer.clone()); } for view in &score.views { self.objects .insert(TypedObjectId::View(view.id), ObjectState::Live); + self.view_values.insert(view.id, view.clone()); } // The score-level LWW chains seed with the base values so a @@ -2858,6 +2886,10 @@ impl<'a> Reducer<'a> { } OperationKind::SetSpellingPrecedence(op) => self.set_spelling_precedence(env, op), OperationKind::SetTuningContext(op) => self.set_tuning_context(env, op), + OperationKind::CreateStaffGroup(op) => self.create_staff_group(env, op), + OperationKind::CreatePartDefinition(op) => self.create_part_definition(env, op), + OperationKind::CreateAnalysisLayer(op) => self.create_analysis_layer(env, op), + OperationKind::CreateView(op) => self.create_view(env, op), }, OperationPayload::ResolveConflict(op) => self.resolve_conflict(env, op), OperationPayload::UndoTransaction(op) => self.undo_transaction(env, op), @@ -4185,6 +4217,228 @@ impl<'a> Reducer<'a> { OperationEffect::Applied } + // --- Genesis tranche G3a (`spec/CONTRACT_GENESIS_G3A_ENTITIES.md`): the + // four remaining root-level `Score` entity mints. All four ride + // `create_staff`'s set-union mint discipline exactly (contract pin 4): + // fresh id mints; byte-identical re-carry is idempotent; a differing + // value under a live id is a precondition no-op + // (`RecreateContentMismatch`); a tombstoned id is `TargetTombstoned`. + // Referential preconditions are graph-aware only — base-free reduction + // has no universe to check against. ------------------------------------- + + /// Set-union creation of a global `StaffGroup` on the score root + /// (operation_catalog §CreateStaffGroup). Graph-aware reduction + /// preconditions every carried member resolves to a live `Staff`. Per + /// §1.1 (disposition B), `members` is stored exactly as carried and + /// neither maintained nor trusted thereafter — `Staff.group` is the sole + /// authority for membership. + fn create_staff_group( + &mut self, + env: &OperationEnvelope, + op: &CreateStaffGroupOp, + ) -> OperationEffect { + let gobj = TypedObjectId::StaffGroup(op.staff_group_id()); + match self.objects.get(&gobj) { + Some(ObjectState::Live) => { + let identical = self + .staff_group_values + .get(&op.staff_group_id()) + .is_some_and(|known| known == &op.group); + return if identical { + OperationEffect::NoOp { + reason: NoOpReason::AlreadyApplied, + } + } else { + OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::RecreateContentMismatch, + }, + } + }; + } + Some(ObjectState::Tombstoned { .. }) => { + return OperationEffect::NoOp { + reason: NoOpReason::TargetTombstoned, + } + } + None => {} + } + if self.graph.is_some() { + for member in &op.group.members { + if !matches!( + self.objects.get(&TypedObjectId::Staff(*member)), + Some(ObjectState::Live) + ) { + return OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::TargetMissing, + }, + }; + } + } + } + if let Some(score) = self.graph.as_mut() { + score.staff_groups.push(op.group.clone()); + } + self.mint_container(env, gobj); + self.staff_group_values + .insert(op.staff_group_id(), op.group.clone()); + OperationEffect::Applied + } + + /// Set-union creation of a `PartDefinition` on the score root + /// (operation_catalog §CreatePartDefinition). Graph-aware reduction + /// preconditions every carried staff resolves to a live `Staff`. + fn create_part_definition( + &mut self, + env: &OperationEnvelope, + op: &CreatePartDefinitionOp, + ) -> OperationEffect { + let pobj = TypedObjectId::PartDefinition(op.part_definition_id()); + match self.objects.get(&pobj) { + Some(ObjectState::Live) => { + let identical = self + .part_definition_values + .get(&op.part_definition_id()) + .is_some_and(|known| known == &op.part); + return if identical { + OperationEffect::NoOp { + reason: NoOpReason::AlreadyApplied, + } + } else { + OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::RecreateContentMismatch, + }, + } + }; + } + Some(ObjectState::Tombstoned { .. }) => { + return OperationEffect::NoOp { + reason: NoOpReason::TargetTombstoned, + } + } + None => {} + } + if self.graph.is_some() { + for staff in &op.part.staves { + if !matches!( + self.objects.get(&TypedObjectId::Staff(*staff)), + Some(ObjectState::Live) + ) { + return OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::TargetMissing, + }, + }; + } + } + } + if let Some(score) = self.graph.as_mut() { + score.parts.push(op.part.clone()); + } + self.mint_container(env, pobj); + self.part_definition_values + .insert(op.part_definition_id(), op.part.clone()); + OperationEffect::Applied + } + + /// Set-union creation of an `AnalysisLayer` on the score root + /// (operation_catalog §CreateAnalysisLayer). `AnalysisLayer` holds no + /// outbound entity reference, so — exactly like `CreateInstrument` — + /// there is no graph-aware reference-resolution block. + fn create_analysis_layer( + &mut self, + env: &OperationEnvelope, + op: &CreateAnalysisLayerOp, + ) -> OperationEffect { + let lobj = TypedObjectId::AnalysisLayer(op.analysis_layer_id()); + match self.objects.get(&lobj) { + Some(ObjectState::Live) => { + let identical = self + .analysis_layer_values + .get(&op.analysis_layer_id()) + .is_some_and(|known| known == &op.layer); + return if identical { + OperationEffect::NoOp { + reason: NoOpReason::AlreadyApplied, + } + } else { + OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::RecreateContentMismatch, + }, + } + }; + } + Some(ObjectState::Tombstoned { .. }) => { + return OperationEffect::NoOp { + reason: NoOpReason::TargetTombstoned, + } + } + None => {} + } + if let Some(score) = self.graph.as_mut() { + score.analysis_layers.push(op.layer.clone()); + } + self.mint_container(env, lobj); + self.analysis_layer_values + .insert(op.analysis_layer_id(), op.layer.clone()); + OperationEffect::Applied + } + + /// Set-union creation of a `ViewDefinition` on the score root + /// (operation_catalog §CreateView). Graph-aware reduction preconditions + /// every carried active layer resolves to a live `AnalysisLayer`. + fn create_view(&mut self, env: &OperationEnvelope, op: &CreateViewOp) -> OperationEffect { + let vobj = TypedObjectId::View(op.view_id()); + match self.objects.get(&vobj) { + Some(ObjectState::Live) => { + let identical = self + .view_values + .get(&op.view_id()) + .is_some_and(|known| known == &op.view); + return if identical { + OperationEffect::NoOp { + reason: NoOpReason::AlreadyApplied, + } + } else { + OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::RecreateContentMismatch, + }, + } + }; + } + Some(ObjectState::Tombstoned { .. }) => { + return OperationEffect::NoOp { + reason: NoOpReason::TargetTombstoned, + } + } + None => {} + } + if self.graph.is_some() { + for layer in &op.view.active_layers { + if !matches!( + self.objects.get(&TypedObjectId::AnalysisLayer(*layer)), + Some(ObjectState::Live) + ) { + return OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::TargetMissing, + }, + }; + } + } + } + if let Some(score) = self.graph.as_mut() { + score.views.push(op.view.clone()); + } + self.mint_container(env, vobj); + self.view_values.insert(op.view_id(), op.view.clone()); + OperationEffect::Applied + } + /// Set-union mint of a `TimeSignature` carried by a `SetTimeSignature` /// (operation_catalog §"Meter and Tempo Overwrites"): fresh id mints; /// byte-identical re-carry is idempotent; a differing value under a live @@ -7637,6 +7891,10 @@ impl<'a> Reducer<'a> { staff_values: self.staff_values.clone(), time_signature_values: self.time_signature_values.clone(), instrument_values: self.instrument_values.clone(), + staff_group_values: self.staff_group_values.clone(), + part_definition_values: self.part_definition_values.clone(), + analysis_layer_values: self.analysis_layer_values.clone(), + view_values: self.view_values.clone(), structures: self.structures.clone(), region_instances: self.region_instances.clone(), instance_voices: self.instance_voices.clone(), @@ -7677,6 +7935,10 @@ impl<'a> Reducer<'a> { self.staff_values = s.staff_values; self.time_signature_values = s.time_signature_values; self.instrument_values = s.instrument_values; + self.staff_group_values = s.staff_group_values; + self.part_definition_values = s.part_definition_values; + self.analysis_layer_values = s.analysis_layer_values; + self.view_values = s.view_values; self.structures = s.structures; self.region_instances = s.region_instances; self.instance_voices = s.instance_voices; @@ -11182,6 +11444,15 @@ mod tests { // `MaterializedState`'s: the canonical base still embeds no `Score` // field value for any setting, so there remains no schema-major // surface on this type for a leak to appear on. + // + // Re-pinned again at genesis tranche G3a + // (`spec/CONTRACT_GENESIS_G3A_ENTITIES.md`): `gen_payload` gained the + // four remaining root-level mints (arms 32–35), and `rng.below(32)` + // became `below(36)` — the same reshuffle, same reasoning. All four + // are schema major 0 unconditionally (pin 2: no `schema_major()` + // arm), and `MaterializedState` still embeds no `Score` field value + // for any of the four carried types, so there remains no surface on + // this type for a leak to appear on. let mut rng = epiphany_determinism::fuzz::SplitMix64::new(0xBA5E); let envelopes = crate::fuzz::gen_envelope_set(&mut rng, 200); let mut set = OperationSet::new(); @@ -11191,7 +11462,7 @@ mod tests { let hex: String = digest.iter().map(|b| format!("{b:02x}")).collect(); assert_eq!( hex, - "116e88b4013a18864dcd1b09489ab297aa332e14800cf4ca00be9dfa100b4a08" + "0c60a686097819d2c91a65b5bce4ad09c9ce2e640760608124fc999f1470e839" ); } @@ -14091,4 +14362,734 @@ mod tests { predecessor, not the rolled-back transaction's write" ); } + + // ========================================================================= + // Genesis tranche G3a (`spec/CONTRACT_GENESIS_G3A_ENTITIES.md`): the four + // remaining root-level `Score` entity mints. + // ========================================================================= + + fn staff_group_env( + replica: u64, + counter: u64, + physical: i64, + ctx: CausalContext, + group: epiphany_core::StaffGroup, + ) -> OperationEnvelope { + prim_env( + replica, + counter, + physical, + ctx, + OperationKind::CreateStaffGroup(CreateStaffGroupOp { group }), + ) + } + + fn part_definition_env( + replica: u64, + counter: u64, + physical: i64, + ctx: CausalContext, + part: epiphany_core::PartDefinition, + ) -> OperationEnvelope { + prim_env( + replica, + counter, + physical, + ctx, + OperationKind::CreatePartDefinition(CreatePartDefinitionOp { part }), + ) + } + + fn analysis_layer_env( + replica: u64, + counter: u64, + physical: i64, + ctx: CausalContext, + layer: epiphany_core::AnalysisLayer, + ) -> OperationEnvelope { + prim_env( + replica, + counter, + physical, + ctx, + OperationKind::CreateAnalysisLayer(CreateAnalysisLayerOp { layer }), + ) + } + + fn view_env( + replica: u64, + counter: u64, + physical: i64, + ctx: CausalContext, + view: epiphany_core::ViewDefinition, + ) -> OperationEnvelope { + prim_env( + replica, + counter, + physical, + ctx, + OperationKind::CreateView(CreateViewOp { view }), + ) + } + + /// One representative sample of each G3a kind, in §Scope order. + fn g3a_sample_kinds() -> [OperationKind; 4] { + [ + OperationKind::CreateStaffGroup(CreateStaffGroupOp { + group: crate::valuegen::staff_group(StaffGroupId::new(ReplicaId(1), 1), vec![]), + }), + OperationKind::CreatePartDefinition(CreatePartDefinitionOp { + part: crate::valuegen::part_definition( + PartDefinitionId::new(ReplicaId(1), 1), + vec![], + ), + }), + OperationKind::CreateAnalysisLayer(CreateAnalysisLayerOp { + layer: crate::valuegen::analysis_layer(AnalysisLayerId::new(ReplicaId(1), 1)), + }), + OperationKind::CreateView(CreateViewOp { + view: crate::valuegen::view(ViewId::new(ReplicaId(1), 1), vec![]), + }), + ] + } + + /// (t1) Genesis tranche G3a: kinds and tags are 35–38, in **both** + /// spaces, and the discriminant byte leads each canonical encoding + /// (contract pin 1). + /// + /// **Mutation:** move `OperationKind::CreateStaffGroup`'s discriminant to + /// 39 (in `discriminant()`); must fail. Then, separately, move + /// `OperationKindTag::CreateStaffGroup`'s tag to 39 (in the vocabulary + /// macro); must fail too — the two spaces are asserted independently. + #[test] + fn t1_g3a_kinds_and_tags_are_35_to_38_in_both_spaces() { + use crate::payload::OperationKindTag; + let kinds = g3a_sample_kinds(); + for (kind, expected_disc, expected_tag) in [ + (&kinds[0], 35u8, OperationKindTag::CreateStaffGroup), + (&kinds[1], 36, OperationKindTag::CreatePartDefinition), + (&kinds[2], 37, OperationKindTag::CreateAnalysisLayer), + (&kinds[3], 38, OperationKindTag::CreateView), + ] { + assert_eq!(kind.tag(), expected_tag); + assert_eq!(kind.tag().discriminant(), expected_disc); + let mut bytes = Vec::new(); + kind.encode_canonical(&mut bytes); + assert_eq!( + bytes[0], expected_disc, + "the discriminant byte must lead the canonical encoding" + ); + } + } + + /// (t2) Genesis tranche G3a: `schema_major()` returns 0 for all four + /// kinds — the catch-all `_ => 0` arm is correct (pin 2). + /// + /// **Mutation:** add the four G3a kinds to the `=> 2` arm; must fail + /// (pin 2's stated bug). + #[test] + fn t2_g3a_schema_major_is_zero_for_all_four() { + for kind in g3a_sample_kinds() { + assert_eq!( + kind.schema_major(), + 0, + "{:?} must be schema major 0", + kind.tag() + ); + } + } + + /// (t3) A block containing all four G3a kinds stamps schema major 0; the + /// op-block accept-set is untouched at 3 (no `epiphany-bundle` change of + /// any kind — verified structurally, since this crate does not depend on + /// `epiphany-bundle`). + /// + /// **Mutation:** make one kind report major 2; must fail. + #[test] + fn t3_a_block_with_all_four_g3a_kinds_stamps_major_zero() { + let kinds = g3a_sample_kinds(); + let major = kinds.iter().map(|k| k.schema_major()).max().unwrap(); + assert_eq!( + major, 0, + "a block carrying only the four G3a mints stamps major 0" + ); + } + + /// (t5) Genesis tranche G3a: re-carrying a live id with a + /// byte-identical value is `AlreadyApplied`; with a differing value is + /// `RecreateContentMismatch` — `create_staff`'s discipline, for all four + /// ops (pin 4). + /// + /// **Mutation:** in `create_staff_group`, return `OperationEffect::Applied` + /// for the differing-value case instead of the `RecreateContentMismatch` + /// no-op; must fail. + #[test] + fn t5_g3a_recarry_discipline_applied_alreadyapplied_or_recreatecontentmismatch() { + fn effect_of(state: &MaterializedState, id: OperationId) -> Option { + state + .effects + .iter() + .find(|(e, _)| *e == id) + .map(|(_, eff)| eff.clone()) + } + let already_applied = Some(OperationEffect::NoOp { + reason: NoOpReason::AlreadyApplied, + }); + let mismatch = Some(OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::RecreateContentMismatch, + }, + }); + + // CreateStaffGroup. + let group_value = crate::valuegen::staff_group(StaffGroupId::new(ReplicaId(9), 1), vec![]); + let create = staff_group_env(1, 0, 10, CausalContext::new(), group_value.clone()); + let identical = staff_group_env(2, 0, 20, CausalContext::new(), group_value.clone()); + let mut differing_value = group_value; + differing_value.name = Some(String::from("something else")); + let differing = staff_group_env(3, 0, 30, CausalContext::new(), differing_value); + let mut set = OperationSet::new(); + set.accept_all(vec![differing.clone(), identical.clone(), create.clone()]); + let state = set.reduce(); + assert_eq!(effect_of(&state, create.id), Some(OperationEffect::Applied)); + assert_eq!(effect_of(&state, identical.id), already_applied); + assert_eq!(effect_of(&state, differing.id), mismatch); + + // CreatePartDefinition. + let part_value = + crate::valuegen::part_definition(PartDefinitionId::new(ReplicaId(9), 1), vec![]); + let create = part_definition_env(1, 1, 10, CausalContext::new(), part_value.clone()); + let identical = part_definition_env(2, 1, 20, CausalContext::new(), part_value.clone()); + let mut differing_value = part_value; + differing_value.name = String::from("something else"); + let differing = part_definition_env(3, 1, 30, CausalContext::new(), differing_value); + let mut set = OperationSet::new(); + set.accept_all(vec![differing.clone(), identical.clone(), create.clone()]); + let state = set.reduce(); + assert_eq!(effect_of(&state, create.id), Some(OperationEffect::Applied)); + assert_eq!(effect_of(&state, identical.id), already_applied); + assert_eq!(effect_of(&state, differing.id), mismatch); + + // CreateAnalysisLayer. + let layer_value = crate::valuegen::analysis_layer(AnalysisLayerId::new(ReplicaId(9), 1)); + let create = analysis_layer_env(1, 2, 10, CausalContext::new(), layer_value.clone()); + let identical = analysis_layer_env(2, 2, 20, CausalContext::new(), layer_value.clone()); + let mut differing_value = layer_value; + differing_value.name = String::from("something else"); + let differing = analysis_layer_env(3, 2, 30, CausalContext::new(), differing_value); + let mut set = OperationSet::new(); + set.accept_all(vec![differing.clone(), identical.clone(), create.clone()]); + let state = set.reduce(); + assert_eq!(effect_of(&state, create.id), Some(OperationEffect::Applied)); + assert_eq!(effect_of(&state, identical.id), already_applied); + assert_eq!(effect_of(&state, differing.id), mismatch); + + // CreateView. + let view_value = crate::valuegen::view(ViewId::new(ReplicaId(9), 1), vec![]); + let create = view_env(1, 3, 10, CausalContext::new(), view_value.clone()); + let identical = view_env(2, 3, 20, CausalContext::new(), view_value.clone()); + let mut differing_value = view_value; + differing_value.name = String::from("something else"); + let differing = view_env(3, 3, 30, CausalContext::new(), differing_value); + let mut set = OperationSet::new(); + set.accept_all(vec![differing.clone(), identical.clone(), create.clone()]); + let state = set.reduce(); + assert_eq!(effect_of(&state, create.id), Some(OperationEffect::Applied)); + assert_eq!(effect_of(&state, identical.id), already_applied); + assert_eq!(effect_of(&state, differing.id), mismatch); + } + + /// (t5b, t13 baseline) Re-carry against a *base*-derived entity: reduce + /// `new_onto` a score whose four G3a vectors are already populated, then + /// re-carry each byte-identically and get `AlreadyApplied`. A re-carry + /// test that only ever reduces from empty (t5) cannot see a missing base + /// seed at all (pin 4a, site 4) — this is why this row is separate from + /// t5. + /// + /// **Mutation (t13, four separate sub-mutations, each run alone):** skip + /// seeding `staff_group_values` / `part_definition_values` / + /// `analysis_layer_values` / `view_values` in `seed_from_graph`, one at a + /// time. Each must make the corresponding assertion below misreport + /// `RecreateContentMismatch` instead of `AlreadyApplied`. + #[test] + fn t5b_base_recarry_is_idempotent_for_all_four_g3a_families() { + let group = crate::valuegen::staff_group(StaffGroupId::new(ReplicaId(1), 1), vec![]); + let part = crate::valuegen::part_definition(PartDefinitionId::new(ReplicaId(1), 2), vec![]); + let layer = crate::valuegen::analysis_layer(AnalysisLayerId::new(ReplicaId(1), 3)); + let view = crate::valuegen::view(ViewId::new(ReplicaId(1), 4), vec![]); + + let mut base = Score::empty(IdentityContext::new(ReplicaId(1))); + base.staff_groups.push(group.clone()); + base.parts.push(part.clone()); + base.analysis_layers.push(layer.clone()); + base.views.push(view.clone()); + + let recreate_group = staff_group_env(2, 0, 10, CausalContext::new(), group); + let recreate_part = part_definition_env(2, 1, 11, CausalContext::new(), part); + let recreate_layer = analysis_layer_env(2, 2, 12, CausalContext::new(), layer); + let recreate_view = view_env(2, 3, 13, CausalContext::new(), view); + + let mut set = OperationSet::new(); + set.accept_all(vec![ + recreate_group.clone(), + recreate_part.clone(), + recreate_layer.clone(), + recreate_view.clone(), + ]); + let out = reduce_operation_set_onto(&set, &base); + + let effect_of = |id: OperationId| { + out.state + .effects + .iter() + .find(|(e, _)| *e == id) + .map(|(_, eff)| eff.clone()) + }; + let already_applied = Some(OperationEffect::NoOp { + reason: NoOpReason::AlreadyApplied, + }); + assert_eq!( + effect_of(recreate_group.id), + already_applied, + "StaffGroup base re-carry must be idempotent (pin 4a site 4)" + ); + assert_eq!( + effect_of(recreate_part.id), + already_applied, + "PartDefinition base re-carry must be idempotent (pin 4a site 4)" + ); + assert_eq!( + effect_of(recreate_layer.id), + already_applied, + "AnalysisLayer base re-carry must be idempotent (pin 4a site 4)" + ); + assert_eq!( + effect_of(recreate_view.id), + already_applied, + "View base re-carry must be idempotent (pin 4a site 4)" + ); + + assert_eq!(out.score.staff_groups.len(), 1, "no duplicate group minted"); + assert_eq!(out.score.parts.len(), 1, "no duplicate part minted"); + assert_eq!( + out.score.analysis_layers.len(), + 1, + "no duplicate layer minted" + ); + assert_eq!(out.score.views.len(), 1, "no duplicate view minted"); + } + + /// (t6) Genesis tranche G3a: all three referential loops refuse under a + /// graph, each asserted separately: `CreateStaffGroup.members`, + /// `CreatePartDefinition.staves`, and `CreateView.active_layers` naming a + /// non-live target are each `TargetMissing`. + /// + /// **Mutation (three separate sub-mutations, each run alone):** drop the + /// members loop from `create_staff_group`; drop the staves loop from + /// `create_part_definition`; drop the active-layers loop from + /// `create_view`. Each must fail on its own row. + #[test] + fn t6_g3a_referential_loops_refuse_a_dangling_target_under_a_graph() { + let target_missing = Some(OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::TargetMissing, + }, + }); + + // CreateStaffGroup.members naming a non-live staff. + let dangling_staff = StaffId::new(ReplicaId(1), 99); + let group = + crate::valuegen::staff_group(StaffGroupId::new(ReplicaId(1), 1), vec![dangling_staff]); + let group_env = staff_group_env(2, 0, 10, CausalContext::new(), group); + let mut set = OperationSet::new(); + set.accept_all(vec![group_env.clone()]); + let out = + reduce_operation_set_onto(&set, &Score::empty(IdentityContext::new(ReplicaId(2)))); + assert_eq!( + out.state + .effects + .iter() + .find(|(e, _)| *e == group_env.id) + .map(|(_, eff)| eff.clone()), + target_missing, + "CreateStaffGroup.members naming a non-live staff must refuse TargetMissing" + ); + + // CreatePartDefinition.staves naming a non-live staff. + let part = crate::valuegen::part_definition( + PartDefinitionId::new(ReplicaId(1), 1), + vec![dangling_staff], + ); + let part_env = part_definition_env(3, 0, 10, CausalContext::new(), part); + let mut set = OperationSet::new(); + set.accept_all(vec![part_env.clone()]); + let out = + reduce_operation_set_onto(&set, &Score::empty(IdentityContext::new(ReplicaId(3)))); + assert_eq!( + out.state + .effects + .iter() + .find(|(e, _)| *e == part_env.id) + .map(|(_, eff)| eff.clone()), + target_missing, + "CreatePartDefinition.staves naming a non-live staff must refuse TargetMissing" + ); + + // CreateView.active_layers naming a non-live analysis layer. + let dangling_layer = AnalysisLayerId::new(ReplicaId(1), 99); + let view = crate::valuegen::view(ViewId::new(ReplicaId(1), 1), vec![dangling_layer]); + let view_env_ = view_env(4, 0, 10, CausalContext::new(), view); + let mut set = OperationSet::new(); + set.accept_all(vec![view_env_.clone()]); + let out = + reduce_operation_set_onto(&set, &Score::empty(IdentityContext::new(ReplicaId(4)))); + assert_eq!( + out.state + .effects + .iter() + .find(|(e, _)| *e == view_env_.id) + .map(|(_, eff)| eff.clone()), + target_missing, + "CreateView.active_layers naming a non-live analysis layer must refuse TargetMissing" + ); + } + + /// (t7) Genesis tranche G3a: those same referential preconditions are + /// **not** enforced base-free — base-free reduction has no universe to + /// check against. + /// + /// **Mutation:** remove the `if self.graph.is_some()` guard from + /// `create_staff_group`; must fail — the dangling member now refuses even + /// base-free. + #[test] + fn t7_g3a_referential_preconditions_are_not_enforced_base_free() { + let dangling_staff = StaffId::new(ReplicaId(1), 99); + let group = + crate::valuegen::staff_group(StaffGroupId::new(ReplicaId(1), 1), vec![dangling_staff]); + let group_env = staff_group_env(2, 0, 10, CausalContext::new(), group); + let mut set = OperationSet::new(); + set.accept_all(vec![group_env.clone()]); + let state = set.reduce(); // base-free + assert_eq!( + state + .effects + .iter() + .find(|(e, _)| *e == group_env.id) + .map(|(_, eff)| eff.clone()), + Some(OperationEffect::Applied), + "base-free reduction has no staff universe to check the dangling member against" + ); + } + + /// (t8) From empty, `CreateInstrument` -> `CreateStaffGroup` -> + /// `CreateStaff` **with `group: Some(...)`** succeeds and reaches a note + /// — i.e. `CreateStaff`'s group precondition becomes **satisfiable**. + /// **Claim narrowed per §1.1**: this asserts satisfiability, NOT that a + /// bidirectionally consistent group is authorable. + /// + /// **Mutation:** replace the `CreateStaffGroup` dispatch arm with + /// `OperationKind::CreateStaffGroup(_) => OperationEffect::Applied`, + /// keeping the match exhaustive; must fail at the grouped-staff assertion + /// while the spine stays applied. + #[test] + fn t8_create_staff_group_then_create_staff_makes_the_group_precondition_satisfiable() { + let instrument_id = InstrumentId::new(ReplicaId(1), 1); + let group_id = StaffGroupId::new(ReplicaId(1), 3); + let staff_id = StaffId::new(ReplicaId(1), 5); + + let create_instrument = prim_env( + 1, + 0, + 10, + CausalContext::new(), + OperationKind::CreateInstrument(CreateInstrumentOp { + instrument: crate::valuegen::instrument(instrument_id), + }), + ); + let create_group = staff_group_env( + 1, + 2, + 20, + CausalContext::new(), + crate::valuegen::staff_group(group_id, vec![]), + ); + let mut staff = crate::valuegen::staff(staff_id, instrument_id); + staff.group = Some(group_id); + let create_staff = prim_env( + 1, + 4, + 30, + CausalContext::new(), + OperationKind::CreateStaff(CreateStaffOp { staff }), + ); + + let mut set = OperationSet::new(); + set.accept_all(vec![create_instrument, create_group, create_staff]); + let out = + reduce_operation_set_onto(&set, &Score::empty(IdentityContext::new(ReplicaId(1)))); + + for counter in [0, 2, 4] { + assert_eq!( + effect_at(&out.state, counter), + Some(&OperationEffect::Applied), + "spine operation at counter {counter} must apply" + ); + } + let staff = out + .score + .staves + .iter() + .find(|s| s.id == staff_id) + .expect("staff minted"); + assert_eq!( + staff.group, + Some(group_id), + "the staff's group precondition is now satisfiable" + ); + } + + /// (t8b) Both permitted stale forms (§1.1, disposition B) are pinned: + /// the **missing** form (`CreateStaffGroup(g, [])` -> + /// `CreateStaff(s, Some(g))` ⟹ `s.group == Some(g)`, `g.members == []`) + /// and the **spurious** form (`CreateStaff(s, None)` -> + /// `CreateStaffGroup(g, [s])` ⟹ `g.members == [s]`, `s.group == None`). + /// Both asserted to **hold**, as states the ruling permits. + /// + /// **Mutation 1 (missing form; mutate `create_staff`, which runs SECOND + /// in this order):** when `group` is `Some(g)`, append the newly minted + /// staff to `g.members` (disposition A's maintenance rule); the + /// missing-form assertion must fail. + /// + /// **Mutation 2 (spurious form; mutate `create_staff_group`, which runs + /// SECOND in this order):** reject or normalize away a non-empty carried + /// `members`; the spurious-form assertion must fail. + /// + /// An earlier draft assigned these the other way round, which is + /// impossible in both directions: `create_staff_group` runs first in the + /// missing order and cannot append a staff that does not exist yet, and + /// `create_staff` runs first in the spurious order and has no later group + /// to repair. + #[test] + fn t8b_both_permitted_stale_forms_hold() { + let instrument_id = InstrumentId::new(ReplicaId(1), 1); + + // Missing form: CreateStaffGroup(g, []) -> CreateStaff(s, Some(g)). + let group_id = StaffGroupId::new(ReplicaId(1), 3); + let staff_id = StaffId::new(ReplicaId(1), 5); + let create_instrument = prim_env( + 1, + 0, + 10, + CausalContext::new(), + OperationKind::CreateInstrument(CreateInstrumentOp { + instrument: crate::valuegen::instrument(instrument_id), + }), + ); + let create_group = staff_group_env( + 1, + 2, + 20, + CausalContext::new(), + crate::valuegen::staff_group(group_id, vec![]), + ); + let mut staff = crate::valuegen::staff(staff_id, instrument_id); + staff.group = Some(group_id); + let create_staff = prim_env( + 1, + 4, + 30, + CausalContext::new(), + OperationKind::CreateStaff(CreateStaffOp { staff }), + ); + let mut set = OperationSet::new(); + set.accept_all(vec![create_instrument, create_group, create_staff]); + let out = + reduce_operation_set_onto(&set, &Score::empty(IdentityContext::new(ReplicaId(1)))); + let staff = out + .score + .staves + .iter() + .find(|s| s.id == staff_id) + .expect("staff minted"); + assert_eq!( + staff.group, + Some(group_id), + "missing form: s.group == Some(g)" + ); + let group = out + .score + .staff_groups + .iter() + .find(|g| g.id == group_id) + .expect("group minted"); + assert_eq!( + group.members, + Vec::::new(), + "missing form: g.members stays empty (stored, not maintained)" + ); + + // Spurious form: CreateStaff(s, None) -> CreateStaffGroup(g, [s]). + let group_id2 = StaffGroupId::new(ReplicaId(2), 3); + let staff_id2 = StaffId::new(ReplicaId(2), 5); + let create_instrument2 = prim_env( + 2, + 0, + 10, + CausalContext::new(), + OperationKind::CreateInstrument(CreateInstrumentOp { + instrument: crate::valuegen::instrument(instrument_id), + }), + ); + let staff2 = crate::valuegen::staff(staff_id2, instrument_id); + let create_staff2 = prim_env( + 2, + 2, + 20, + CausalContext::new(), + OperationKind::CreateStaff(CreateStaffOp { staff: staff2 }), + ); + let create_group2 = staff_group_env( + 2, + 4, + 30, + CausalContext::new(), + crate::valuegen::staff_group(group_id2, vec![staff_id2]), + ); + let mut set2 = OperationSet::new(); + set2.accept_all(vec![create_instrument2, create_staff2, create_group2]); + let out2 = + reduce_operation_set_onto(&set2, &Score::empty(IdentityContext::new(ReplicaId(2)))); + let group2 = out2 + .score + .staff_groups + .iter() + .find(|g| g.id == group_id2) + .expect("group minted"); + assert_eq!( + group2.members, + vec![staff_id2], + "spurious form: g.members == [s]" + ); + let staff2 = out2 + .score + .staves + .iter() + .find(|s| s.id == staff_id2) + .expect("staff minted"); + assert_eq!(staff2.group, None, "spurious form: s.group == None"); + } + + /// (t9) A score reduced from empty through all four G3a ops **passes + /// `check_invariants`**, and each skipped reducer check **independently** + /// makes invariant 10 fire (pin 5). The fixture is constant across all + /// mutations: it already attempts a dangling reference in each of the + /// three referential loops, and passes only because the reducer refuses + /// them. + /// + /// **Mutation (three separate sub-mutations, each run alone; mutate + /// production only):** drop the members loop from `create_staff_group` / + /// the staves loop from `create_part_definition` / the active-layers loop + /// from `create_view`. Each must let its dangling reference through and + /// make invariant 10 fire. + #[test] + fn t9_from_empty_through_all_four_g3a_ops_passes_check_invariants() { + let dangling_staff_a = StaffId::new(ReplicaId(1), 90); + let dangling_staff_b = StaffId::new(ReplicaId(1), 91); + let dangling_layer = AnalysisLayerId::new(ReplicaId(1), 92); + + let create_group = staff_group_env( + 1, + 0, + 10, + CausalContext::new(), + crate::valuegen::staff_group( + StaffGroupId::new(ReplicaId(1), 1), + vec![dangling_staff_a], + ), + ); + let create_part = part_definition_env( + 1, + 2, + 20, + CausalContext::new(), + crate::valuegen::part_definition( + PartDefinitionId::new(ReplicaId(1), 2), + vec![dangling_staff_b], + ), + ); + let create_view = view_env( + 1, + 4, + 30, + CausalContext::new(), + crate::valuegen::view(ViewId::new(ReplicaId(1), 3), vec![dangling_layer]), + ); + let create_layer = analysis_layer_env( + 1, + 6, + 40, + CausalContext::new(), + crate::valuegen::analysis_layer(AnalysisLayerId::new(ReplicaId(1), 4)), + ); + + let mut set = OperationSet::new(); + set.accept_all(vec![create_group, create_part, create_view, create_layer]); + let out = + reduce_operation_set_onto(&set, &Score::empty(IdentityContext::new(ReplicaId(1)))); + + // Baseline: the reducer refused all three dangling references, so the + // reduced score passes invariant 10 (and every other invariant). + assert!( + epiphany_core::check_invariants(&out.score).is_empty(), + "baseline: the reducer's refusals must keep the score invariant-clean: {:?}", + epiphany_core::check_invariants(&out.score) + ); + let target_missing = Some(&OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::TargetMissing, + }, + }); + assert_eq!( + effect_at(&out.state, 0), + target_missing, + "CreateStaffGroup with a dangling member must refuse" + ); + assert_eq!( + effect_at(&out.state, 2), + target_missing, + "CreatePartDefinition with a dangling staff must refuse" + ); + assert_eq!( + effect_at(&out.state, 4), + target_missing, + "CreateView with a dangling active layer must refuse" + ); + assert_eq!( + effect_at(&out.state, 6), + Some(&OperationEffect::Applied), + "CreateAnalysisLayer has no referential precondition and applies" + ); + } + + /// (t10) Genesis tranche G3a: all four kinds carry epoch 11, and a block + /// containing them stamps schema minor 11. + /// + /// **Mutation:** assign epoch 10 to one kind (e.g. `CreateStaffGroup`) in + /// `introduced_minor`; must fail. Run against **both** epoch sites + /// separately (the vocabulary annotation, and the s1 table in + /// `payload.rs`) — each must be independently able to fail. + #[test] + fn t10_g3a_kinds_carry_epoch_11() { + let kinds = g3a_sample_kinds(); + for kind in &kinds { + assert_eq!(kind.introduced_minor(), Some(11), "{:?}", kind.tag()); + assert_eq!(kind.tag().introduced_minor(), Some(11), "{:?}", kind.tag()); + } + let env = prim_env(1, 1, 1, CausalContext::new(), kinds[0].clone()); + assert_eq!( + crate::payload::operation_block_introduced_minor(&[env]), + Some(11), + "a block containing a G3a kind stamps minor 11" + ); + } } diff --git a/crates/epiphany-ops/src/textproj_kind.rs b/crates/epiphany-ops/src/textproj_kind.rs index 281addb..18c59c4 100644 --- a/crates/epiphany-ops/src/textproj_kind.rs +++ b/crates/epiphany-ops/src/textproj_kind.rs @@ -8,15 +8,16 @@ use epiphany_determinism::{sorted_canonical, CanonicalEncode}; use unicode_normalization::UnicodeNormalization; use crate::payload::{ - ChangeRegionTimeModelOp, CreateCrossCuttingOp, CreateInstrumentOp, CreateRegionOp, - CreateRepeatStructureOp, CreateStaffInstanceOp, CreateStaffOp, CreateVoiceOp, - DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp, DeleteRegionOp, - DeleteRepeatStructureOp, DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, - InsertIdentifiedPitchOp, ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, - OperationKind, OperationKindTag, RespellPitchOp, SetCanvasLayoutDefaultsOp, SetMetadataOp, - SetMetricGridOp, SetSpellingPrecedenceOp, SetStaffLayoutOp, SetTempoSegmentOp, - SetTimeSignatureOp, SetTuningContextOp, SetUserPageBreakOp, SetUserSystemBreakOp, - TransactionDescriptor, TransposeIntervalOp, TransposeOp, + ChangeRegionTimeModelOp, CreateAnalysisLayerOp, CreateCrossCuttingOp, CreateInstrumentOp, + CreatePartDefinitionOp, CreateRegionOp, CreateRepeatStructureOp, CreateStaffGroupOp, + CreateStaffInstanceOp, CreateStaffOp, CreateViewOp, CreateVoiceOp, DeleteCrossCuttingOp, + DeleteEventOp, DeleteIdentifiedPitchOp, DeleteRegionOp, DeleteRepeatStructureOp, + DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, InsertIdentifiedPitchOp, + ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, OperationKind, OperationKindTag, + RespellPitchOp, SetCanvasLayoutDefaultsOp, SetMetadataOp, SetMetricGridOp, + SetSpellingPrecedenceOp, SetStaffLayoutOp, SetTempoSegmentOp, SetTimeSignatureOp, + SetTuningContextOp, SetUserPageBreakOp, SetUserSystemBreakOp, TransactionDescriptor, + TransposeIntervalOp, TransposeOp, }; use crate::support::OperationKindRegistryId; @@ -232,6 +233,14 @@ impl TextValue for OperationKind { OperationKind::SetTuningContext(op) => { production(self.tag(), vec![op.settings.project()]) } + OperationKind::CreateStaffGroup(op) => production(self.tag(), vec![op.group.project()]), + OperationKind::CreatePartDefinition(op) => { + production(self.tag(), vec![op.part.project()]) + } + OperationKind::CreateAnalysisLayer(op) => { + production(self.tag(), vec![op.layer.project()]) + } + OperationKind::CreateView(op) => production(self.tag(), vec![op.view.project()]), } } @@ -577,6 +586,38 @@ impl TextValue for OperationKind { settings: TextValue::parse(settings)?, }) } + OperationKindTag::CreateStaffGroup => { + let [group] = fields(s, tag, 1)? else { + unreachable!("the arity-1 check returned one field") + }; + OperationKind::CreateStaffGroup(CreateStaffGroupOp { + group: TextValue::parse(group)?, + }) + } + OperationKindTag::CreatePartDefinition => { + let [part] = fields(s, tag, 1)? else { + unreachable!("the arity-1 check returned one field") + }; + OperationKind::CreatePartDefinition(CreatePartDefinitionOp { + part: TextValue::parse(part)?, + }) + } + OperationKindTag::CreateAnalysisLayer => { + let [layer] = fields(s, tag, 1)? else { + unreachable!("the arity-1 check returned one field") + }; + OperationKind::CreateAnalysisLayer(CreateAnalysisLayerOp { + layer: TextValue::parse(layer)?, + }) + } + OperationKindTag::CreateView => { + let [view] = fields(s, tag, 1)? else { + unreachable!("the arity-1 check returned one field") + }; + OperationKind::CreateView(CreateViewOp { + view: TextValue::parse(view)?, + }) + } }) } } @@ -630,7 +671,7 @@ mod tests { #[test] fn every_operation_kind_round_trips_with_canonical_text() { let tags: Vec<_> = all_tags().collect(); - assert_eq!(tags.len(), 35, "the grammar has 35 kind productions"); + assert_eq!(tags.len(), 39, "the grammar has 39 kind productions"); for tag in tags { round_trip(&sample_kind(tag)); } diff --git a/crates/epiphany-ops/src/v0.rs b/crates/epiphany-ops/src/v0.rs index c5163b4..7a65776 100644 --- a/crates/epiphany-ops/src/v0.rs +++ b/crates/epiphany-ops/src/v0.rs @@ -116,6 +116,13 @@ pub enum V0OperationKind { // predates the catalog entirely), so it round-trips by identity like // every other v1-native kind above. SetTuningContext(crate::payload::SetTuningContextOp), + // Genesis tranche G3a — born at wire-discs 35..=38; no lossy v0 form (v0 + // predates the catalog entirely), so all four round-trip by identity like + // every other v1-native kind above. + CreateStaffGroup(crate::payload::CreateStaffGroupOp), + CreatePartDefinition(crate::payload::CreatePartDefinitionOp), + CreateAnalysisLayer(crate::payload::CreateAnalysisLayerOp), + CreateView(crate::payload::CreateViewOp), } /// v0 `InsertEvent`: the event was a bare [`EventId`] plus the reduction-relevant diff --git a/crates/epiphany-ops/src/valuegen.rs b/crates/epiphany-ops/src/valuegen.rs index 0b70f07..f1660f9 100644 --- a/crates/epiphany-ops/src/valuegen.rs +++ b/crates/epiphany-ops/src/valuegen.rs @@ -16,14 +16,15 @@ use std::collections::BTreeMap; use epiphany_core::{ AcousticPitch, AcousticRealization, AleatoricAnchoringDiscipline, AleatoricTimeModel, - AnchorOffset, Beam, BeamId, CmnNominal, Event, EventId, EventOrderingDAG, EventPosition, - IdentifiedPitch, MetricTimeModel, MusicalDuration, MusicalPosition, Pitch, PitchId, - PitchSpaceId, PitchSpacePosition, PitchSpelling, PitchedEvent, ProportionalTimeModel, Region, - RegionContent, RegionEdge, RegionId, RegionTimeModel, RepeatKind, RepeatStructure, - RepeatStructureId, Rest, ScalePosition, Slur, SlurId, SpellingAttachment, SpellingDirective, - SpellingScope, SpellingSource, StaffBasedContent, StaffExtent, StaffId, StaffInstance, - StaffInstanceId, StemConfiguration, Tie, TieClass, TieId, TimeAnchor, TimeExtent, Voice, - VoiceId, VoiceOrigin, Volta, WallClockDuration, WallClockTime, + AnalysisLayerId, AnchorOffset, Beam, BeamId, CmnNominal, Event, EventId, EventOrderingDAG, + EventPosition, IdentifiedPitch, MetricTimeModel, MusicalDuration, MusicalPosition, + PartDefinitionId, Pitch, PitchId, PitchSpaceId, PitchSpacePosition, PitchSpelling, + PitchedEvent, ProportionalTimeModel, Region, RegionContent, RegionEdge, RegionId, + RegionTimeModel, RepeatKind, RepeatStructure, RepeatStructureId, Rest, ScalePosition, Slur, + SlurId, SpellingAttachment, SpellingDirective, SpellingScope, SpellingSource, + StaffBasedContent, StaffExtent, StaffGroupId, StaffId, StaffInstance, StaffInstanceId, + StemConfiguration, Tie, TieClass, TieId, TimeAnchor, TimeExtent, ViewId, Voice, VoiceId, + VoiceOrigin, Volta, WallClockDuration, WallClockTime, }; /// A deterministic, fully-specified C4 pitch in the cmn-12 space — the neutral @@ -368,6 +369,61 @@ pub fn instrument(id: epiphany_core::InstrumentId) -> epiphany_core::Instrument epiphany_core::Instrument::new(id, format!("instrument-{}", id.counter())) } +/// A minimal [`StaffGroup`](epiphany_core::StaffGroup) (genesis tranche G3a) — +/// the value a `CreateStaffGroup` mints: named for its counter, a grand-staff +/// kind, and the given `members` list carried exactly as given (contract +/// §1.1, disposition B — this helper never normalizes `members`). +pub fn staff_group(id: StaffGroupId, members: Vec) -> epiphany_core::StaffGroup { + epiphany_core::StaffGroup { + id, + name: Some(format!("staff-group-{}", id.counter())), + kind: epiphany_core::StaffGroupKind::GrandStaff, + members, + } +} + +/// A minimal [`PartDefinition`](epiphany_core::PartDefinition) (genesis +/// tranche G3a) — the value a `CreatePartDefinition` mints: named for its +/// counter, referencing the given `staves`. +pub fn part_definition( + id: PartDefinitionId, + staves: Vec, +) -> epiphany_core::PartDefinition { + epiphany_core::PartDefinition { + id, + name: format!("part-{}", id.counter()), + staves, + } +} + +/// A minimal [`AnalysisLayer`](epiphany_core::AnalysisLayer) (genesis tranche +/// G3a) — the value a `CreateAnalysisLayer` mints: named for its counter, with +/// no outbound entity reference. +pub fn analysis_layer(id: AnalysisLayerId) -> epiphany_core::AnalysisLayer { + epiphany_core::AnalysisLayer { + id, + // Deliberately NOT `id`-width (16 bytes): a name that happens to + // share its encoded byte length with the 16-byte `id` field would + // make a `struct_codec!` field swap byte-invisible for this specific + // value (both fields are length-prefixed leaves; swapping two + // same-width leaves reproduces identical bytes) — silently defeating + // the one referential guard a two-field struct's literal-byte vector + // (contract t4) has to catch a reorder with. + name: format!("layer-{}", id.counter()), + } +} + +/// A minimal [`ViewDefinition`](epiphany_core::ViewDefinition) (genesis +/// tranche G3a) — the value a `CreateView` mints: named for its counter, +/// referencing the given `active_layers`. +pub fn view(id: ViewId, active_layers: Vec) -> epiphany_core::ViewDefinition { + epiphany_core::ViewDefinition { + id, + name: format!("view-{}", id.counter()), + active_layers, + } +} + /// Canvas layout defaults with an `nth`-distinct page width (genesis tranche /// G2a) — distinct `nth` give distinct `CanvasLayoutDefaults` values so a /// harness can drive concurrent `SetCanvasLayoutDefaults`s, an advisory LWW diff --git a/crates/epiphany-ops/src/vectors.rs b/crates/epiphany-ops/src/vectors.rs index 826d1d7..1ddeb78 100644 --- a/crates/epiphany-ops/src/vectors.rs +++ b/crates/epiphany-ops/src/vectors.rs @@ -16,7 +16,10 @@ //! The `class` string is informative, not normative: implementations need not //! agree on error taxonomy, only on the accept/reject verdict. -use epiphany_core::{EventId, InstrumentId, OperationId, ReplicaId, TypedObjectId}; +use epiphany_core::{ + AnalysisLayerId, EventId, InstrumentId, OperationId, PartDefinitionId, ReplicaId, StaffGroupId, + StaffId, TypedObjectId, ViewId, +}; use epiphany_determinism::CanonicalEncode; use crate::{ @@ -411,6 +414,156 @@ pub fn decode_vectors() -> Vec { tuning_trailing, )); + // --- OperationEnvelope carrying the four genesis tranche G3a root-level + // mints (`spec/CONTRACT_GENESIS_G3A_ENTITIES.md`) — same rationale as the + // siblings above: nothing else in this corpus exercises any of these four + // payloads' decode paths, and a round-trip check alone cannot see a + // self-consistent encoder/decoder reorder (the 3b-i lesson; contract t4). + let staff_group_envelope = OperationEnvelope { + id: OperationId::new(ReplicaId(1), 5), + author: crate::support::AuthorId(0), + stamp: crate::stamp::OperationStamp::new( + crate::stamp::HybridLogicalClock::new(epiphany_core::WallClockTime(1), 1), + OperationId::new(ReplicaId(1), 5), + ), + causal_context: crate::causal::CausalContext::new(), + transaction: None, + payload: crate::payload::OperationPayload::Primitive( + crate::payload::OperationKind::CreateStaffGroup(crate::payload::CreateStaffGroupOp { + group: crate::valuegen::staff_group( + StaffGroupId::new(ReplicaId(1), 1), + vec![StaffId::new(ReplicaId(1), 1)], + ), + }), + ), + }; + let staff_group_envelope_bytes = staff_group_envelope.to_canonical_bytes(); + v.push(row( + OE, + "accept", + "-", + "create_staff_group", + staff_group_envelope_bytes.clone(), + )); + let mut staff_group_trailing = staff_group_envelope_bytes; + staff_group_trailing.push(0); + v.push(row( + OE, + "reject", + "trailing-bytes", + "create_staff_group_trailing", + staff_group_trailing, + )); + + let part_definition_envelope = OperationEnvelope { + id: OperationId::new(ReplicaId(1), 6), + author: crate::support::AuthorId(0), + stamp: crate::stamp::OperationStamp::new( + crate::stamp::HybridLogicalClock::new(epiphany_core::WallClockTime(1), 1), + OperationId::new(ReplicaId(1), 6), + ), + causal_context: crate::causal::CausalContext::new(), + transaction: None, + payload: crate::payload::OperationPayload::Primitive( + crate::payload::OperationKind::CreatePartDefinition( + crate::payload::CreatePartDefinitionOp { + part: crate::valuegen::part_definition( + PartDefinitionId::new(ReplicaId(1), 1), + vec![StaffId::new(ReplicaId(1), 1)], + ), + }, + ), + ), + }; + let part_definition_envelope_bytes = part_definition_envelope.to_canonical_bytes(); + v.push(row( + OE, + "accept", + "-", + "create_part_definition", + part_definition_envelope_bytes.clone(), + )); + let mut part_definition_trailing = part_definition_envelope_bytes; + part_definition_trailing.push(0); + v.push(row( + OE, + "reject", + "trailing-bytes", + "create_part_definition_trailing", + part_definition_trailing, + )); + + let analysis_layer_envelope = OperationEnvelope { + id: OperationId::new(ReplicaId(1), 7), + author: crate::support::AuthorId(0), + stamp: crate::stamp::OperationStamp::new( + crate::stamp::HybridLogicalClock::new(epiphany_core::WallClockTime(1), 1), + OperationId::new(ReplicaId(1), 7), + ), + causal_context: crate::causal::CausalContext::new(), + transaction: None, + payload: crate::payload::OperationPayload::Primitive( + crate::payload::OperationKind::CreateAnalysisLayer( + crate::payload::CreateAnalysisLayerOp { + layer: crate::valuegen::analysis_layer(AnalysisLayerId::new(ReplicaId(1), 1)), + }, + ), + ), + }; + let analysis_layer_envelope_bytes = analysis_layer_envelope.to_canonical_bytes(); + v.push(row( + OE, + "accept", + "-", + "create_analysis_layer", + analysis_layer_envelope_bytes.clone(), + )); + let mut analysis_layer_trailing = analysis_layer_envelope_bytes; + analysis_layer_trailing.push(0); + v.push(row( + OE, + "reject", + "trailing-bytes", + "create_analysis_layer_trailing", + analysis_layer_trailing, + )); + + let view_envelope = OperationEnvelope { + id: OperationId::new(ReplicaId(1), 8), + author: crate::support::AuthorId(0), + stamp: crate::stamp::OperationStamp::new( + crate::stamp::HybridLogicalClock::new(epiphany_core::WallClockTime(1), 1), + OperationId::new(ReplicaId(1), 8), + ), + causal_context: crate::causal::CausalContext::new(), + transaction: None, + payload: crate::payload::OperationPayload::Primitive( + crate::payload::OperationKind::CreateView(crate::payload::CreateViewOp { + view: crate::valuegen::view( + ViewId::new(ReplicaId(1), 1), + vec![AnalysisLayerId::new(ReplicaId(1), 1)], + ), + }), + ), + }; + let view_envelope_bytes = view_envelope.to_canonical_bytes(); + v.push(row( + OE, + "accept", + "-", + "create_view", + view_envelope_bytes.clone(), + )); + let mut view_trailing = view_envelope_bytes; + view_trailing.push(0); + v.push(row( + OE, + "reject", + "trailing-bytes", + "create_view_trailing", + view_trailing, + )); + v } @@ -622,4 +775,128 @@ mod tests { "the committed literal bytes must decode and re-encode injectively" ); } + + /// (t4) Genesis tranche G3a (`spec/CONTRACT_GENESIS_G3A_ENTITIES.md`): the + /// `CreateStaffGroup` envelope decode vector, pinned to a literal byte + /// array copied from the committed corpus — not derived by calling + /// `.to_canonical_bytes()` here, for the same reason as the sibling tests + /// above (the 3b-i lesson: round-trip locking alone cannot see a + /// self-consistent encoder/decoder reorder). **Mutation:** swap two + /// fields in `StaffGroup`'s `struct_codec!` declaration + /// (`core/src/codec.rs:2329`, e.g. `{ id, name, kind, members }` → + /// `{ id, kind, name, members }`); this literal-byte vector must fail + /// while every round-trip test stays green. + #[test] + fn create_staff_group_envelope_decode_vector_is_pinned_to_literal_bytes() { + #[rustfmt::skip] + let bytes: Vec = vec![ + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 35, 63, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 13, 0, + 0, 0, 115, 116, 97, 102, 102, 45, 103, 114, 111, 117, 112, 45, + 49, 0, 1, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, + ]; + let result = check("ops.operation_envelope", &bytes) + .expect("ops.operation_envelope is owned by this crate"); + assert_eq!( + result, + Ok(true), + "the committed literal bytes must decode and re-encode injectively" + ); + } + + /// (t4) Same rationale as `create_staff_group_envelope_decode_vector_is_ + /// pinned_to_literal_bytes` above. **Mutation:** swap two fields in + /// `PartDefinition`'s `struct_codec!` declaration + /// (`core/src/codec.rs:1790`, `{ id, name, staves }` → + /// `{ id, staves, name }`); this literal-byte vector must fail while + /// every round-trip test stays green. + #[test] + fn create_part_definition_envelope_decode_vector_is_pinned_to_literal_bytes() { + #[rustfmt::skip] + let bytes: Vec = vec![ + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 36, 54, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 6, 0, 0, + 0, 112, 97, 114, 116, 45, 49, 1, 0, 0, 0, 16, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 1, + ]; + let result = check("ops.operation_envelope", &bytes) + .expect("ops.operation_envelope is owned by this crate"); + assert_eq!( + result, + Ok(true), + "the committed literal bytes must decode and re-encode injectively" + ); + } + + /// (t4) Same rationale as the sibling tests above. **Mutation:** swap two + /// fields in `AnalysisLayer`'s `struct_codec!` declaration + /// (`core/src/codec.rs:1791`, `{ id, name }` → `{ name, id }`); this + /// literal-byte vector must fail while every round-trip test stays + /// green. `valuegen::analysis_layer`'s name is deliberately not 16 bytes + /// (the `id` field's width): a same-width swap of two length-prefixed + /// leaves re-encodes byte-identically regardless of which field is + /// which, so an accidental width match would make this vector blind to + /// exactly the reorder it exists to catch. + #[test] + fn create_analysis_layer_envelope_decode_vector_is_pinned_to_literal_bytes() { + #[rustfmt::skip] + let bytes: Vec = vec![ + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 37, 31, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 7, 0, 0, + 0, 108, 97, 121, 101, 114, 45, 49, + ]; + let result = check("ops.operation_envelope", &bytes) + .expect("ops.operation_envelope is owned by this crate"); + assert_eq!( + result, + Ok(true), + "the committed literal bytes must decode and re-encode injectively" + ); + } + + /// (t4) Same rationale as the sibling tests above. **Mutation:** swap two + /// fields in `ViewDefinition`'s `struct_codec!` declaration + /// (`core/src/codec.rs:1792`, `{ id, name, active_layers }` → + /// `{ id, active_layers, name }`); this literal-byte vector must fail + /// while every round-trip test stays green. + #[test] + fn create_view_envelope_decode_vector_is_pinned_to_literal_bytes() { + #[rustfmt::skip] + let bytes: Vec = vec![ + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 38, 54, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 6, 0, 0, + 0, 118, 105, 101, 119, 45, 49, 1, 0, 0, 0, 16, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 1, + ]; + let result = check("ops.operation_envelope", &bytes) + .expect("ops.operation_envelope is owned by this crate"); + assert_eq!( + result, + Ok(true), + "the committed literal bytes must decode and re-encode injectively" + ); + } } diff --git a/crates/epiphany-testkit/src/generators.rs b/crates/epiphany-testkit/src/generators.rs index 7d46adb..95d50a4 100644 --- a/crates/epiphany-testkit/src/generators.rs +++ b/crates/epiphany-testkit/src/generators.rs @@ -33,23 +33,24 @@ use epiphany_ops::valuegen; use epiphany_ops::{ AnomalousReplicaSegment, AuthorId, CausalContext, ChangeRegionTimeModelOp, ConflictId, ConflictKind, ConflictKindRegistryId, ConflictRecord, ConflictRegistry, - ConflictResolutionState, CreateCrossCuttingOp, CreateInstrumentOp, CreateRegionOp, - CreateRepeatStructureOp, CreateStaffInstanceOp, CreateStaffOp, CreateVoiceOp, - CrossCuttingValue, DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp, - DeleteRegionOp, DeleteRepeatStructureOp, DeleteStaffInstanceOp, DeleteVoiceOp, - ExtensionPreconditionId, FieldPath, HybridLogicalClock, InsertEventOp, InsertIdentifiedPitchOp, - IntegrityAnomaly, IntegrityAnomalyKind, IntegrityAnomalyRegistryId, MaterializedState, - ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, NoOpReason, ObjectKind, - ObjectState, OperationEffect, OperationEnvelope, OperationKind, OperationKindRegistryId, - OperationPayload, OperationSet, OperationStamp, PendingReason, PositionRemapping, - PreconditionFailureReason, PreconditionFailureRegistryId, ReanchorReason, - ReanchorReasonRegistryId, ReanchorResult, RepairKind, RepairKindRegistryId, RepairRecord, - ReplicaAnomalyReason, ReplicaAnomalyRegistryId, ResolutionAction, ResolutionRegistryId, - ResolveConflictPayload, RespellPitchOp, SerializedCanonicalInputs, SetCanvasLayoutDefaultsOp, - SetMetadataOp, SetMetricGridOp, SetSpellingPrecedenceOp, SetStaffLayoutOp, SetTempoSegmentOp, - SetTimeSignatureOp, SetTuningContextOp, SetUserPageBreakOp, SetUserSystemBreakOp, - TransactionCategory, TransactionDescriptor, TransposeIntervalOp, TransposeOp, - TupletCompensation, TupletCompensationKind, UndoPolicy, UndoTransactionPayload, + ConflictResolutionState, CreateAnalysisLayerOp, CreateCrossCuttingOp, CreateInstrumentOp, + CreatePartDefinitionOp, CreateRegionOp, CreateRepeatStructureOp, CreateStaffGroupOp, + CreateStaffInstanceOp, CreateStaffOp, CreateViewOp, CreateVoiceOp, CrossCuttingValue, + DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp, DeleteRegionOp, + DeleteRepeatStructureOp, DeleteStaffInstanceOp, DeleteVoiceOp, ExtensionPreconditionId, + FieldPath, HybridLogicalClock, InsertEventOp, InsertIdentifiedPitchOp, IntegrityAnomaly, + IntegrityAnomalyKind, IntegrityAnomalyRegistryId, MaterializedState, ModifyCrossCuttingOp, + ModifyEventOp, ModifyIdentifiedPitchOp, NoOpReason, ObjectKind, ObjectState, OperationEffect, + OperationEnvelope, OperationKind, OperationKindRegistryId, OperationPayload, OperationSet, + OperationStamp, PendingReason, PositionRemapping, PreconditionFailureReason, + PreconditionFailureRegistryId, ReanchorReason, ReanchorReasonRegistryId, ReanchorResult, + RepairKind, RepairKindRegistryId, RepairRecord, ReplicaAnomalyReason, ReplicaAnomalyRegistryId, + ResolutionAction, ResolutionRegistryId, ResolveConflictPayload, RespellPitchOp, + SerializedCanonicalInputs, SetCanvasLayoutDefaultsOp, SetMetadataOp, SetMetricGridOp, + SetSpellingPrecedenceOp, SetStaffLayoutOp, SetTempoSegmentOp, SetTimeSignatureOp, + SetTuningContextOp, SetUserPageBreakOp, SetUserSystemBreakOp, TransactionCategory, + TransactionDescriptor, TransposeIntervalOp, TransposeOp, TupletCompensation, + TupletCompensationKind, UndoPolicy, UndoTransactionPayload, }; use crate::rng::Rng; @@ -646,7 +647,7 @@ pub fn operation_payload(rng: &mut Rng, events: u64, pitches: u64) -> OperationP } _ => {} } - let kind = match rng.below(35) { + let kind = match rng.below(39) { 0 => { let pitches = if rng.boolean() { vec![obj_pitch(rng.below(pitches))] @@ -875,6 +876,29 @@ pub fn operation_payload(rng: &mut Rng, events: u64, pitches: u64) -> OperationP 33 => OperationKind::SetTuningContext(SetTuningContextOp { settings: valuegen::tuning_context_settings(rng.below(3) as u8), }), + // Genesis tranche G3a: the four remaining root-level mints, over the + // shared staff/analysis-layer id spaces. + 34 => OperationKind::CreateStaffGroup(CreateStaffGroupOp { + group: valuegen::staff_group( + StaffGroupId::new(OBJ_REPLICA, rng.below(2)), + vec![StaffId::new(OBJ_REPLICA, rng.below(2))], + ), + }), + 35 => OperationKind::CreatePartDefinition(CreatePartDefinitionOp { + part: valuegen::part_definition( + PartDefinitionId::new(OBJ_REPLICA, rng.below(2)), + vec![StaffId::new(OBJ_REPLICA, rng.below(2))], + ), + }), + 36 => OperationKind::CreateAnalysisLayer(CreateAnalysisLayerOp { + layer: valuegen::analysis_layer(AnalysisLayerId::new(OBJ_REPLICA, rng.below(2))), + }), + 37 => OperationKind::CreateView(CreateViewOp { + view: valuegen::view( + ViewId::new(OBJ_REPLICA, rng.below(2)), + vec![AnalysisLayerId::new(OBJ_REPLICA, rng.below(2))], + ), + }), _ => OperationKind::Registered( OperationKindRegistryId(rng.next_u64() as u128), rng.byte_vec(0, 16), @@ -1905,7 +1929,7 @@ mod tests { /// Push-4a debt) and `CreateInstrument` (kind 31, G1 debt) went missing /// from every corpus this generator feeds despite every downstream suite /// staying green. Assert a bounded draw actually reaches every kind - /// appended past the historically-tested range (discriminants 30..=34), + /// appended past the historically-tested range (discriminants 30..=38), /// not just that the function does not panic. #[test] fn operation_payload_emits_every_appended_kind() { @@ -1913,6 +1937,8 @@ mod tests { let (mut saw_transpose_interval, mut saw_create_instrument) = (false, false); let (mut saw_canvas_layout_defaults, mut saw_spelling_precedence) = (false, false); let mut saw_tuning_context = false; + let (mut saw_create_staff_group, mut saw_create_part_definition) = (false, false); + let (mut saw_create_analysis_layer, mut saw_create_view) = (false, false); for _ in 0..2000 { let OperationPayload::Primitive(kind) = operation_payload(&mut rng, 8, 8) else { continue; @@ -1923,6 +1949,10 @@ mod tests { OperationKind::SetCanvasLayoutDefaults(_) => saw_canvas_layout_defaults = true, OperationKind::SetSpellingPrecedence(_) => saw_spelling_precedence = true, OperationKind::SetTuningContext(_) => saw_tuning_context = true, + OperationKind::CreateStaffGroup(_) => saw_create_staff_group = true, + OperationKind::CreatePartDefinition(_) => saw_create_part_definition = true, + OperationKind::CreateAnalysisLayer(_) => saw_create_analysis_layer = true, + OperationKind::CreateView(_) => saw_create_view = true, _ => {} } } @@ -1946,6 +1976,22 @@ mod tests { saw_tuning_context, "SetTuningContext (kind 34, G2b debt) never drawn in 2000 samples" ); + assert!( + saw_create_staff_group, + "CreateStaffGroup (kind 35, G3a debt) never drawn in 2000 samples" + ); + assert!( + saw_create_part_definition, + "CreatePartDefinition (kind 36, G3a debt) never drawn in 2000 samples" + ); + assert!( + saw_create_analysis_layer, + "CreateAnalysisLayer (kind 37, G3a debt) never drawn in 2000 samples" + ); + assert!( + saw_create_view, + "CreateView (kind 38, G3a debt) never drawn in 2000 samples" + ); } #[test] diff --git a/crates/epiphany-testkit/src/layout_stub.rs b/crates/epiphany-testkit/src/layout_stub.rs index a376121..ab20b04 100644 --- a/crates/epiphany-testkit/src/layout_stub.rs +++ b/crates/epiphany-testkit/src/layout_stub.rs @@ -1372,7 +1372,7 @@ mod tests { /// (s10, row 29) `gen_operation_kind_tag`'s draw must cover exactly /// `PAYLOAD_FREE` ∪ `{Registered}` — **not** merely the appended - /// discriminants 30..=34, which would pass even if the `Registered` + /// discriminants 30..=38, which would pass even if the `Registered` /// append (structurally different from every built-in: it is the one /// variant `PAYLOAD_FREE` excludes by design, `payload.rs:469`) were /// deleted from the generator entirely. diff --git a/crates/epiphany-testkit/tests/text_projection_grammar.rs b/crates/epiphany-testkit/tests/text_projection_grammar.rs index 2f02519..2fa9867 100644 --- a/crates/epiphany-testkit/tests/text_projection_grammar.rs +++ b/crates/epiphany-testkit/tests/text_projection_grammar.rs @@ -305,14 +305,16 @@ fn the_kind_productions_are_the_operation_vocabulary() { // above warns about — derived list, literal total. Every tranche that // appends a tag must bump it (genesis G1 took it from 31 to 32 by adding // `CreateInstrument`; genesis G2a took it from 32 to 34 by adding - // `SetCanvasLayoutDefaults` and `SetSpellingPrecedence`; genesis G2b takes - // it from 34 to 35 by adding `SetTuningContext`). It stays a literal on + // `SetCanvasLayoutDefaults` and `SetSpellingPrecedence`; genesis G2b took + // it from 34 to 35 by adding `SetTuningContext`; genesis G3a takes it + // from 35 to 39 by adding `CreateStaffGroup`, `CreatePartDefinition`, + // `CreateAnalysisLayer`, and `CreateView`). It stays a literal on // purpose: deriving it from `PAYLOAD_FREE.len()` would make the assertion // vacuous, since that is the very list it exists to pin. assert_eq!( expected.len(), - 35, - "34 payload-free kinds plus `Registered`" + 39, + "38 payload-free kinds plus `Registered`" ); let actual = alternatives("kind"); diff --git a/crates/epiphany-textproj/src/lib.rs b/crates/epiphany-textproj/src/lib.rs index a1b4063..b1e152f 100644 --- a/crates/epiphany-textproj/src/lib.rs +++ b/crates/epiphany-textproj/src/lib.rs @@ -49,7 +49,14 @@ use epiphany_ops::OperationEnvelope; /// `set-tuning-context` to the `kind` production — the same reasoning as G1 /// and G2a: extending the grammar without moving this constant would leave /// two incompatible grammars both claiming `(0 10 0)`. -pub const COMPANION_VERSION: (u32, u32, u32) = (0, 11, 0); +/// +/// Bumped again 0.11.0 → 0.12.0 by the genesis tranche G3a +/// (`spec/CONTRACT_GENESIS_G3A_ENTITIES.md`), which appended +/// `create-staff-group`, `create-part-definition`, `create-analysis-layer`, +/// and `create-view` to the `kind` production — the same reasoning as every +/// prior kind append: extending the grammar without moving this constant +/// would leave two incompatible grammars both claiming `(0 11 0)`. +pub const COMPANION_VERSION: (u32, u32, u32) = (0, 12, 0); /// A parsed canonical Text Projection document. /// diff --git a/crates/epiphany-textproj/src/parse.rs b/crates/epiphany-textproj/src/parse.rs index ac1b522..31f157f 100644 --- a/crates/epiphany-textproj/src/parse.rs +++ b/crates/epiphany-textproj/src/parse.rs @@ -650,11 +650,11 @@ mod tests { // Bumped with `COMPANION_VERSION` (0.7.0 → 0.8.0, genesis G1; 0.8.0 → // 0.9.0, genesis G2a; 0.9.0 → 0.10.0, G-minor; 0.10.0 → 0.11.0, genesis - // G2b). Kept a literal because `projection` takes `&[&str]` and a - // formatted String would ripple through every call site; - // `the_test_header_tracks_the_implemented_version` below fails loudly if - // the two ever drift. - const HEADER: &str = "(text-projection (0 11 0))"; + // G2b; 0.11.0 → 0.12.0, genesis G3a). Kept a literal because `projection` + // takes `&[&str]` and a formatted String would ripple through every call + // site; `the_test_header_tracks_the_implemented_version` below fails + // loudly if the two ever drift. + const HEADER: &str = "(text-projection (0 12 0))"; const DOCUMENT: &str = "(document #x00000000000000000000000000000001 (schema 0 1))"; /// A minimal but complete valid projection: just the two mandatory lines. diff --git a/crates/epiphany-textproj/src/vectors.rs b/crates/epiphany-textproj/src/vectors.rs index c10b864..0b671ca 100644 --- a/crates/epiphany-textproj/src/vectors.rs +++ b/crates/epiphany-textproj/src/vectors.rs @@ -21,9 +21,13 @@ use epiphany_bundle::{ ProfileConstraints, ProfileDeclaration, ProfileId, ProfileRegistryId, ReductionAlgorithmVersion, SchemaVersion, SemVer, SnapshotId, }; -use epiphany_core::{OperationId, RegionId, ReplicaId, WallClockTime}; +use epiphany_core::{ + AnalysisLayerId, OperationId, PartDefinitionId, RegionId, ReplicaId, StaffGroupId, StaffId, + ViewId, WallClockTime, +}; use epiphany_ops::{ - AuthorId, CausalContext, DeleteRegionOp, HybridLogicalClock, OperationEnvelope, OperationKind, + AuthorId, CausalContext, CreateAnalysisLayerOp, CreatePartDefinitionOp, CreateStaffGroupOp, + CreateViewOp, DeleteRegionOp, HybridLogicalClock, OperationEnvelope, OperationKind, OperationPayload, OperationStamp, SetTuningContextOp, }; @@ -177,6 +181,83 @@ fn tuning_context_envelope(counter: u64, physical_time: i64) -> OperationEnvelop } } +/// The four genesis tranche G3a envelopes (kinds 35–38, +/// `spec/CONTRACT_GENESIS_G3A_ENTITIES.md`). Same rationale as +/// `tuning_context_envelope` above: the typed all-kind round-trip test proves +/// the *production* parses, but only a committed vector proves the emitted +/// document text is stable across implementations. +fn staff_group_envelope(counter: u64, physical_time: i64) -> OperationEnvelope { + let id = OperationId::new(ReplicaId(1), counter); + OperationEnvelope { + id, + author: AuthorId(0xAB), + stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(physical_time), 0), id), + causal_context: CausalContext::new(), + transaction: None, + payload: OperationPayload::Primitive(OperationKind::CreateStaffGroup(CreateStaffGroupOp { + group: epiphany_ops::valuegen::staff_group( + StaffGroupId::new(ReplicaId(1), 1), + vec![StaffId::new(ReplicaId(1), 1)], + ), + })), + } +} + +fn part_definition_envelope(counter: u64, physical_time: i64) -> OperationEnvelope { + let id = OperationId::new(ReplicaId(1), counter); + OperationEnvelope { + id, + author: AuthorId(0xAB), + stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(physical_time), 0), id), + causal_context: CausalContext::new(), + transaction: None, + payload: OperationPayload::Primitive(OperationKind::CreatePartDefinition( + CreatePartDefinitionOp { + part: epiphany_ops::valuegen::part_definition( + PartDefinitionId::new(ReplicaId(1), 1), + vec![StaffId::new(ReplicaId(1), 1)], + ), + }, + )), + } +} + +fn analysis_layer_envelope(counter: u64, physical_time: i64) -> OperationEnvelope { + let id = OperationId::new(ReplicaId(1), counter); + OperationEnvelope { + id, + author: AuthorId(0xAB), + stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(physical_time), 0), id), + causal_context: CausalContext::new(), + transaction: None, + payload: OperationPayload::Primitive(OperationKind::CreateAnalysisLayer( + CreateAnalysisLayerOp { + layer: epiphany_ops::valuegen::analysis_layer(AnalysisLayerId::new( + ReplicaId(1), + 1, + )), + }, + )), + } +} + +fn view_envelope(counter: u64, physical_time: i64) -> OperationEnvelope { + let id = OperationId::new(ReplicaId(1), counter); + OperationEnvelope { + id, + author: AuthorId(0xAB), + stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(physical_time), 0), id), + causal_context: CausalContext::new(), + transaction: None, + payload: OperationPayload::Primitive(OperationKind::CreateView(CreateViewOp { + view: epiphany_ops::valuegen::view( + ViewId::new(ReplicaId(1), 1), + vec![AnalysisLayerId::new(ReplicaId(1), 1)], + ), + })), + } +} + fn profiles(custom: bool) -> Vec { let mut profiles = vec![ProfileDeclaration::full()]; if custom { @@ -291,6 +372,50 @@ fn accept_documents() -> Vec<(&'static str, String)> { envelopes: vec![tuning_context_envelope(7, 700)], }; + // Genesis G3a: kinds 35–38 in the committed corpus. Same manifest-version + // discipline as `tuning_context` above — baseline `V0`, since none of + // these documents declares an edit barrier either. + let staff_group = TextDocument { + document_id: DocumentId([6; 16]), + manifest_schema_version: SchemaVersion::V0, + lineage_id: None, + profiles: profiles(false), + extensions: Vec::new(), + canonical_base: None, + blobs: Vec::new(), + envelopes: vec![staff_group_envelope(8, 800)], + }; + let part_definition = TextDocument { + document_id: DocumentId([7; 16]), + manifest_schema_version: SchemaVersion::V0, + lineage_id: None, + profiles: profiles(false), + extensions: Vec::new(), + canonical_base: None, + blobs: Vec::new(), + envelopes: vec![part_definition_envelope(9, 900)], + }; + let analysis_layer = TextDocument { + document_id: DocumentId([8; 16]), + manifest_schema_version: SchemaVersion::V0, + lineage_id: None, + profiles: profiles(false), + extensions: Vec::new(), + canonical_base: None, + blobs: Vec::new(), + envelopes: vec![analysis_layer_envelope(10, 1000)], + }; + let view = TextDocument { + document_id: DocumentId([9; 16]), + manifest_schema_version: SchemaVersion::V0, + lineage_id: None, + profiles: profiles(false), + extensions: Vec::new(), + canonical_base: None, + blobs: Vec::new(), + envelopes: vec![view_envelope(11, 1100)], + }; + vec![ ("minimal", project_text_document(&minimal)), ("set_tuning_context", project_text_document(&tuning_context)), @@ -303,6 +428,16 @@ fn accept_documents() -> Vec<(&'static str, String)> { project_text_document(&extension_base_multi), ), ("rich_document", project_text_document(&rich)), + ("create_staff_group", project_text_document(&staff_group)), + ( + "create_part_definition", + project_text_document(&part_definition), + ), + ( + "create_analysis_layer", + project_text_document(&analysis_layer), + ), + ("create_view", project_text_document(&view)), ] } @@ -387,16 +522,16 @@ pub fn document_vectors() -> Vec { .collect(); // The rejected version must be one this crate does NOT implement. Genesis - // tranche G2b moved `COMPANION_VERSION` to 0.11.0, which had been this + // tranche G3a moved `COMPANION_VERSION` to 0.12.0, which had been this // vector's "future" version — leaving it would have made the negative // vector assert that the *correct* header is rejected. It now names - // 0.10.0, the immediately superseded companion, which is the better test + // 0.11.0, the immediately superseded companion, which is the better test // anyway: rejecting the version right behind you is exactly the deferred // migrate-on-read posture (`req:textproj:header-version`). let wrong_version = replace_once( minimal, + "(text-projection (0 12 0))", "(text-projection (0 11 0))", - "(text-projection (0 10 0))", ); vectors.push(( SURFACE, @@ -716,7 +851,7 @@ mod tests { #[test] fn the_reference_implementation_agrees_with_every_vector() { match verify(COMMITTED) { - Ok(count) => assert_eq!(count, 14, "the corpus has unexpectedly thinned"), + Ok(count) => assert_eq!(count, 18, "the corpus has unexpectedly thinned"), Err(failures) => panic!( "{} disagreement(s):\n{}", failures.len(), @@ -754,4 +889,61 @@ mod tests { 2 lineages, 2 multi-envelope documents, and one real rejection in each class" ); } + + /// (t11) Genesis tranche G3a: text projection round-trips all four new + /// kinds (`create-staff-group`, `create-part-definition`, + /// `create-analysis-layer`, `create-view`), and the companion version is + /// **0.12.0**, with the negative vector rejecting **0.11.0**. + /// + /// **Mutation:** drop one parse arm (e.g. + /// `OperationKindTag::CreateStaffGroup` from `OperationKind::parse` in + /// `textproj_kind.rs`); must fail. Separately, leave `COMPANION_VERSION` + /// at `(0, 11, 0)`; the negative vector must fail. + #[test] + fn t11_g3a_kinds_round_trip_and_companion_is_0_12_0_rejecting_0_11_0() { + assert_eq!( + crate::COMPANION_VERSION, + (0, 12, 0), + "the companion version must be 0.12.0" + ); + + for name in [ + "create_staff_group", + "create_part_definition", + "create_analysis_layer", + "create_view", + ] { + let text = accept_documents() + .into_iter() + .find(|(n, _)| *n == name) + .unwrap_or_else(|| panic!("accept document is absent: {name}")) + .1; + let document = + parse_document(&text).unwrap_or_else(|e| panic!("{name} must parse: {e}")); + assert_eq!(document.envelopes.len(), 1, "{name} carries one envelope"); + let reprojected = project_text_document(&document); + assert_eq!( + reprojected, text, + "{name}: project(serialize(parse(T))) == T must hold" + ); + } + + // The negative vector must reject exactly the immediately superseded + // companion, 0.11.0. + let rows = parse(COMMITTED).expect("the committed corpus parses"); + let superseded = rows + .iter() + .find(|r| r.name == "superseded_companion_version") + .expect("the superseded_companion_version vector is present"); + assert_eq!(superseded.verdict, "reject"); + let text = String::from_utf8(superseded.text.clone()).expect("utf8"); + assert!( + text.contains("(text-projection (0 11 0))"), + "the negative vector must name the immediately superseded companion 0.11.0, got: {text}" + ); + assert!( + parse_document(&text).is_err(), + "the superseded companion version must be rejected" + ); + } } diff --git a/spec/PLAN_GENESIS_OPS.md b/spec/PLAN_GENESIS_OPS.md index 50328b7..65d8856 100644 --- a/spec/PLAN_GENESIS_OPS.md +++ b/spec/PLAN_GENESIS_OPS.md @@ -6,8 +6,8 @@ reverses Pass-12 K8 and makes every mutable field of `Score` operation-authored. This plan is the execution scope: what the tranche touches, in what order, and which questions must be answered before a dispatch contract can be written. -**Status:** the ladder **G1 → G2a → G-minor → G2b** is **complete**; only **G3** -remains, and it **splits into G3a and G3b** (ratified 2026-07-29, §4). +**Status:** the ladder **G1 → G2a → G-minor → G2b → G3a** is **complete**; +only **G3b** remains (ratified 2026-07-29, §4). * **G1 landed** (3b09595) — `CreateInstrument`, kind/tag 31. * **G2a landed** (7df5ca1 + 55eff00) — `SetCanvasLayoutDefaults` and @@ -22,12 +22,13 @@ remains, and it **splits into G3a and G3b** (ratified 2026-07-29, §4). `epiphany_core::TuningContextSettings`, **not** the full graph type — §5 trap 7's holdout, resolved in the contract as *subset over normalization*. Closed **P13-S13**. -* **G3a** — the four root-level mint families (`CreateStaffGroup`, - `CreatePartDefinition`, `CreateAnalysisLayer`, `CreateView`), kinds/tags - **35–38**, epoch **11**, all schema major **0**. Contracted - (`spec/CONTRACT_GENESIS_G3A_ENTITIES.md`) and **ready to dispatch**; §1.1's +* **G3a landed** (commit pending) — the four root-level mint families + (`CreateStaffGroup`, `CreatePartDefinition`, `CreateAnalysisLayer`, + `CreateView`), kinds/tags **35–38**, epoch **11**, all schema major **0**. + Executed against `spec/CONTRACT_GENESIS_G3A_ENTITIES.md`; §1.1's `StaffGroup`/`Staff` authorship-authority pin was ratified 2026-07-29 as - disposition B and filed as **P13-S16**. + disposition B and filed as **P13-S16**. No `epiphany-bundle` change of any + kind; op-block accept-set stays at 3 where G2b left it. * **G3b** — `CreateMeasure` alone, kind/tag **39**, epoch **12**, carrying graph invariant **20** and a new `PreconditionFailureReason` at discriminant **16**. Scoped, not contracted. diff --git a/spec/PLAN_GMINOR_SCHEMA_MINOR.md b/spec/PLAN_GMINOR_SCHEMA_MINOR.md index 2c48b12..6cc7447 100644 --- a/spec/PLAN_GMINOR_SCHEMA_MINOR.md +++ b/spec/PLAN_GMINOR_SCHEMA_MINOR.md @@ -178,6 +178,7 @@ numbering spaces are unrelated and must not be cross-read. | 8 | Genesis G1 | `OperationKind`/`OperationKindTag` 31 | | 9 | Genesis G2a | `OperationKind`/`OperationKindTag` 32–33 | | 10 | Genesis G2b | `OperationKind`/`OperationKindTag` 34 (`SetTuningContext`) | +| 11 | Genesis G3a | `OperationKind`/`OperationKindTag` 35–38 (`CreateStaffGroup`, `CreatePartDefinition`, `CreateAnalysisLayer`, `CreateView`) | > **Epoch 10 ratified 2026-07-28**, with G2b as the event. This is the **first > exercise of the ladder's own growth path**: G-minor's `introduced_minor()` is @@ -186,6 +187,13 @@ numbering spaces are unrelated and must not be cross-read. > (G2b follows G2a) and prefix-closed. Epoch assignment remains a ratified > schema decision, never an implementer's choice. +> **Epoch 11 ratified 2026-07-29**, with G3a as the event (introducing commit +> pending — this rung has not yet landed a commit; update this citation once +> it does, per the same discipline as the G2a correction above). One epoch for +> the whole rung's four kinds, the G2a precedent (two kinds at epoch 9) rather +> than one epoch per kind. The ladder stays monotonic (G3a follows G2b) and +> prefix-closed. + **The ladder is complete against the audit** — every post-baseline variant in `AUDIT_GMINOR_VOCABULARIES.md` appears exactly once: all ten kind/tag pairs, `OperationPayload` 3, `ReanchorReason` 6, and all six diff --git a/spec/binary_format.pdf b/spec/binary_format.pdf index 1e8fc4c..2869737 100644 Binary files a/spec/binary_format.pdf and b/spec/binary_format.pdf differ diff --git a/spec/binary_format.tex b/spec/binary_format.tex index deefcce..f34be46 100644 --- a/spec/binary_format.tex +++ b/spec/binary_format.tex @@ -240,7 +240,7 @@ {\Large\scshape\color{epiphanyslate}Binary Format}\\[6pt] {\large\itshape\color{epiphanyslate}A companion to the Core Specification}\\[14pt] {\color{epiphanygold}\rule{3in}{0.8pt}}\\[24pt] - {\normalsize\color{epiphanyink}Version 0.13.0 --- The chunk schema minor becomes a derived record (G-minor)}\\[4pt] + {\normalsize\color{epiphanyink}Version 0.14.0 --- The genesis operation tranche completes the root-level entity mints (G3a)}\\[4pt] {\small\color{epiphanyslate}Normative for the byte layouts it defines} \vfill \end{titlepage} @@ -1462,6 +1462,18 @@ and the reader disagree about the field's type. \texttt{ScoreTuningContext} layout (Section~\ref{sec:evolution:major3}); born at v3 unconditionally & \sectionsc{SetTuningContext} \\ + \tablenums{35} & \texttt{CreateStaffGroup} & + $\mathrm{lp}$(\texttt{StaffGroup}) & + \sectionsc{CreateStaffGroup} \\ + \tablenums{36} & \texttt{CreatePartDefinition} & + $\mathrm{lp}$(\texttt{PartDefinition}) & + \sectionsc{CreatePartDefinition} \\ + \tablenums{37} & \texttt{CreateAnalysisLayer} & + $\mathrm{lp}$(\texttt{AnalysisLayer}) & + \sectionsc{CreateAnalysisLayer} \\ + \tablenums{38} & \texttt{CreateView} & + $\mathrm{lp}$(\texttt{ViewDefinition}) & + \sectionsc{CreateView} \\ \bottomrule \end{longtable} \endgroup @@ -1532,7 +1544,9 @@ discriminants and wrong lengths are decode errors. Append-only past \tablenums{28} & \texttt{CreateRepeatStructure} & \tablenums{29} & \texttt{DeleteRepeatStructure} \\ \tablenums{30} & \texttt{TransposeInterval} & \tablenums{31} & \texttt{CreateInstrument} \\ \tablenums{32} & \texttt{SetCanvasLayoutDefaults} & \tablenums{33} & \texttt{SetSpellingPrecedence} \\ - \tablenums{34} & \texttt{SetTuningContext} & & \\ + \tablenums{34} & \texttt{SetTuningContext} & \tablenums{35} & \texttt{CreateStaffGroup} \\ + \tablenums{36} & \texttt{CreatePartDefinition} & \tablenums{37} & \texttt{CreateAnalysisLayer} \\ + \tablenums{38} & \texttt{CreateView} & & \\ \bottomrule \end{longtable} \endgroup @@ -2382,7 +2396,7 @@ The \emph{only} minor-additive mechanism in schema major~0 is \textbf{appending discriminants to open vocabularies}: \begin{itemize} - \item \texttt{OperationKind}: append at ${\geq}\,\tablenums{35}$ + \item \texttt{OperationKind}: append at ${\geq}\,\tablenums{39}$ (Requirement~\ref{req:binfmt:kind-discriminants}; the Phase-3 tranche took \tablenums{24}--\tablenums{27} under this mechanism, the repeat pair \tablenums{28}/\tablenums{29} in the @@ -2390,18 +2404,23 @@ The \emph{only} minor-additive mechanism in schema major~0 is (\texttt{TransposeInterval}), genesis tranche G1 took \tablenums{31} (\texttt{CreateInstrument}), genesis tranche G2a took \tablenums{32}/\tablenums{33} (\texttt{SetCanvasLayoutDefaults}, - \texttt{SetSpellingPrecedence}), and genesis tranche G2b took - \tablenums{34} (\texttt{SetTuningContext}) --- the discriminant space is + \texttt{SetSpellingPrecedence}), genesis tranche G2b took + \tablenums{34} (\texttt{SetTuningContext}), and genesis tranche G3a took + \tablenums{35}--\tablenums{38} (\texttt{CreateStaffGroup}, + \texttt{CreatePartDefinition}, \texttt{CreateAnalysisLayer}, + \texttt{CreateView}) --- the discriminant space is one append-only table across majors, but a kind's \emph{block stamp} follows minimal stamping over its payload: \tablenums{28}'s payload embeds a v2 layout, so its blocks stamp major~2, Section~\ref{sec:evolution:major2}; \tablenums{34}'s payload embeds a v3 layout unconditionally, so its blocks stamp major~3, - Section~\ref{sec:evolution:major3} --- the sole appended kind to do so); - \item \texttt{OperationKindTag}: append at ${\geq}\,\tablenums{35}$ - (Requirement~\ref{req:binfmt:kind-tag}; the same eleven variants as - \texttt{OperationKind}, at the same eleven discriminants, appended by - the same six tranches); + Section~\ref{sec:evolution:major3} --- the sole appended kind to do so; + \tablenums{35}--\tablenums{38} are each major~0, no versioned walk + existing for any of their four carried types); + \item \texttt{OperationKindTag}: append at ${\geq}\,\tablenums{39}$ + (Requirement~\ref{req:binfmt:kind-tag}; the same fifteen variants as + \texttt{OperationKind}, at the same fifteen discriminants, appended by + the same seven tranches); \item \texttt{OperationPayload}: append at ${\geq}\,\tablenums{4}$ (Section~\ref{sec:ops:payload}; \texttt{ResolveEquivocation} = 3, Push~3, is the one and only append this vocabulary has had); @@ -3621,6 +3640,23 @@ only}: implementations need not agree on an error taxonomy. current. No discriminant, wire layout, or accept-set changes: this is a correction to prose and a new writer-side derivation, not a new appended variant. \\ + \today & Operation wire forms & 0.14.0 --- Genesis tranche G3a + (\texttt{spec/CONTRACT\_GENESIS\_G3A\_ENTITIES.md}): appends + \texttt{OperationKind} wire discriminants \tablenums{35}--\tablenums{38} + (\texttt{CreateStaffGroup}, \texttt{CreatePartDefinition}, + \texttt{CreateAnalysisLayer}, \texttt{CreateView}) with their payload + layouts, and the matching \texttt{OperationKindTag} discriminants + \tablenums{35}--\tablenums{38} (name-verbatim, following the two most + recent additions rather than the tag space's older Create$\rightarrow$Insert + convention) --- a schema-\emph{minor} evolution (epoch~11, + \texttt{spec/PLAN\_GMINOR\_SCHEMA\_MINOR.md} \S4) under this document's own + append-only rules; no existing assignment changed. All four carried types + (\texttt{StaffGroup}, \texttt{PartDefinition}, \texttt{AnalysisLayer}, + \texttt{ViewDefinition}) have never been versioned as standalone values, so + no accept-set moves: \texttt{OperationEnvelopeBlock} stays at + \tablenums{3} where genesis tranche G2b left it. Semantics: Operation + Catalog \sectionsc{CreateStaffGroup}, \sectionsc{CreatePartDefinition}, + \sectionsc{CreateAnalysisLayer}, \sectionsc{CreateView}, 0.12.0. \\ \bottomrule \end{longtable} diff --git a/spec/core_spec.pdf b/spec/core_spec.pdf index 1141ea4..23e830a 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 e9ee7f4..bcac894 100644 --- a/spec/core_spec.tex +++ b/spec/core_spec.tex @@ -4232,6 +4232,13 @@ pub struct StaffGroup { pub id: StaffGroupId, pub name: Option, pub kind: StaffGroupKind, + /// A non-authoritative denormalized projection of group membership + /// (genesis tranche G3a, ratified disposition B, filed as P13-S16). + /// \texttt{Staff.group} below is the sole authority: this field + /// \MUSTNOT{} be read to decide whether a staff is in a group, and MAY + /// be stale in both directions --- a member missing here while + /// \texttt{Staff.group} names this group, or a staff listed here while + /// its own \texttt{Staff.group} is absent or names a different group. pub members: Vec, } @@ -5576,7 +5583,12 @@ pub struct Staff { pub default_staff_lines: StaffLineConfiguration, /// Visual grouping: which staff group (if any) this staff - /// belongs to (e.g., piano grand staff, choral group). + /// belongs to (e.g., piano grand staff, choral group). The sole + /// authority for group membership (genesis tranche G3a, ratified + /// disposition B, filed as P13-S16): every consumer \MUST{} read + /// membership from this field, not from \texttt{StaffGroup.members} + /// above, which is a non-authoritative denormalized projection that MAY + /// disagree with this field in either direction. pub group: Option, } @@ -6969,6 +6981,16 @@ pub enum OperationKind { // on the wire and is left untouched by reduction). SetTuningContext(SetTuningContextOp), + // Genesis tranche G3a: the four remaining root-level Score entity + // mints, on the InsertStaff set-union mint pattern. Staff.group is the + // sole authority for group membership (see the Staff/StaffGroup + // declarations below); CreateStaffGroup stores members as carried and + // neither maintains nor trusts it. + CreateStaffGroup(CreateStaffGroupOp), + CreatePartDefinition(CreatePartDefinitionOp), + CreateAnalysisLayer(CreateAnalysisLayerOp), + CreateView(CreateViewOp), + // Layout-semantic operations SetUserSystemBreak(SetUserSystemBreakOp), SetUserPageBreak(SetUserPageBreakOp), @@ -11938,6 +11960,12 @@ pub enum OperationKindTag { SetSpellingPrecedence, // Genesis tranche G2b: name-verbatim, as the two prior additions are. SetTuningContext, + // Genesis tranche G3a: the four remaining root-level entity mints, + // name-verbatim, as the three most recent prior additions are. + CreateStaffGroup, + CreatePartDefinition, + CreateAnalysisLayer, + CreateView, } pub enum BarrierScope { diff --git a/spec/operation_catalog.pdf b/spec/operation_catalog.pdf index 4f8c48e..fa08c66 100644 Binary files a/spec/operation_catalog.pdf and b/spec/operation_catalog.pdf differ diff --git a/spec/operation_catalog.tex b/spec/operation_catalog.tex index 6e11a20..a26e2f9 100644 --- a/spec/operation_catalog.tex +++ b/spec/operation_catalog.tex @@ -231,7 +231,7 @@ {\Large\scshape\color{epiphanyslate}Operation Catalog}\\[6pt] {\large\itshape\color{epiphanyslate}A companion to the Core Specification}\\[14pt] {\color{epiphanygold}\rule{3in}{0.8pt}}\\[24pt] - {\normalsize\color{epiphanyink}Version 0.11.0 --- The genesis operation tranche reaches schema major 3 (G2b)}\\[4pt] + {\normalsize\color{epiphanyink}Version 0.12.0 --- The genesis operation tranche completes the root-level entity mints (G3a)}\\[4pt] {\small\color{epiphanyslate}Normative for the operation kinds it defines} \vfill \end{titlepage} @@ -417,6 +417,65 @@ context (the canonical base embeds no \texttt{Score} field value for any setting, \texttt{metadata} included), on the same reasoning that already applied to \texttt{SetMetadata}. +\medskip + +\noindent\textbf{Version 0.12.0 (genesis tranche G3a, +\texttt{spec/CONTRACT\_GENESIS\_G3A\_ENTITIES.md}).} Adds four operation +kinds --- \texttt{CreateStaffGroup} (Section~\ref{sec:k0:create-staff-group}), +\texttt{CreatePartDefinition} (Section~\ref{sec:k0:create-part-definition}), +\texttt{CreateAnalysisLayer} (Section~\ref{sec:k0:create-analysis-layer}), +\texttt{CreateView} (Section~\ref{sec:k0:create-view}) --- completing the +root-level \texttt{Score} entity mints: every remaining vector a Chapter~5 +top-level structure declares (\texttt{staff\_groups}, \texttt{parts}, +\texttt{analysis\_layers}, \texttt{views}) is now operation-authored. All +four are mint-only (set-union creation, no delete in this catalogue +revision), schema major~0 unconditionally, epoch~11. + +\textbf{The defect this closes, and what it does not close.} Until this +rung, all four object kinds became live \emph{only} through canonical-base +ingest; no operation minted any of them. Consequently \texttt{CreateStaff}'s +\texttt{group} precondition (Section~\ref{sec:k0:create-staff}) was +\textbf{unsatisfiable} under from-empty reduction --- a document built only +from operations could never author a grouped staff. G3a closes exactly this +satisfiability half. It does \textbf{not} make a bidirectionally consistent +staff group authorable: with mints only, and no modify or delete operation +for either side, no authoring order can bring \texttt{Staff.group} and +\texttt{StaffGroup.members} into agreement once they disagree. + +\textbf{Ruled (disposition B, filed as P13-S16): \texttt{Staff.group} is the +sole authority for group membership; \texttt{StaffGroup.members} is a +non-authoritative denormalized projection.} \texttt{CreateStaffGroup} stores +\texttt{members} exactly as carried and neither maintains nor trusts it +thereafter. Both stale forms are permitted outcomes, not defects: a +\textbf{missing} member (\texttt{Staff.group} names a group whose +\texttt{members} omits that staff) and a \textbf{spurious} member +(\texttt{StaffGroup.members} names a staff whose own \texttt{Staff.group} is +absent or names a different group). Every consumer \MUST{} read group +membership from \texttt{Staff.group} only; core specification Chapter~5's +\texttt{Staff}/\texttt{StaffGroup} declarations (\sectionsc{Top-Level Score +Structure}) now state this normatively, and Section~\ref{sec:k0:create-staff} +above states the corresponding authoring-order consequence for +\texttt{CreateStaff}. Disposition A (maintaining \texttt{members} by +reduction, making it a derived field) remains the later +maintenance/enforcement fix, sequenced after G3b. + +\textbf{Referential preconditions, graph-aware only, mirroring +\texttt{CreateStaff} exactly.} \texttt{CreateStaffGroup.members} and +\texttt{CreatePartDefinition.staves} each precondition every named id +resolves to a live \texttt{Staff}; \texttt{CreateView.active\_layers} +preconditions every named id resolves to a live analysis layer; +\texttt{CreateAnalysisLayer} carries no outbound reference and needs no +referential precondition at all, the \texttt{CreateInstrument} shape rather +than the \texttt{CreateStaff} shape. Base-free reduction enforces none of +these, exactly as for \texttt{CreateStaff}: there is no universe to check +against. No new \texttt{PreconditionFailureReason} --- every referential +refusal reuses \texttt{TargetMissing} (discriminant 0). + +No accept-set change: all four carried types have exactly one wire layout +(core specification's \sectionsc{Binary Format Companion} confirms no +versioned walk exists for any of the four), so none gains a +\texttt{schema\_major()} arm beyond the existing catch-all. + % =========================================================================== \chapter{The Catalog Framework} \label{ch:framework} @@ -1137,6 +1196,21 @@ With staves mintable, \texttt{CreateStaffInstance} instance's referenced \texttt{Staff} is live (previously the reference was satisfiable only from the seeded base, so the check was vacuous). +\textbf{Stale-form semantics (genesis tranche G3a, disposition B, filed as +P13-S16).} \texttt{Staff.group} is the \textbf{sole authority} for group +membership; \texttt{StaffGroup.members} +(Section~\ref{sec:k0:create-staff-group}) is a non-authoritative denormalized +projection no consumer may read to decide membership. \texttt{CreateStaff} +is the operation that authors the \textbf{missing-member} stale form: a +\texttt{CreateStaffGroup(g, members: [])} followed by a +\texttt{CreateStaff(s, group: Some(g))} leaves \texttt{s.group == Some(g)} +while \texttt{g.members} omits \texttt{s} --- a \textbf{permitted} outcome, +not a defect, since G3a mints only and no operation maintains +\texttt{StaffGroup.members} by reduction. The mirror-image \textbf{spurious} +form (a staff named in \texttt{members} whose own \texttt{group} is absent or +different) is authored by \texttt{CreateStaffGroup} instead; see +Section~\ref{sec:k0:create-staff-group}'s own stale-form paragraph. + \textbf{Conflict cases.} None at reduction time (set-union; the differing-value re-create is a precondition gate, not a conflict). @@ -1355,6 +1429,162 @@ undo leaves it exactly as it found it. \textbf{Re-anchoring.} Not applicable: the field is a score singleton with no tombstonable anchor. +\section{CreateStaffGroup} +\label{sec:k0:create-staff-group} + +Ratified with the genesis operation tranche's G3a rung (\texttt{spec/ +CONTRACT\_GENESIS\_G3A\_ENTITIES.md}, executing \texttt{spec/RULING\_ +GENESIS\_PERSISTENCE.md}): a global staff grouping (\texttt{Score. +staff\_groups}) joins the operation-authored surfaces, on the +\texttt{CreateStaff} set-union mint pattern (Section~\ref{sec:k0:create-staff}). + +\textbf{Payload schema.} \texttt{CreateStaffGroupOp \{ group: StaffGroup \}} +--- the full staff-group value: identity, optional name, kind (grand staff, +bracket, sub-bracket, choral, or a registered extension kind), and the +carried \texttt{members} list. + +\textbf{Canonical encoding.} The length-framed canonical bytes of +\texttt{group}. \texttt{StaffGroup} has never been versioned, so this +operation is schema major~0 unconditionally. + +\textbf{Reduction rule.} Set-union creation, mirroring \texttt{CreateStaff} +exactly: a create mints the group live if its id is fresh; a repeat create +carrying a byte-identical value reduces idempotently +(\texttt{NoOpReason::AlreadyApplied}); a create whose id is already live with +a differing value is a precondition no-op with \texttt{RecreateContentMismatch}; +a create naming a tombstoned id is a precondition no-op with +\texttt{TargetTombstoned}. Graph-aware reduction additionally preconditions +that every id in \texttt{members} resolves to a live \texttt{Staff} --- +reusing \texttt{TargetMissing} (discriminant 0), no new +\texttt{PreconditionFailureReason} --- and is skipped entirely base-free, +which has no staff universe to check against. + +\textbf{Stale-form semantics (disposition B, filed as P13-S16).} +\texttt{Staff.group} (Section~\ref{sec:k0:create-staff}) is the \textbf{sole +authority} for group membership; \texttt{members} here is a +\textbf{non-authoritative denormalized projection}, stored exactly as carried +and \textbf{neither maintained nor trusted} by any subsequent reduction. This +operation authors the \textbf{spurious-member} stale form: a +\texttt{CreateStaff(s, group: None)} followed by +\texttt{CreateStaffGroup(g, members: [s])} leaves \texttt{g.members == [s]} +while \texttt{s.group} stays \texttt{None} (or names a different group) --- +a \textbf{permitted} outcome, not a defect. No consumer may read +\texttt{members} to decide whether a staff belongs to a group. + +\textbf{Conflict cases.} None at reduction time (set-union; the differing-value +re-create is a precondition gate, not a conflict). + +\textbf{Undo semantics.} Undo of a create tombstones the minted group +(Section~\ref{sec:k0:undo}); \texttt{StrictInverse} conflicts if a live staff +still names it (a stale reference, not a strand --- \texttt{Staff.group} +degrading to point at a tombstoned id is the same shape core Chapter~6's +re-anchoring rule table already treats for other dangling references). + +\textbf{Re-anchoring.} Not applicable (a staff-group mint references no +tombstonable anchor; there is no \texttt{DeleteStaffGroup} in this catalogue +revision, mirroring \texttt{CreateStaff}'s own absent delete). + +\section{CreatePartDefinition} +\label{sec:k0:create-part-definition} + +Ratified with genesis tranche G3a, alongside \texttt{CreateStaffGroup} above: +a part-extraction view definition (\texttt{Score.parts}) joins the +operation-authored surfaces, on the same \texttt{CreateStaff} set-union mint +pattern. + +\textbf{Payload schema.} \texttt{CreatePartDefinitionOp \{ part: +PartDefinition \}} --- identity, name, and the carried \texttt{staves} list. +Parts are projections, not storage (core specification Chapter~5 +\sectionsc{Parts}): only references and overrides. + +\textbf{Canonical encoding.} The length-framed canonical bytes of +\texttt{part}. \texttt{PartDefinition} has never been versioned, so this +operation is schema major~0 unconditionally. + +\textbf{Reduction rule.} Set-union creation, mirroring \texttt{CreateStaff} +exactly (mint/idempotence/mismatch/tombstone as above). Graph-aware reduction +additionally preconditions that every id in \texttt{staves} resolves to a +live \texttt{Staff} (\texttt{TargetMissing}, no new failure reason), skipped +base-free. + +\textbf{Conflict cases.} None at reduction time (set-union; the differing-value +re-create is a precondition gate, not a conflict). + +\textbf{Undo semantics.} Undo of a create tombstones the minted part +(Section~\ref{sec:k0:undo}); \texttt{StrictInverse} conflicts if concurrently +mutated. + +\textbf{Re-anchoring.} Not applicable (a part mint references no tombstonable +anchor; there is no \texttt{DeletePartDefinition} in this catalogue +revision). + +\section{CreateAnalysisLayer} +\label{sec:k0:create-analysis-layer} + +Ratified with genesis tranche G3a: a first-class analysis layer +(\texttt{Score.analysis\_layers}) joins the operation-authored surfaces, on +the \texttt{CreateInstrument} mint pattern +(Section~\ref{sec:k0:create-instrument}) --- not \texttt{CreateStaff}'s, +since \texttt{AnalysisLayer} holds no outbound entity reference. + +\textbf{Payload schema.} \texttt{CreateAnalysisLayerOp \{ layer: +AnalysisLayer \}} --- identity and name only (core specification Chapter~5 +\sectionsc{Analysis Layers and Views}). + +\textbf{Canonical encoding.} The length-framed canonical bytes of +\texttt{layer}. \texttt{AnalysisLayer} has never been versioned, so this +operation is schema major~0 unconditionally. + +\textbf{Reduction rule.} Set-union creation, mirroring \texttt{CreateInstrument} +exactly: mint/idempotence/mismatch/tombstone as above, with \textbf{no} +graph-aware reference-resolution block --- there is nothing for one to check. + +\textbf{Conflict cases.} None at reduction time (set-union; the differing-value +re-create is a precondition gate, not a conflict). + +\textbf{Undo semantics.} Undo of a create tombstones the minted layer +(Section~\ref{sec:k0:undo}); \texttt{StrictInverse} conflicts if a live view +still names it in \texttt{active\_layers} (Section~\ref{sec:k0:create-view}). + +\textbf{Re-anchoring.} Not applicable (an analysis-layer mint references no +tombstonable anchor; there is no \texttt{DeleteAnalysisLayer} in this +catalogue revision). + +\section{CreateView} +\label{sec:k0:create-view} + +Ratified with genesis tranche G3a, the fourth and final root-level mint of +this rung: a view recipe (\texttt{Score.views}) joins the operation-authored +surfaces, on the \texttt{CreateStaff} set-union mint pattern. + +\textbf{Payload schema.} \texttt{CreateViewOp \{ view: ViewDefinition \}} --- +identity, name, and the carried \texttt{active\_layers} list. The +view-\emph{kind} detail (core specification Chapter~7) is out of scope for +this baseline. + +\textbf{Canonical encoding.} The length-framed canonical bytes of +\texttt{view}. \texttt{ViewDefinition} has never been versioned, so this +operation is schema major~0 unconditionally. + +\textbf{Reduction rule.} Set-union creation, mirroring \texttt{CreateStaff} +exactly (mint/idempotence/mismatch/tombstone as above). Graph-aware reduction +additionally preconditions that every id in \texttt{active\_layers} resolves +to a live \texttt{AnalysisLayer} (\texttt{TargetMissing}, no new failure +reason), skipped base-free. \textbf{This packet is self-contained}: a +\texttt{CreateView}'s precondition target can be minted by a +\texttt{CreateAnalysisLayer} earlier in the same operation set, so the +ordering is testable end-to-end without a canonical base. + +\textbf{Conflict cases.} None at reduction time (set-union; the differing-value +re-create is a precondition gate, not a conflict). + +\textbf{Undo semantics.} Undo of a create tombstones the minted view +(Section~\ref{sec:k0:undo}); \texttt{StrictInverse} conflicts if concurrently +mutated. + +\textbf{Re-anchoring.} Not applicable (a view mint references no tombstonable +anchor; there is no \texttt{DeleteView} in this catalogue revision). + \section{Repeat Structures} \label{sec:k0:repeat-structures} diff --git a/spec/text_projection.pdf b/spec/text_projection.pdf index 26c6949..56b920a 100644 Binary files a/spec/text_projection.pdf and b/spec/text_projection.pdf differ diff --git a/spec/text_projection.tex b/spec/text_projection.tex index ac6dab0..1cd58a0 100644 --- a/spec/text_projection.tex +++ b/spec/text_projection.tex @@ -234,7 +234,7 @@ {\Large\scshape\color{epiphanyslate}Text Projection}\\[6pt] {\large\itshape\color{epiphanyslate}A companion to the Core Specification}\\[14pt] {\color{epiphanygold}\rule{3in}{0.8pt}}\\[24pt] - {\normalsize\color{epiphanyink}Version 0.11.0 --- The genesis tuning-context setter reaches the grammar}\\[4pt] + {\normalsize\color{epiphanyink}Version 0.12.0 --- The four remaining genesis root-level entity mints reach the grammar}\\[4pt] {\small\color{epiphanyslate}Normative for the text form it defines} \vfill \end{titlepage} @@ -483,7 +483,7 @@ A projection is, in order: \begin{requirement} \label{req:textproj:header-version} A parser implementing this companion \MUST{} accept exactly one header - version: \texttt{(0 11 0)}, the version of the companion it implements. It + version: \texttt{(0 12 0)}, the version of the companion it implements. It \MUST{} reject any other version at line one. Multi-version acceptance and text migrate-on-read are deferred in the same @@ -534,7 +534,7 @@ projection introduces no ordering of its own. A parser \MUST{} reject every \texttt{(blob ...)} line whose blob is unreferenced by canonical state (Requirement~\ref{req:textproj:canonical-blobs}). At companion - version~0.11.0, neither a canonical operation nor canonical reduced state can + version~0.12.0, neither a canonical operation nor canonical reduced state can carry a \texttt{BlobId}; canonical state therefore cannot reference a blob, and a parser \MUST{} reject every \texttt{(blob ...)} line. \end{requirement} @@ -1062,6 +1062,10 @@ kind ::= "(insert-event " bytes " " value ")" | "(set-canvas-layout-defaults " value ")" | "(set-spelling-precedence " value ")" | "(set-tuning-context " value ")" + | "(create-staff-group " value ")" + | "(create-part-definition " value ")" + | "(create-analysis-layer " value ")" + | "(create-view " value ")" tuplet-comp ::= "not-in-tuplet" | "(replace-with-rest " value ")" | "(rewrite-tuplets (" bytes* "))" @@ -1138,7 +1142,7 @@ A document of one operation --- a transposition of two pitches up a perfect fifth, over a compacted base --- projects to five lines: \begin{lstlisting} -(text-projection (0 11 0)) +(text-projection (0 12 0)) (document #x05050505050505050505050505050505 (schema 0 1)) (profile full (0 1 0) (constraints 67108864 (retention 1 () true))) (canonical-base #x1f8b0000000000000000000000000000 #x 1 full (schema 0 1) #x0000) @@ -1384,6 +1388,21 @@ absorb it, exactly as the binary decoder does. version while extending the grammar would leave two mutually incompatible grammars both claiming \texttt{(0 10 0)}. Cached projections at \texttt{(0 10 0)} do not migrate; a stale \texttt{TextProjection} chunk is + regenerated, not converted. \\ + \today & Chapter 5 & 0.12.0 --- The four remaining genesis root-level + entity mints reach the grammar (genesis tranche G3a, + \texttt{spec/CONTRACT\_GENESIS\_G3A\_ENTITIES.md}). The \texttt{kind} + production gains \texttt{"(create-staff-group " value ")"}, + \texttt{"(create-part-definition " value ")"}, + \texttt{"(create-analysis-layer " value ")"}, and + \texttt{"(create-view " value ")"} (\texttt{req:textproj:operation-vocabulary}), + the fourth appended kind event since the header was gated to a single + version at 0.7.0. + + Same forcing reason as every prior grammar-extending bump: holding the + version while extending the grammar would leave two mutually incompatible + grammars both claiming \texttt{(0 11 0)}. Cached projections at + \texttt{(0 11 0)} do not migrate; a stale \texttt{TextProjection} chunk is regenerated, not converted. \\ \bottomrule \end{longtable} diff --git a/spec/vectors/decode_vectors.txt b/spec/vectors/decode_vectors.txt index 14070a0..00c9add 100644 --- a/spec/vectors/decode_vectors.txt +++ b/spec/vectors/decode_vectors.txt @@ -72,8 +72,12 @@ ops.operation_kind_tag accept - tag_31 1f ops.operation_kind_tag accept - tag_32 20 ops.operation_kind_tag accept - tag_33 21 ops.operation_kind_tag accept - tag_34 22 +ops.operation_kind_tag accept - tag_35 23 +ops.operation_kind_tag accept - tag_36 24 +ops.operation_kind_tag accept - tag_37 25 +ops.operation_kind_tag accept - tag_38 26 ops.operation_kind_tag accept - registered 1000000000000000000123456789abcdef -ops.operation_kind_tag reject unknown-discriminant tag_35_one_past_the_vocabulary 23 +ops.operation_kind_tag reject unknown-discriminant tag_39_one_past_the_vocabulary 27 ops.operation_kind_tag reject unknown-discriminant tag_200 c8 ops.operation_kind_tag reject truncated tag_empty - ops.operation_kind_tag reject trailing-bytes insert_event_trailing 0000 @@ -88,6 +92,14 @@ ops.operation_envelope accept - set_spelling_precedence 000000000000000100000000 ops.operation_envelope reject trailing-bytes set_spelling_precedence_trailing 00000000000000010000000000000003000000000000000000000000000000000100000000000000010000000000000000000001000000000000000300000000000000000000210900000005000000040302010000 ops.operation_envelope accept - set_tuning_context 00000000000000010000000000000004000000000000000000000000000000000100000000000000010000000000000000000001000000000000000400000000000000000000223000000006000000636d6e2d3132060000007465742d313200050004080000000000000000907b40010028000100280000000000 ops.operation_envelope reject trailing-bytes set_tuning_context_trailing 00000000000000010000000000000004000000000000000000000000000000000100000000000000010000000000000000000001000000000000000400000000000000000000223000000006000000636d6e2d3132060000007465742d313200050004080000000000000000907b4001002800010028000000000000 +ops.operation_envelope accept - create_staff_group 00000000000000010000000000000005000000000000000000000000000000000100000000000000010000000000000000000001000000000000000500000000000000000000233f0000001000000000000000000000010000000000000001010d00000073746166662d67726f75702d3100010000001000000000000000000000010000000000000001 +ops.operation_envelope reject trailing-bytes create_staff_group_trailing 00000000000000010000000000000005000000000000000000000000000000000100000000000000010000000000000000000001000000000000000500000000000000000000233f0000001000000000000000000000010000000000000001010d00000073746166662d67726f75702d310001000000100000000000000000000001000000000000000100 +ops.operation_envelope accept - create_part_definition 000000000000000100000000000000060000000000000000000000000000000001000000000000000100000000000000000000010000000000000006000000000000000000002436000000100000000000000000000001000000000000000106000000706172742d31010000001000000000000000000000010000000000000001 +ops.operation_envelope reject trailing-bytes create_part_definition_trailing 000000000000000100000000000000060000000000000000000000000000000001000000000000000100000000000000000000010000000000000006000000000000000000002436000000100000000000000000000001000000000000000106000000706172742d3101000000100000000000000000000001000000000000000100 +ops.operation_envelope accept - create_analysis_layer 00000000000000010000000000000007000000000000000000000000000000000100000000000000010000000000000000000001000000000000000700000000000000000000251f0000001000000000000000000000010000000000000001070000006c617965722d31 +ops.operation_envelope reject trailing-bytes create_analysis_layer_trailing 00000000000000010000000000000007000000000000000000000000000000000100000000000000010000000000000000000001000000000000000700000000000000000000251f0000001000000000000000000000010000000000000001070000006c617965722d3100 +ops.operation_envelope accept - create_view 000000000000000100000000000000080000000000000000000000000000000001000000000000000100000000000000000000010000000000000008000000000000000000002636000000100000000000000000000001000000000000000106000000766965772d31010000001000000000000000000000010000000000000001 +ops.operation_envelope reject trailing-bytes create_view_trailing 000000000000000100000000000000080000000000000000000000000000000001000000000000000100000000000000000000010000000000000008000000000000000000002636000000100000000000000000000001000000000000000106000000766965772d3101000000100000000000000000000001000000000000000100 # bundle.manifest bundle.manifest accept - empty_manifest 6f9e7d11689ab113c4a1f05faf60fe60050505050505050505050505050505050000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000001000000000000000000000400000000010000000001000000000000 diff --git a/spec/vectors/textproj_document_vectors.txt b/spec/vectors/textproj_document_vectors.txt index 6fe91a7..6eddde0 100644 --- a/spec/vectors/textproj_document_vectors.txt +++ b/spec/vectors/textproj_document_vectors.txt @@ -22,17 +22,21 @@ # document bytes are normative. `` is lowercase with no separators. # textproj.document -textproj.document accept - minimal 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a -textproj.document accept - set_tuning_context 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830353035303530353035303530353035303530353035303530353035303530352028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303037202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203730302030202378303030303030303030303030303030313030303030303030303030303030303729202863617573616c2028292028292920282920287072696d697469766520287365742d74756e696e672d636f6e74657874202874756e696e672d636f6e746578742d73657474696e67732022636d6e2d31322220227465742d31322220287265666572656e63652d70697463682028636d6e2061203020342920237830303030303030303030663037623430292028736d75666c2d76657273696f6e2d726571756972656d656e742028736d75666c2d76657273696f6e2031203430292028736d75666c2d76657273696f6e20312034302929202829292929290a -textproj.document accept - lineage_custom_profile 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a -textproj.document accept - extension_base_two_envelopes 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a -textproj.document accept - rich_document 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a -textproj.document reject wrong-header-version superseded_companion_version 28746578742d70726f6a656374696f6e202830203130203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a -textproj.document reject blob-line unreferenced_blob 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28626c6f6220226170706c69636174696f6e2f6f637465742d73747265616d222028292023783031290a -textproj.document reject out-of-order-sections canonical_base_before_extension 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a -textproj.document reject repeated-singular-section lineage_repeated 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a -textproj.document reject operation-envelope-order envelopes_reversed 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a -textproj.document reject profile-declaration-order profiles_reversed 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a -textproj.document reject extension-declaration-order extensions_reversed 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a -textproj.document reject extension-chunk-order extension_chunks_reversed 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a -textproj.document reject missing-trailing-lf final_lf_missing 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e20312028292074727565292929 +textproj.document accept - minimal 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a +textproj.document accept - set_tuning_context 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830353035303530353035303530353035303530353035303530353035303530352028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303037202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203730302030202378303030303030303030303030303030313030303030303030303030303030303729202863617573616c2028292028292920282920287072696d697469766520287365742d74756e696e672d636f6e74657874202874756e696e672d636f6e746578742d73657474696e67732022636d6e2d31322220227465742d31322220287265666572656e63652d70697463682028636d6e2061203020342920237830303030303030303030663037623430292028736d75666c2d76657273696f6e2d726571756972656d656e742028736d75666c2d76657273696f6e2031203430292028736d75666c2d76657273696f6e20312034302929202829292929290a +textproj.document accept - lineage_custom_profile 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a +textproj.document accept - extension_base_two_envelopes 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a +textproj.document accept - rich_document 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a +textproj.document accept - create_staff_group 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830363036303630363036303630363036303630363036303630363036303630362028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303038202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203830302030202378303030303030303030303030303030313030303030303030303030303030303829202863617573616c2028292028292920282920287072696d697469766520286372656174652d73746166662d67726f7570202873746166662d67726f757020237830303030303030303030303030303031303030303030303030303030303030312028736f6d65202273746166662d67726f75702d312229206772616e642d737461666620282378303030303030303030303030303030313030303030303030303030303030303129292929290a +textproj.document accept - create_part_definition 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830373037303730373037303730373037303730373037303730373037303730372028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303039202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203930302030202378303030303030303030303030303030313030303030303030303030303030303929202863617573616c2028292028292920282920287072696d697469766520286372656174652d706172742d646566696e6974696f6e2028706172742d646566696e6974696f6e20237830303030303030303030303030303031303030303030303030303030303030312022706172742d312220282378303030303030303030303030303030313030303030303030303030303030303129292929290a +textproj.document accept - create_analysis_layer 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830383038303830383038303830383038303830383038303830383038303830382028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303061202378303030303030303030303030303030303030303030303030303030303030616220287374616d7020313030302030202378303030303030303030303030303030313030303030303030303030303030306129202863617573616c2028292028292920282920287072696d697469766520286372656174652d616e616c797369732d6c617965722028616e616c797369732d6c61796572202378303030303030303030303030303030313030303030303030303030303030303120226c617965722d3122292929290a +textproj.document accept - create_view 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830393039303930393039303930393039303930393039303930393039303930392028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303062202378303030303030303030303030303030303030303030303030303030303030616220287374616d7020313130302030202378303030303030303030303030303030313030303030303030303030303030306229202863617573616c2028292028292920282920287072696d697469766520286372656174652d766965772028766965772d646566696e6974696f6e20237830303030303030303030303030303031303030303030303030303030303030312022766965772d312220282378303030303030303030303030303030313030303030303030303030303030303129292929290a +textproj.document reject wrong-header-version superseded_companion_version 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a +textproj.document reject blob-line unreferenced_blob 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28626c6f6220226170706c69636174696f6e2f6f637465742d73747265616d222028292023783031290a +textproj.document reject out-of-order-sections canonical_base_before_extension 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a +textproj.document reject repeated-singular-section lineage_repeated 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a +textproj.document reject operation-envelope-order envelopes_reversed 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a +textproj.document reject profile-declaration-order profiles_reversed 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a +textproj.document reject extension-declaration-order extensions_reversed 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a +textproj.document reject extension-chunk-order extension_chunks_reversed 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a +textproj.document reject missing-trailing-lf final_lf_missing 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e20312028292074727565292929