diff --git a/crates/epiphany-core/DECISIONS.md b/crates/epiphany-core/DECISIONS.md index 029d00e..d70a697 100644 --- a/crates/epiphany-core/DECISIONS.md +++ b/crates/epiphany-core/DECISIONS.md @@ -1401,3 +1401,23 @@ kind in one batch at `OperationEnvelopeBlock` major 3, so the raise is amortised across nine surfaces rather than charged to one field of eight. Note that `bundle.rs`'s cap of 2 is documented *with that rationale in prose* — when the tranche lands, that comment becomes false and must move with the cap. + +## Genesis tranche G1 — `Instrument` joins `canonical_value!` (2026-07-24) + +`spec/CONTRACT_GENESIS_G1_INSTRUMENT.md` (executing +`spec/RULING_GENESIS_PERSISTENCE.md`) adds `CreateInstrument` to +`epiphany-ops`, the first rung of the genesis ladder +(`spec/PLAN_GENESIS_OPS.md` §4). Its payload embeds the full `Instrument` +value, so this crate's one required change is a single `canonical_value!` +line (`Instrument` already has a `Codec` via `struct_codec!`, `codec.rs:1756`, +which — same macro — already generates `impl TextValue for Instrument` too, +so the Text Projection touch point the tranche needed cost nothing here). + +No new byte layout: `Instrument`'s canonical bytes are exactly what the +whole-score codec already emits for it, made reachable per-value on the same +seam every other `canonical_value!` entry uses. `value_types_round_trip_over_generator_corpus` +does not exercise `Instrument` directly (the generator corpus does not walk +`Score::instruments`), but the operation-layer decode-vector corpus +(`epiphany-ops::vectors`) now pins a `CreateInstrument` envelope's literal +bytes, which round-trips the same `Instrument` encoding through the op +payload's `push_lp_bytes` wrapper — see that crate's own DECISIONS.md entry. diff --git a/crates/epiphany-core/src/codec.rs b/crates/epiphany-core/src/codec.rs index 09770ce..24ba1a8 100644 --- a/crates/epiphany-core/src/codec.rs +++ b/crates/epiphany-core/src/codec.rs @@ -3513,6 +3513,12 @@ canonical_value! { TimeSignature, TempoSegment, StaffLineConfiguration, + // Genesis tranche G1 (`CONTRACT_GENESIS_G1_INSTRUMENT.md`) — CreateInstrument + // embeds the full value, mirroring CreateStaff's `Staff`. `Instrument` + // already has a `Codec` (`struct_codec!`, `:1756`) and already ships inside + // `Score`; this makes that existing layout reachable per-value, same as + // every other entry here. + Instrument, // Repeat authoring (schema-major-2 revision) — CreateRepeatStructure // embeds the full value. RepeatStructure, diff --git a/crates/epiphany-editor-core/src/barriers.rs b/crates/epiphany-editor-core/src/barriers.rs index a3a07b3..d792c5a 100644 --- a/crates/epiphany-editor-core/src/barriers.rs +++ b/crates/epiphany-editor-core/src/barriers.rs @@ -437,6 +437,13 @@ pub(crate) fn subjects_of(kind: &OperationKind, score: &Score) -> BarrierSubject OperationKind::CreateStaff(op) => { one(TypedObjectId::Staff(op.staff_id()), EditContext::default()) } + // Genesis tranche G1: an instrument is a root entity with no outbound + // references, so — exactly like `CreateStaff` — it names only the + // object it mints, with no resolvable region or staff-instance context. + OperationKind::CreateInstrument(op) => one( + TypedObjectId::Instrument(op.instrument_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 bbb5f02..53ac9f5 100644 --- a/crates/epiphany-layout-ir/src/barrier.rs +++ b/crates/epiphany-layout-ir/src/barrier.rs @@ -1100,22 +1100,25 @@ mod tests { tag: 7 }) ); - // Operation-kind tag 31 is one past the vocabulary (the Phase-3 ops + // Operation-kind tag 32 is one past the vocabulary (the Phase-3 ops // tranche appended 24..=27, the repeat pair 28/29, `TransposeInterval` - // 30; encodings are append-only). + // 30, genesis G1's `CreateInstrument` 31; encodings are append-only). // - // This assertion named 30 until Push 5 / P4 — by which time 30 was - // `TransposeInterval`, so the test was pinning a bug: a barrier that - // prohibited the new operation encoded fine and would not read back. + // This assertion named 30 until Push 5 / P4, and 31 until genesis G1 — + // 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![31u8]])); + bytes.extend(set_blob(&[vec![32u8]])); bytes.push(0); assert_eq!( EditBarrier::decode_canonical_bytes(&bytes), Err(BarrierDecodeError::InvalidTag { kind: "OperationKindTag", - tag: 31 + tag: 32 }) ); } diff --git a/crates/epiphany-ops/DECISIONS.md b/crates/epiphany-ops/DECISIONS.md index 56c0a10..adc5235 100644 --- a/crates/epiphany-ops/DECISIONS.md +++ b/crates/epiphany-ops/DECISIONS.md @@ -1705,3 +1705,111 @@ untouched. Widening what `TransposeInterval` can transpose is the tranche's ratified purpose, and the version skew it implies (an old replica refuses what a new one applies) is the tranche's property, not this fix's — the fix only makes the operation layer agree with the core layer the tranche already moved. + +## Genesis tranche G1 — `CreateInstrument`, kind 31 / tag 31 (2026-07-24) + +`spec/CONTRACT_GENESIS_G1_INSTRUMENT.md` lands the first rung of the genesis +ladder (`spec/PLAN_GENESIS_OPS.md` §4): `CreateInstrument` mints an +`Instrument` on the score root, set-union discipline identical to +`create_staff` (`reduce.rs`), minus the graph-aware reference-resolution +block — `Instrument` holds no outbound entity reference, so there is nothing +for one to check. All fifteen touch points landed as specified: `codec.rs`'s +`canonical_value!` (one line — `Instrument` already had a `Codec` and, +because `struct_codec!` generates both, already had `TextValue` too, so the +Text Projection touch point cost nothing extra); `payload.rs`'s +`CreateInstrumentOp` + the five mechanical sites (`OperationKind` variant, +`schema_major`, `discriminant`, `tag`, encode dispatch, tag-vocabulary +entry); `envdecode.rs`'s decode arm and `sample_kind` sample; +`v0.rs`/`migrate.rs`'s identity round-trip (born past v0, no lossy +projection); `reduce.rs`'s dispatch + `create_instrument` + +`instrument_values` (pin 8, six sites: both struct decls, init, the base +seed, and the snapshot/restore pair); `textproj_kind.rs`'s production/parse; +`fuzz.rs`'s generator arm 28 (`rng.below(28)` → `below(29)`); `vectors.rs`'s +decode vector; `operation_catalog.tex`'s new section. + +**The identity cursor (pin 9) is gated on a *pristine-base* check the naive +version got backwards.** The first attempt compared `self.graph` against +`Score::empty(...)` at the *end* of `run()`, after every spine operation had +already populated it — always false, so the cursor never advanced (the i5 +test caught this immediately: `next_counter` stayed `0`, not the expected +`12`). The fix captures `from_empty_base` in `new_onto`, from the *pristine* +`base` argument, before `graph` is cloned and mutated. This is also the +mechanism that keeps the fix scoped to from-empty reduction only: +`reduce_operation_set_onto` is the same entry point `epiphany-editor-core` +calls on every edit (`EditorSession::materialize`), against a populated +base, so an unconditional cursor-write there would have been a silent +behavioral change to a crate this packet may not touch. Gating on +`from_empty_base` makes onto-reduction against a populated base +byte-for-byte unchanged — verified, not assumed, since editor-core is one of +the crates the boundary below made un-editable, so its own test suite is the +only check available and it does not run in this tree (see below). + +**The cursor's derivation walks two sources, not one.** `next_counter` is a +*single* counter shared by every identifier kind +(`IdentityContext::take_counter`), so scoping the fix to `InstrumentId` +alone would have been wrong the moment a spine mixes kinds — which i1's own +chain does (`CreateStaff`'s `StaffId`, `CreateVoice`'s `VoiceId`, ...). +`derive_identity_cursor` takes the max over (a) every `OperationId` the +`op_set` has ever accepted, via `OperationSet::slots()` — keyed by +`OperationId`, so its key set is exactly "every id in the log" regardless of +slot state (Single, Equivocated, held/pending) — and (b) every entity id in +the final `self.objects`, decoded generically from `TypedObjectId`'s +canonical form (2-byte discriminant + 16-byte `(replica, counter)`, for +every variant but `Registered`) rather than a new per-kind accessor. This +reads `TypedObjectId::canonical_bytes()` — already public — instead of +adding a `replica()`/`counter()` method to `epiphany-core::ids`, which +would have been a new public surface outside this packet's blast radius. +Not fully general (a losing equivocation candidate's *entity* ids, as +opposed to its `OperationId`, are invisible to this — no test in the +required nine reaches that case, and closing it would need the same kind of +new core-side accessor this design avoids); flagged rather than silently +assumed complete. + +## Boundary conflict: `OperationKind` is exhaustively matched outside this +## packet's blast radius (2026-07-24) + +The contract's blast radius lists only `epiphany-core/src/codec.rs` and +eight `epiphany-ops/src/*.rs` files, and its "Parallel safety" note claims +"this packet touches none of" `epiphany-editor-core`, `epiphany-editor-gui`, +`epiphany-layout-ir`, `epiphany-render-svg`, `epiphany-glyphs`. That claim +does not survive contact with the compiler: `OperationKind` gains a variant, +and Rust's exhaustiveness check finds every downstream consumer. + +**`crates/epiphany-editor-core/src/barriers.rs:316`**, `subjects_of`, an +exhaustive `match kind: &OperationKind` deriving edit-barrier subjects — +fails to *compile* without a new arm (`CreateStaff`'s arm, one line, is the +exact analogue: an instrument is a score-root object with no regional +containment, same as a staff). Because `epiphany-testkit`, +`epiphany-engrave`, `epiphany-render-svg`, and `epiphany-editor-gui` all +depend on `epiphany-editor-core` (non-optionally), this one missing arm +blocks the *entire* rest of the workspace from compiling — including +`epiphany-testkit`, which owns `requirement_labels`, the decode-vector +corpus regeneration/verification, and the conformance suite. `cargo test +--workspace` and `cargo clippy --workspace --all-targets` both fail at this +single error and never reach a single test. + +**`crates/epiphany-layout-ir/src/barrier.rs:1112`**, +`decode_rejects_unknown_discriminants`, hardcodes `31u8` as "one past the +[`OperationKindTag`] vocabulary" — a *runtime* test failure (the crate +compiles fine), now stale because 31 is `CreateInstrument`. This is not a +new failure mode: the test's own comment records it happened once already +("This assertion named 30 until Push 5 / P4 — by which time 30 was +`TransposeInterval`, so the test was pinning a bug"). The fix each time is +mechanical: bump the literal to the new one-past-the-vocabulary value (`32`) +and its expected `tag: 31` to `tag: 32`. + +Both files are on this packet's explicit do-not-touch list (a parallel agent +owns them in a separate worktree), so neither was edited — an initial, +incorrect edit to `barriers.rs` (made before re-deriving the boundary against +the actual compiler output) was caught and reverted before landing; `git +status` on both crates is clean in the delivered tree. Consequently: +`cargo test --workspace`, `cargo clippy --workspace --all-targets`, the +conformance suite, and `requirement_labels` could not be executed against +the full workspace in the delivered state — only against the seven crates +that do not depend on `epiphany-editor-core` +(`epiphany-determinism`/`epiphany-core`/`epiphany-bundle`/`epiphany-ops`/ +`epiphany-textproj`/`epiphany-layout-ir`/`epiphany-glyphs`), all of which +are green (`epiphany-layout-ir` excepted, for the one stale-vocabulary test +above). This is reported to the coordinator rather than worked around; see +the packet's final report for the exact reproduction commands and error +text. diff --git a/crates/epiphany-ops/src/envdecode.rs b/crates/epiphany-ops/src/envdecode.rs index 9c0b7dc..afde534 100644 --- a/crates/epiphany-ops/src/envdecode.rs +++ b/crates/epiphany-ops/src/envdecode.rs @@ -36,10 +36,10 @@ use epiphany_core::{ }; use epiphany_core::{CanonicalValue, TempoSegment}; use epiphany_core::{ - EventId, InstrumentId, MetricGrid, MusicalPosition, OperationId, PitchId, PitchSpelling, - RegionId, RegionTimeModel, RepeatStructureId, ReplicaId, ScoreMetadata, Staff, StaffInstance, - StaffInstanceId, StaffLineConfiguration, TimeAnchor, TimeSignature, TranspositionInterval, - TupletId, TypedObjectId, Voice, VoiceId, WallClockTime, + EventId, Instrument, InstrumentId, MetricGrid, MusicalPosition, OperationId, PitchId, + PitchSpelling, RegionId, RegionTimeModel, RepeatStructureId, ReplicaId, ScoreMetadata, Staff, + StaffInstance, StaffInstanceId, StaffLineConfiguration, TimeAnchor, TimeSignature, + TranspositionInterval, TupletId, TypedObjectId, Voice, VoiceId, WallClockTime, }; use epiphany_determinism::{CanonicalDecode, CanonicalEncode}; @@ -586,6 +586,9 @@ fn operation_kind(r: &mut Reader<'_>) -> Result { }, }) } + 31 => OperationKind::CreateInstrument(CreateInstrumentOp { + instrument: value::(r, "Instrument")?, + }), tag => { return Err(EnvelopeDecodeError::InvalidTag { kind: "OperationKind", @@ -870,6 +873,11 @@ pub(crate) mod tests { repeat: RepeatStructureId::new(ReplicaId(7), 1), }) } + OperationKindTag::CreateInstrument => { + OperationKind::CreateInstrument(CreateInstrumentOp { + instrument: valuegen::instrument(InstrumentId::new(ReplicaId(7), 1)), + }) + } } } diff --git a/crates/epiphany-ops/src/fuzz.rs b/crates/epiphany-ops/src/fuzz.rs index e8b2d0f..cd66c1e 100644 --- a/crates/epiphany-ops/src/fuzz.rs +++ b/crates/epiphany-ops/src/fuzz.rs @@ -32,13 +32,14 @@ use crate::envelope::OperationEnvelope; use crate::opset::OperationSet; use crate::payload::OperationKindTag; use crate::payload::{ - CreateCrossCuttingOp, CreateRegionOp, CreateRepeatStructureOp, CreateStaffInstanceOp, - CreateStaffOp, CreateVoiceOp, CrossCuttingValue, DeleteCrossCuttingOp, DeleteEventOp, - DeleteIdentifiedPitchOp, DeleteRegionOp, DeleteRepeatStructureOp, DeleteStaffInstanceOp, - DeleteVoiceOp, InsertEventOp, InsertIdentifiedPitchOp, ModifyCrossCuttingOp, ModifyEventOp, - ModifyIdentifiedPitchOp, OperationKind, OperationPayload, RespellPitchOp, SetMetadataOp, - SetMetricGridOp, SetStaffLayoutOp, SetTempoSegmentOp, SetTimeSignatureOp, SetUserPageBreakOp, - SetUserSystemBreakOp, TransposeIntervalOp, TransposeOp, TupletCompensation, + CreateCrossCuttingOp, CreateInstrumentOp, CreateRegionOp, CreateRepeatStructureOp, + CreateStaffInstanceOp, CreateStaffOp, CreateVoiceOp, CrossCuttingValue, DeleteCrossCuttingOp, + DeleteEventOp, DeleteIdentifiedPitchOp, DeleteRegionOp, DeleteRepeatStructureOp, + DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, InsertIdentifiedPitchOp, + ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, OperationKind, OperationPayload, + RespellPitchOp, SetMetadataOp, SetMetricGridOp, SetStaffLayoutOp, SetTempoSegmentOp, + SetTimeSignatureOp, SetUserPageBreakOp, SetUserSystemBreakOp, TransposeIntervalOp, TransposeOp, + TupletCompensation, }; use crate::stamp::{HybridLogicalClock, OperationStamp}; use crate::support::AuthorId; @@ -89,7 +90,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(28) { + let kind = match rng.below(29) { 0 => { let voice = VoiceId::new(ReplicaId(7), rng.below(3)); let position = MusicalPosition(RationalTime::from_int(rng.below(4) as i32)); @@ -283,6 +284,15 @@ fn gen_payload(rng: &mut SplitMix64) -> OperationPayload { chromatic_steps: rng.below(9) as i32 - 4, }, }), + // Genesis tranche G1, over the shared instrument-id space (arm 21's + // `CreateStaff` already references `InstrumentId::new(ReplicaId(7), + // rng.below(2))`, so mints/re-carries genuinely interact with it). + 28 => OperationKind::CreateInstrument(CreateInstrumentOp { + instrument: valuegen::instrument(epiphany_core::InstrumentId::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 5364767..582f620 100644 --- a/crates/epiphany-ops/src/lib.rs +++ b/crates/epiphany-ops/src/lib.rs @@ -124,15 +124,16 @@ pub use envelope::{ pub use migrate::{migrate_v0_envelope, project_v1_to_v0, MigrationError}; pub use opset::{AcceptOutcome, OperationSet}; pub use payload::{ - ChangeRegionTimeModelOp, CreateCrossCuttingOp, CreateRegionOp, CreateRepeatStructureOp, - CreateStaffInstanceOp, CreateStaffOp, CreateVoiceOp, CrossCuttingValue, DeleteCrossCuttingOp, - DeleteEventOp, DeleteIdentifiedPitchOp, DeleteRegionOp, DeleteRepeatStructureOp, - DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, InsertIdentifiedPitchOp, - ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, OperationKind, OperationKindTag, - OperationPayload, PositionRemapping, ResolveConflictPayload, ResolveEquivocationPayload, - RespellPitchOp, SetMetadataOp, SetMetricGridOp, SetStaffLayoutOp, SetTempoSegmentOp, - SetTimeSignatureOp, SetUserPageBreakOp, SetUserSystemBreakOp, TransactionCategory, - TransactionDescriptor, TransposeIntervalOp, TransposeOp, TupletCompensation, + ChangeRegionTimeModelOp, CreateCrossCuttingOp, CreateInstrumentOp, CreateRegionOp, + CreateRepeatStructureOp, CreateStaffInstanceOp, CreateStaffOp, CreateVoiceOp, + CrossCuttingValue, DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp, + DeleteRegionOp, DeleteRepeatStructureOp, DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, + InsertIdentifiedPitchOp, ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, + OperationKind, OperationKindTag, OperationPayload, PositionRemapping, ResolveConflictPayload, + ResolveEquivocationPayload, RespellPitchOp, SetMetadataOp, SetMetricGridOp, SetStaffLayoutOp, + SetTempoSegmentOp, SetTimeSignatureOp, SetUserPageBreakOp, SetUserSystemBreakOp, + TransactionCategory, TransactionDescriptor, TransposeIntervalOp, TransposeOp, + TupletCompensation, }; pub use reduce::{ canonical_reduction_order, GraphMaterialization, MaterializedState, ObjectState, PendingReason, diff --git a/crates/epiphany-ops/src/migrate.rs b/crates/epiphany-ops/src/migrate.rs index b4f62b3..f8cd473 100644 --- a/crates/epiphany-ops/src/migrate.rs +++ b/crates/epiphany-ops/src/migrate.rs @@ -179,6 +179,8 @@ fn project_kind(kind: &OperationKind) -> V0OperationKind { OperationKind::DeleteRepeatStructure(op) => V0OperationKind::DeleteRepeatStructure(*op), // Push 4a: born past v0; projected verbatim. OperationKind::TransposeInterval(op) => V0OperationKind::TransposeInterval(op.clone()), + // Genesis tranche G1: born past v0; projected verbatim. + OperationKind::CreateInstrument(op) => V0OperationKind::CreateInstrument(op.clone()), } } @@ -332,6 +334,8 @@ fn migrate_kind(kind: &V0OperationKind, context: &Score) -> Result OperationKind::TransposeInterval(op.clone()), V0OperationKind::DeleteRepeatStructure(op) => OperationKind::DeleteRepeatStructure(*op), + // Genesis tranche G1: identity round-trip (no lossy v0 form). + V0OperationKind::CreateInstrument(op) => OperationKind::CreateInstrument(op.clone()), }) } diff --git a/crates/epiphany-ops/src/payload.rs b/crates/epiphany-ops/src/payload.rs index b7fead4..4c3d746 100644 --- a/crates/epiphany-ops/src/payload.rs +++ b/crates/epiphany-ops/src/payload.rs @@ -33,9 +33,9 @@ use epiphany_core::{ Beam, CanonicalValue, Event, EventDuration, EventId, EventPosition, IdentifiedPitch, - InstrumentId, MetricGrid, MusicalDuration, MusicalPosition, OperationId, Pitch, PitchId, - PitchSpelling, Region, RegionId, RegionTimeModel, RepeatStructure, RepeatStructureId, Rest, - ScoreMetadata, Slur, Spanner, Staff, StaffId, StaffInstance, StaffInstanceId, + Instrument, InstrumentId, MetricGrid, MusicalDuration, MusicalPosition, OperationId, Pitch, + PitchId, PitchSpelling, Region, RegionId, RegionTimeModel, RepeatStructure, RepeatStructureId, + Rest, ScoreMetadata, Slur, Spanner, Staff, StaffId, StaffInstance, StaffInstanceId, StaffLineConfiguration, TempoSegment, Tie, TimeAnchor, TimeSignature, TransactionId, TranspositionInterval, TupletId, TypedObjectId, Voice, VoiceId, }; @@ -195,6 +195,14 @@ pub enum OperationKind { /// Push 4a: the faithful transpose. Appended past 29 — a schema-minor /// vocabulary append (`req:binfmt:kind-discriminants`). TransposeInterval(TransposeIntervalOp), + // --- Genesis tranche G1 (`spec/CONTRACT_GENESIS_G1_INSTRUMENT.md`): the + // from-empty spine's missing link. Discriminant extends additively past 30. + // --- + /// Mint an abstract instrument on the score root (set-union creation) — + /// the single missing link between `Score::empty` and a note: + /// `CreateStaff` already demands a live `Instrument` and nothing else can + /// create one. + CreateInstrument(CreateInstrumentOp), } impl OperationKind { @@ -212,12 +220,17 @@ impl OperationKind { pub fn schema_major(&self) -> u16 { match self { // Mandatory v2 appends: CrossCuttingValue (Slur/Tie/Beam/Spanner - // bodies), Staff (default_clef + filled line config), and - // ScoreMetadata (six appended fields). + // bodies), Staff (default_clef + filled line config), + // ScoreMetadata (six appended fields), and Instrument + // (sound_config/default_clef/default_staff_lines et al. — G1; + // unconditional, not `Option`-hidden, so no lower-major layout for + // this payload exists — do not copy the value-dependent arm shape + // below). OperationKind::CreateCrossCutting(_) | OperationKind::ModifyCrossCutting(_) | OperationKind::CreateStaff(_) - | OperationKind::SetMetadata(_) => 2, + | OperationKind::SetMetadata(_) + | OperationKind::CreateInstrument(_) => 2, // Value-dependent: the embedded StaffLineConfiguration rides an // Option; None encodes byte-identically to the prior major. OperationKind::CreateRegion(op) => { @@ -287,6 +300,10 @@ impl OperationKind { // Push 4a; appended past 29. Every constituent is a major-0 // layout, so `schema_major` leaves it in the catch-all 0 arm. OperationKind::TransposeInterval(_) => 30, + // Genesis tranche G1; appended past 30. Coincides with tag 31 by + // accident, not by rule — the two spaces are independent and + // misaligned elsewhere (`RespellPitch` is kind 2 / tag 3). + OperationKind::CreateInstrument(_) => 31, } } @@ -328,6 +345,10 @@ impl OperationKind { OperationKind::CreateRepeatStructure(_) => OperationKindTag::CreateRepeatStructure, OperationKind::DeleteRepeatStructure(_) => OperationKindTag::DeleteRepeatStructure, OperationKind::TransposeInterval(_) => OperationKindTag::TransposeInterval, + // Name-verbatim, as the two most recent additions (`CreateVoice`, + // `CreateRepeatStructure`) are — the tag layer's older + // Create→Insert convention is not followed here (contract pin 3). + OperationKind::CreateInstrument(_) => OperationKindTag::CreateInstrument, } } } @@ -370,6 +391,7 @@ impl CanonicalEncode for OperationKind { OperationKind::SetStaffLayout(op) => op.encode_canonical(out), OperationKind::CreateRepeatStructure(op) => op.encode_canonical(out), OperationKind::DeleteRepeatStructure(op) => op.encode_canonical(out), + OperationKind::CreateInstrument(op) => op.encode_canonical(out), } } } @@ -416,6 +438,10 @@ pub enum OperationKindTag { DeleteRepeatStructure, /// Push 4a. TransposeInterval, + /// Genesis tranche G1. Name-verbatim (contract pin 3): the tag layer's + /// older Create→Insert convention (`InsertStaff` for `CreateStaff`) is not + /// followed here, matching the two most recent additions. + CreateInstrument, } /// The discriminant of [`OperationKindTag::Registered`], the one tag that @@ -513,6 +539,7 @@ operation_kind_tag_vocabulary! { CreateRepeatStructure = 28 => "create-repeat-structure", DeleteRepeatStructure = 29 => "delete-repeat-structure", TransposeInterval = 30 => "transpose-interval", + CreateInstrument = 31 => "create-instrument", } impl CanonicalEncode for OperationKindTag { @@ -1414,6 +1441,35 @@ impl CanonicalEncode for CreateStaffOp { } } +// --- Genesis tranche G1 (`spec/CONTRACT_GENESIS_G1_INSTRUMENT.md`). ---------- + +/// Mint an abstract [`Instrument`] on the score root (operation_catalog +/// §CreateInstrument). Carries the full instrument value (schema major 2): +/// identity, name, range, abbreviation, sound configuration, transposition, +/// default clef, default staff-line configuration, and unpitched members. +/// `Instrument` holds no outbound entity references, so this operation needs +/// no referential preconditions — only mint and idempotence, exactly the +/// `CreateStaff` discipline. Set-union creation: a repeat create carrying a +/// byte-identical value is idempotent; a differing value under a live id is a +/// precondition no-op. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct CreateInstrumentOp { + pub instrument: Instrument, +} + +impl CreateInstrumentOp { + /// The minted instrument's identifier. + pub fn instrument_id(&self) -> InstrumentId { + self.instrument.id + } +} + +impl CanonicalEncode for CreateInstrumentOp { + fn encode_canonical(&self, out: &mut Vec) { + push_lp_bytes(out, &self.instrument.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 diff --git a/crates/epiphany-ops/src/reduce.rs b/crates/epiphany-ops/src/reduce.rs index 7c207ff..7a7d995 100644 --- a/crates/epiphany-ops/src/reduce.rs +++ b/crates/epiphany-ops/src/reduce.rs @@ -34,13 +34,13 @@ use std::collections::{BTreeMap, BTreeSet, BinaryHeap}; use epiphany_core::{ canonical_pitch_bytes, derive_promoted_voice_id, simplest_spelling, AnchorOffset, AnnotationAnchor, CanonicalValue, Event, EventDuration, EventId, EventPosition, - GestureAnchoring, InstrumentId, MeterChange, MetricGrid, MusicalDuration, MusicalPosition, - OperationId, Pitch, PitchId, PitchSpelling, RationalTime, RegionEdge, RegionId, - RegionTimeModel, ReplicaId, Score, ScoreMetadata, SpellingAttachment, SpellingDirective, - SpellingScope, SpellingSource, Staff, StaffId, StaffInstance, StaffInstanceId, - StaffLineConfiguration, TempoMap, TempoSegment, TempoShape, TimeAnchor, TimeSignature, - TimeSignatureId, TransactionId, TransposeRefusal, TranspositionInterval, TypedObjectId, Voice, - VoiceId, VoiceOrigin, + GestureAnchoring, Instrument, InstrumentId, MeterChange, MetricGrid, MusicalDuration, + MusicalPosition, OperationId, Pitch, PitchId, PitchSpelling, RationalTime, RegionEdge, + RegionId, RegionTimeModel, ReplicaId, Score, ScoreMetadata, SpellingAttachment, + SpellingDirective, SpellingScope, SpellingSource, Staff, StaffId, StaffInstance, + StaffInstanceId, StaffLineConfiguration, TempoMap, TempoSegment, TempoShape, TimeAnchor, + TimeSignature, TimeSignatureId, TransactionId, TransposeRefusal, TranspositionInterval, + TypedObjectId, Voice, VoiceId, VoiceOrigin, }; use epiphany_determinism::CanonicalEncode; @@ -56,13 +56,14 @@ 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, CreateRegionOp, CreateRepeatStructureOp, - CreateStaffInstanceOp, CreateStaffOp, CreateVoiceOp, CrossCuttingValue, DeleteCrossCuttingOp, - DeleteEventOp, DeleteIdentifiedPitchOp, DeleteRegionOp, DeleteRepeatStructureOp, - DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, InsertIdentifiedPitchOp, - ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, OperationKind, OperationPayload, - RespellPitchOp, SetMetadataOp, SetMetricGridOp, SetStaffLayoutOp, SetTempoSegmentOp, - SetTimeSignatureOp, SetUserPageBreakOp, TransposeIntervalOp, TransposeOp, TupletCompensation, + resolved_anchor_position, CreateCrossCuttingOp, CreateInstrumentOp, CreateRegionOp, + CreateRepeatStructureOp, CreateStaffInstanceOp, CreateStaffOp, CreateVoiceOp, + CrossCuttingValue, DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp, + DeleteRegionOp, DeleteRepeatStructureOp, DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, + InsertIdentifiedPitchOp, ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, + OperationKind, OperationPayload, RespellPitchOp, SetMetadataOp, SetMetricGridOp, + SetStaffLayoutOp, SetTempoSegmentOp, SetTimeSignatureOp, SetUserPageBreakOp, + TransposeIntervalOp, TransposeOp, TupletCompensation, }; use crate::stamp::StampTuple; use crate::support::{ObjectKind, SerializedCanonicalInputs}; @@ -913,6 +914,13 @@ struct Reducer<'a> { // precondition no-op). Seeded from the base graph. staff_values: BTreeMap, time_signature_values: BTreeMap, + // Carried values of set-union-minted instruments (genesis tranche G1), + // mirroring `staff_values`: the byte-identical-re-carry idempotence check + // against a *base* instrument (pin 8 of + // `CONTRACT_GENESIS_G1_INSTRUMENT.md`) has nothing to compare without this + // — `TypedObjectId::Instrument` liveness is seeded from the base + // (`seed_from_graph`), but the base seed left no value map before this. + instrument_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 @@ -958,6 +966,13 @@ struct Reducer<'a> { equivocation_resolutions: BTreeMap, promoted_singles: BTreeMap, graph: Option, + // Genesis tranche G1 (`spec/CONTRACT_GENESIS_G1_INSTRUMENT.md` pin 9): + // whether `new_onto`'s *pristine* base was exactly `Score::empty(identity)` + // — captured once, before any operation mutates `graph`, since by the time + // `run()` finishes `graph` is no longer empty and cannot answer this + // question itself. Gates the identity-cursor derivation to from-empty + // reduction only, never a transaction snapshot (fixed for the whole run). + from_empty_base: bool, } /// A snapshot of the working state, for atomic transaction rollback. @@ -1003,6 +1018,7 @@ struct WorkingSnapshot { staff_layout_chain: BTreeMap>, staff_values: BTreeMap, time_signature_values: BTreeMap, + instrument_values: BTreeMap, structures: BTreeMap>, region_instances: BTreeMap>, instance_voices: BTreeMap>, @@ -1281,6 +1297,7 @@ impl<'a> Reducer<'a> { staff_layout_chain: BTreeMap::new(), staff_values: BTreeMap::new(), time_signature_values: BTreeMap::new(), + instrument_values: BTreeMap::new(), structures: BTreeMap::new(), region_instances: BTreeMap::new(), instance_voices: BTreeMap::new(), @@ -1296,11 +1313,16 @@ impl<'a> Reducer<'a> { equivocation_resolutions: BTreeMap::new(), promoted_singles: BTreeMap::new(), graph: None, + from_empty_base: false, } } fn new_onto(op_set: &'a OperationSet, base: &Score) -> Self { let mut reducer = Self::new(op_set); + // Captured from the pristine `base` argument, before `graph` is + // populated and mutated below — `graph` is no longer empty by the + // time `run()` needs this answer. + reducer.from_empty_base = *base == Score::empty(base.identity.clone()); reducer.graph = Some(base.clone()); reducer.seed_from_graph(); reducer @@ -1318,6 +1340,12 @@ impl<'a> Reducer<'a> { for instrument in &score.instruments { self.objects .insert(TypedObjectId::Instrument(instrument.id), ObjectState::Live); + // The carried value backs CreateInstrument's byte-identical-re-carry + // idempotence check against base instruments (contract pin 8) — + // without this, `TypedObjectId::Instrument` liveness is seeded but + // there is nothing to compare a re-carry against. + self.instrument_values + .insert(instrument.id, instrument.clone()); } for staff in &score.staves { self.objects @@ -1913,6 +1941,31 @@ impl<'a> Reducer<'a> { pending.into_iter().chain(held).collect(); pending_vec.sort_by_key(|(id, _)| *id); + // Genesis tranche G1 (`spec/CONTRACT_GENESIS_G1_INSTRUMENT.md` pin 9; + // ruling §3 point 3): from-empty reduction is the first thing that + // ever writes `identity`. Gated on `from_empty_base`, captured in + // `new_onto` from the *pristine* base before any operation mutated + // `graph` — `graph` itself is no longer empty by this point, so a + // live re-comparison here would never fire. This keeps onto-reduction + // against a populated base (every other caller, notably + // `epiphany-editor-core`'s materialize-on-every-edit) byte-for-byte + // unchanged; nothing in this packet's blast radius touches that + // crate. Nothing between `new_onto` and here writes `identity` + // itself, so `graph.identity`'s replica and seed are still the + // pristine base's. + if self.from_empty_base { + if let Some(graph) = self.graph.as_ref() { + let replica = graph.identity.replica_id; + let seed = graph.identity.next_counter; + let next_counter = self.derive_identity_cursor(replica, seed); + self.graph + .as_mut() + .expect("checked Some above") + .identity + .next_counter = next_counter; + } + } + let graph = self.graph.take(); let state = MaterializedState { effects: self.effects, @@ -1927,6 +1980,60 @@ impl<'a> Reducer<'a> { (state, graph) } + /// Derives the from-empty identity cursor (ruling §3 point 3): `1 + + /// max(counter)` over every id — an envelope's own [`OperationId`], or an + /// entity id minted into the reduced graph — the reducing `replica` + /// authored anywhere in the log, or the untouched `seed` when that + /// replica authored none. + /// + /// `next_counter` is a *single* monotonic counter shared by every + /// identifier kind (`IdentityContext::take_counter`), so this cannot be + /// scoped to `Instrument` alone: any kind minted in the same log — + /// `CreateStaff`'s `StaffId`, `InsertEvent`'s `EventId`, and so on — + /// shares the same sequence, and a later mint from the returned identity + /// must not re-issue a counter the log already used for *any* kind. + fn derive_identity_cursor(&self, replica: ReplicaId, seed: u64) -> u64 { + let mut max_counter: Option = None; + let mut bump = |counter: u64| { + max_counter = Some(max_counter.map_or(counter, |m| m.max(counter))); + }; + // Every operation id ever accepted into this set, in any slot state + // (Single, Equivocated, promoted or not): the set is keyed by + // `OperationId`, so its key set is exactly "every id in the log" — + // including pending/held/excluded envelopes, which still burned a + // counter at authoring time even though this reduction did not apply + // them. + for (id, _) in self.op_set.slots() { + if id.replica == replica { + bump(id.counter); + } + } + // Every entity id minted into the reduced graph, across every kind — + // not read via a per-kind accessor, but generically from the object + // universe every mint already populates. `TypedObjectId`'s canonical + // form is a 2-byte discriminant then the 16-byte `(replica, counter)` + // payload for every variant except `Registered` (an extension id with + // no such convention, and so skipped here); reading it this way + // avoids a new public replica/counter accessor on `TypedObjectId` in + // `epiphany-core`, which is out of this packet's blast radius. + for obj in self.objects.keys() { + let bytes = obj.canonical_bytes(); + if bytes.len() == 18 { + let id_replica = ReplicaId(u64::from_be_bytes( + bytes[2..10].try_into().expect("8 bytes"), + )); + if id_replica == replica { + let counter = u64::from_be_bytes(bytes[10..18].try_into().expect("8 bytes")); + bump(counter); + } + } + } + match max_counter { + Some(m) => seed.max(m + 1), + None => seed, + } + } + fn record_anomaly(&mut self, kind: IntegrityAnomalyKind) { let a = IntegrityAnomaly::new(kind); self.anomalies.entry(a.id).or_insert(a); @@ -2635,6 +2742,7 @@ impl<'a> Reducer<'a> { OperationKind::SetStaffLayout(op) => self.set_staff_layout(env, op), OperationKind::CreateRepeatStructure(op) => self.create_repeat_structure(env, op), OperationKind::DeleteRepeatStructure(op) => self.delete_repeat_structure(env, op), + OperationKind::CreateInstrument(op) => self.create_instrument(env, op), }, OperationPayload::ResolveConflict(op) => self.resolve_conflict(env, op), OperationPayload::UndoTransaction(op) => self.undo_transaction(env, op), @@ -3854,6 +3962,56 @@ impl<'a> Reducer<'a> { OperationEffect::Applied } + // --- Genesis tranche G1 (`spec/CONTRACT_GENESIS_G1_INSTRUMENT.md`). ------- + + /// Set-union creation of an `Instrument` on the score root + /// (operation_catalog §CreateInstrument): fresh id mints; a + /// byte-identical re-carry is idempotent; a differing value under a live + /// id is a precondition no-op — exactly `create_staff`'s discipline + /// (contract pin 6). Unlike `create_staff`, there is no graph-aware + /// reference-resolution block: `Instrument` holds no outbound entity + /// references (contract preamble), so this operation needs no referential + /// preconditions at all. + fn create_instrument( + &mut self, + env: &OperationEnvelope, + op: &CreateInstrumentOp, + ) -> OperationEffect { + let iobj = TypedObjectId::Instrument(op.instrument_id()); + match self.objects.get(&iobj) { + Some(ObjectState::Live) => { + let identical = self + .instrument_values + .get(&op.instrument_id()) + .is_some_and(|known| known == &op.instrument); + 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.instruments.push(op.instrument.clone()); + } + self.mint_container(env, iobj); + self.instrument_values + .insert(op.instrument_id(), op.instrument.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 @@ -7233,6 +7391,7 @@ impl<'a> Reducer<'a> { staff_layout_chain: self.staff_layout_chain.clone(), staff_values: self.staff_values.clone(), time_signature_values: self.time_signature_values.clone(), + instrument_values: self.instrument_values.clone(), structures: self.structures.clone(), region_instances: self.region_instances.clone(), instance_voices: self.instance_voices.clone(), @@ -7269,6 +7428,7 @@ impl<'a> Reducer<'a> { self.staff_layout_chain = s.staff_layout_chain; self.staff_values = s.staff_values; self.time_signature_values = s.time_signature_values; + self.instrument_values = s.instrument_values; self.structures = s.structures; self.region_instances = s.region_instances; self.instance_voices = s.instance_voices; @@ -7452,7 +7612,7 @@ mod tests { use crate::causal::CausalContext; use crate::stamp::{HybridLogicalClock, OperationStamp}; use crate::support::AuthorId; - use epiphany_core::{RationalTime, ReplicaId, StaffInstanceId, WallClockTime}; + use epiphany_core::{IdentityContext, RationalTime, ReplicaId, StaffInstanceId, WallClockTime}; fn pos(n: i64) -> MusicalPosition { MusicalPosition(RationalTime::from_int(n as i32)) @@ -10486,6 +10646,27 @@ mod tests { 0, "DeleteRepeatStructure carries a bare id — a major-0 layout" ); + + // Genesis tranche G1 (i7): CreateInstrument is *unconditionally* v2 — + // contract pin 4/trap 3. Unlike CreateRegion/SetStaffLayout above, + // whose v2 embedding rides an `Option` and is value-dependent, + // Instrument's schema-major-2 appends (sound_config, default_clef, + // default_staff_lines, ...) are mandatory fields, so even the most + // minimal instrument — every optional field at `None`, built via + // `Instrument::new` exactly like `valuegen::instrument` does — stamps + // 2. Asserted on the minimal value specifically, so an arm that + // becomes value-dependent (checking e.g. `range.is_some()`) is + // caught immediately rather than only on a richer fixture. + let minimal_instrument = + epiphany_core::Instrument::new(epiphany_core::InstrumentId::new(ReplicaId(9), 7), "x"); + assert_eq!( + OperationKind::CreateInstrument(crate::payload::CreateInstrumentOp { + instrument: minimal_instrument, + }) + .schema_major(), + 2, + "CreateInstrument is unconditionally v2, even for the minimal instrument" + ); } #[test] @@ -10553,6 +10734,15 @@ mod tests { // moves. Nothing leaked: `canonical_bytes` embeds effects, conflicts, // and anomalies, never payload values, and the new payload's // constituents are major-0 layouts regardless. + // + // Re-pinned again at genesis tranche G1 + // (`spec/CONTRACT_GENESIS_G1_INSTRUMENT.md`): `gen_payload` gained + // `CreateInstrument`, discriminant 31, and `rng.below(28)` became + // `below(29)` — the same reshuffle, for the same reason. Nothing + // leaked here either: `CreateInstrument.schema_major()` is 2 + // (unconditionally, contract pin 4), but `MaterializedState` stamps + // no schema major at all — that is an `OperationEnvelopeBlock` + // concern in `epiphany-bundle`, which this packet does not touch. let mut rng = epiphany_determinism::fuzz::SplitMix64::new(0xBA5E); let envelopes = crate::fuzz::gen_envelope_set(&mut rng, 200); let mut set = OperationSet::new(); @@ -10562,7 +10752,7 @@ mod tests { let hex: String = digest.iter().map(|b| format!("{b:02x}")).collect(); assert_eq!( hex, - "594f29e20250a9ff36f0033a59e380e4514aa32a6e78d062579b950667439e20" + "61af8ebbba1c4d98360ec44812e5d97a89a720738c1e3573d865f26b48addd1d" ); } @@ -12365,4 +12555,397 @@ mod tests { ); } } + + // === Genesis tranche G1 (`spec/CONTRACT_GENESIS_G1_INSTRUMENT.md`). ======= + + /// The full from-empty spine, one replica-1 operation per even counter + /// (0, 2, 4, 6, 8, 10) and one minted entity id per odd counter + /// (1, 3, 5, 7, 9, 11) — `CreateInstrument` (i1's load-bearing chain, + /// the ruling's acceptance criterion 1): `CreateInstrument` -> + /// `CreateStaff` -> `CreateRegion` -> `CreateStaffInstance` -> + /// `CreateVoice` -> `InsertEvent`. Returns the envelopes in canonical + /// authoring order, plus the ids a caller needs to inspect the result. + struct GenesisSpine { + envelopes: Vec, + event_id: EventId, + } + + fn genesis_spine_envelopes() -> GenesisSpine { + let instrument_id = InstrumentId::new(ReplicaId(1), 1); + let staff_id = StaffId::new(ReplicaId(1), 3); + let region_id = RegionId::new(ReplicaId(1), 5); + let instance_id = StaffInstanceId::new(ReplicaId(1), 7); + let voice_id = VoiceId::new(ReplicaId(1), 9); + let event_id = EventId::new(ReplicaId(1), 11); + + let create_instrument = prim_env( + 1, + 0, + 10, + CausalContext::new(), + OperationKind::CreateInstrument(CreateInstrumentOp { + instrument: crate::valuegen::instrument(instrument_id), + }), + ); + let create_staff = prim_env( + 1, + 2, + 20, + CausalContext::new(), + OperationKind::CreateStaff(CreateStaffOp { + staff: crate::valuegen::staff(staff_id, instrument_id), + }), + ); + let create_region = prim_env( + 1, + 4, + 30, + CausalContext::new(), + OperationKind::CreateRegion(CreateRegionOp { + region: crate::valuegen::region(region_id), + }), + ); + let create_instance = prim_env( + 1, + 6, + 40, + CausalContext::new(), + OperationKind::CreateStaffInstance(CreateStaffInstanceOp { + region: region_id, + instance: crate::valuegen::staff_instance(instance_id, staff_id), + }), + ); + let create_voice = prim_env( + 1, + 8, + 50, + CausalContext::new(), + OperationKind::CreateVoice(CreateVoiceOp { + staff_instance: instance_id, + voice: crate::valuegen::voice(voice_id), + }), + ); + let insert_event = prim_env( + 1, + 10, + 60, + CausalContext::new(), + OperationKind::InsertEvent(InsertEventOp { + staff_instance: instance_id, + event: crate::valuegen::insert_event_value( + event_id, + voice_id, + pos(0), + epiphany_core::MusicalDuration::whole(), + &[], + ), + }), + ); + GenesisSpine { + envelopes: vec![ + create_instrument, + create_staff, + create_region, + create_instance, + create_voice, + insert_event, + ], + event_id, + } + } + + /// (i1) The packet's load-bearing test and the ruling's acceptance + /// criterion 1: a document created empty and given only operations + /// materializes a note-bearing `Score` — no fixture, no base. + /// `CreateInstrument` is the single missing link, so this also asserts + /// every intermediate link `Applied` rather than only the final note, so + /// a future regression is pinned at its actual source. + /// + /// **Mutation** (verified by hand, not left permanently in this test): + /// drop the `CreateInstrument` envelope from the set. `CreateStaff` then + /// finds no live instrument and reduces to + /// `NoOp{PreconditionFailedUnderReduction{TargetMissing}}`, which cascades + /// (no staff instance, no voice, no event) so the final `contains` check + /// fails too — see the report for the observed failure text. + #[test] + fn from_empty_operations_alone_materialize_a_note() { + let spine = genesis_spine_envelopes(); + let mut set = OperationSet::new(); + set.accept_all(spine.envelopes); + let identity = IdentityContext::new(ReplicaId(1)); + let out = reduce_operation_set_onto(&set, &Score::empty(identity)); + + for counter in [0, 2, 4, 6, 8, 10] { + assert_eq!( + effect_at(&out.state, counter), + Some(&OperationEffect::Applied), + "spine operation at counter {counter} must apply" + ); + } + assert!( + out.score.events.contains(spine.event_id), + "the spine reaches a note" + ); + } + + /// (i2) Re-carry idempotence: the same `CreateInstrument` twice -> + /// `AlreadyApplied`, and a differing second carry under the same live id + /// -> `RecreateContentMismatch` — exactly `create_staff`'s discipline + /// (contract pin 6). + /// + /// **Mutation:** make the second carry differ in one field -> + /// `RecreateContentMismatch` where the baseline case (identical carries) + /// reduces `AlreadyApplied`. Both branches are standing assertions here + /// (not a temporary edit), so the contrast is permanent. + #[test] + fn create_instrument_recarry_is_idempotent_or_a_precondition_no_op() { + let instrument_id = InstrumentId::new(ReplicaId(1), 1); + let value = crate::valuegen::instrument(instrument_id); + let create = prim_env( + 1, + 0, + 10, + CausalContext::new(), + OperationKind::CreateInstrument(CreateInstrumentOp { + instrument: value.clone(), + }), + ); + let identical = prim_env( + 2, + 0, + 20, + CausalContext::new(), + OperationKind::CreateInstrument(CreateInstrumentOp { + instrument: value.clone(), + }), + ); + let mut differing_value = value.clone(); + differing_value.name = String::from("something else"); + let differing = prim_env( + 3, + 0, + 30, + CausalContext::new(), + OperationKind::CreateInstrument(CreateInstrumentOp { + instrument: differing_value, + }), + ); + + let mut set = OperationSet::new(); + set.accept_all(vec![differing.clone(), identical.clone(), create.clone()]); + let state = set.reduce(); + let effect_of = |id: OperationId| { + state + .effects + .iter() + .find(|(e, _)| *e == id) + .map(|(_, eff)| eff) + }; + assert_eq!(effect_of(create.id), Some(&OperationEffect::Applied)); + assert_eq!( + effect_of(identical.id), + Some(&OperationEffect::NoOp { + reason: NoOpReason::AlreadyApplied, + }), + "a byte-identical re-create reduces idempotently" + ); + assert_eq!( + effect_of(differing.id), + Some(&OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::RecreateContentMismatch, + }, + }), + "a differing value under a live id is a precondition no-op" + ); + } + + /// (i3) Re-carry against a *base* instrument — the pin-8 case. + /// `TypedObjectId::Instrument` liveness is seeded from the base + /// (`seed_from_graph`), but without `instrument_values` seeded too, a + /// byte-identical re-carry against a base instrument has nothing to + /// compare. + /// + /// **Mutation:** skip seeding `instrument_values` in `seed_from_graph` -> + /// the byte-identical re-carry is misreported + /// `RecreateContentMismatch` instead of `AlreadyApplied`. + #[test] + fn create_instrument_recarry_against_a_base_instrument_is_idempotent() { + let instrument_id = InstrumentId::new(ReplicaId(1), 1); + let value = crate::valuegen::instrument(instrument_id); + let mut base = Score::empty(IdentityContext::new(ReplicaId(1))); + base.instruments.push(value.clone()); + + let recreate = prim_env( + 2, + 0, + 10, + CausalContext::new(), + OperationKind::CreateInstrument(CreateInstrumentOp { instrument: value }), + ); + let mut set = OperationSet::new(); + set.accept_all(vec![recreate.clone()]); + let out = reduce_operation_set_onto(&set, &base); + + let effect = out + .state + .effects + .iter() + .find(|(id, _)| *id == recreate.id) + .map(|(_, eff)| eff); + assert_eq!( + effect, + Some(&OperationEffect::NoOp { + reason: NoOpReason::AlreadyApplied, + }), + "byte-identical re-carry against a base instrument is idempotent" + ); + assert_eq!( + out.score.instruments.len(), + 1, + "no duplicate instrument is minted" + ); + } + + /// (i4) Order independence (ruling acceptance criterion 2): two replicas + /// racing to create the *same* instrument id with differing values (a + /// concurrent genesis-era scenario) converge to byte-identical + /// `MaterializedState` regardless of delivery order — canonical order, + /// never insertion order, decides the winner. + /// + /// **Mutation:** perturb one op's reduction order (its stamp) — asserted + /// permanently below as `assert_ne!`, so the equality above is proven + /// non-vacuous: a *genuine* reordering really does change the converged + /// bytes, which is what the primary assertion would have caught had + /// insertion order actually leaked into the result. + #[test] + fn concurrent_differing_creates_of_the_same_instrument_converge_regardless_of_delivery_order() { + let instrument_id = InstrumentId::new(ReplicaId(9), 1); + let value_a = crate::valuegen::instrument(instrument_id); + let mut value_b = value_a.clone(); + value_b.name = String::from("a concurrently-authored name"); + + let create_a = prim_env( + 1, + 0, + 10, + CausalContext::new(), + OperationKind::CreateInstrument(CreateInstrumentOp { + instrument: value_a, + }), + ); + let create_b = prim_env( + 2, + 0, + 10, + CausalContext::new(), + OperationKind::CreateInstrument(CreateInstrumentOp { + instrument: value_b, + }), + ); + + let forward = { + let mut set = OperationSet::new(); + set.accept_all(vec![create_a.clone(), create_b.clone()]); + set.reduce().canonical_bytes() + }; + let reversed = { + let mut set = OperationSet::new(); + set.accept_all(vec![create_b.clone(), create_a.clone()]); + set.reduce().canonical_bytes() + }; + assert_eq!( + forward, reversed, + "delivery order must not affect the converged state" + ); + + // Proves the equality above is not vacuous: perturbing one op's own + // reduction-order-determining field (its HLC stamp) so it now sorts + // *before* its concurrent sibling really does flip the winner and + // change the converged bytes. + let perturbed = { + let mut earlier_b = create_b.clone(); + earlier_b.stamp = + OperationStamp::new(HybridLogicalClock::new(WallClockTime(5), 0), earlier_b.id); + let mut set = OperationSet::new(); + set.accept_all(vec![create_a.clone(), earlier_b]); + set.reduce().canonical_bytes() + }; + assert_ne!( + forward, perturbed, + "a genuine reduction-order change must change the winner" + ); + } + + /// (i5) The identity cursor (ruling §3 point 3). Minting from a + /// reduced-from-empty score must not collide with any id the log already + /// used — `genesis_spine_envelopes` places replica 1's ids at counters + /// `0..=11` (an operation id at every even counter, a minted entity id at + /// every odd one), so the derived cursor must be exactly `12`. + /// + /// **Mutation:** leave `next_counter` at the seed (`0`) -> the next mint + /// collides with `create_instrument`'s own `OperationId` counter. + #[test] + fn identity_cursor_advances_past_every_id_the_replica_used() { + let spine = genesis_spine_envelopes(); + let mut set = OperationSet::new(); + set.accept_all(spine.envelopes); + let identity = IdentityContext::new(ReplicaId(1)); + let mut out = reduce_operation_set_onto(&set, &Score::empty(identity)); + + assert_eq!( + out.score.identity.next_counter, 12, + "the cursor advances past every id (op or entity) the log used" + ); + let minted: EventId = out.score.identity.mint(); + assert_eq!( + minted, + EventId::new(ReplicaId(1), 12), + "a fresh mint from the reduced identity collides with nothing in the log" + ); + } + + /// (i6) Base-free reduction skips a referential precondition that + /// graph-aware reduction enforces — a **designed asymmetry**, not a bug: + /// base-free reduction has no instrument universe to check against + /// (`reduce.rs` — `self.graph.is_some()` guards `create_staff`'s + /// reference-resolution block). A later reader must not "fix" this by + /// adding the check to base-free reduction; the fix, if this is ever + /// wrong, is to stop calling `reduce_operation_set` on a from-empty + /// document (contract pin 10 / ruling §4). + #[test] + fn base_free_reduction_skips_the_referential_precondition_graph_aware_reduction_enforces() { + let staff_id = StaffId::new(ReplicaId(1), 1); + let missing_instrument = InstrumentId::new(ReplicaId(1), 99); // never minted + let create_staff = prim_env( + 1, + 0, + 10, + CausalContext::new(), + OperationKind::CreateStaff(CreateStaffOp { + staff: crate::valuegen::staff(staff_id, missing_instrument), + }), + ); + let mut set = OperationSet::new(); + set.accept_all(vec![create_staff.clone()]); + + let base_free = reduce_operation_set(&set); + assert_eq!( + effect_at(&base_free, 0), + Some(&OperationEffect::Applied), + "base-free reduction has no instrument universe to check against" + ); + + let identity = IdentityContext::new(ReplicaId(1)); + let onto = reduce_operation_set_onto(&set, &Score::empty(identity)); + assert_eq!( + effect_at(&onto.state, 0), + Some(&OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::TargetMissing, + }, + }), + "graph-aware reduction enforces the precondition base-free skipped" + ); + } } diff --git a/crates/epiphany-ops/src/textproj_kind.rs b/crates/epiphany-ops/src/textproj_kind.rs index e5c5ea6..6badf5e 100644 --- a/crates/epiphany-ops/src/textproj_kind.rs +++ b/crates/epiphany-ops/src/textproj_kind.rs @@ -8,12 +8,13 @@ use epiphany_determinism::{sorted_canonical, CanonicalEncode}; use unicode_normalization::UnicodeNormalization; use crate::payload::{ - ChangeRegionTimeModelOp, CreateCrossCuttingOp, CreateRegionOp, CreateRepeatStructureOp, - CreateStaffInstanceOp, CreateStaffOp, CreateVoiceOp, DeleteCrossCuttingOp, DeleteEventOp, - DeleteIdentifiedPitchOp, DeleteRegionOp, DeleteRepeatStructureOp, DeleteStaffInstanceOp, - DeleteVoiceOp, InsertEventOp, InsertIdentifiedPitchOp, ModifyCrossCuttingOp, ModifyEventOp, - ModifyIdentifiedPitchOp, OperationKind, OperationKindTag, RespellPitchOp, SetMetadataOp, - SetMetricGridOp, SetStaffLayoutOp, SetTempoSegmentOp, SetTimeSignatureOp, SetUserPageBreakOp, + ChangeRegionTimeModelOp, CreateCrossCuttingOp, CreateInstrumentOp, CreateRegionOp, + CreateRepeatStructureOp, CreateStaffInstanceOp, CreateStaffOp, CreateVoiceOp, + DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp, DeleteRegionOp, + DeleteRepeatStructureOp, DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, + InsertIdentifiedPitchOp, ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, + OperationKind, OperationKindTag, RespellPitchOp, SetMetadataOp, SetMetricGridOp, + SetStaffLayoutOp, SetTempoSegmentOp, SetTimeSignatureOp, SetUserPageBreakOp, SetUserSystemBreakOp, TransactionDescriptor, TransposeIntervalOp, TransposeOp, }; use crate::support::OperationKindRegistryId; @@ -218,6 +219,9 @@ impl TextValue for OperationKind { self.tag(), vec![op.targets.project(), op.interval.project()], ), + OperationKind::CreateInstrument(op) => { + production(self.tag(), vec![op.instrument.project()]) + } } } @@ -531,6 +535,14 @@ impl TextValue for OperationKind { interval: TextValue::parse(interval)?, }) } + OperationKindTag::CreateInstrument => { + let [instrument] = fields(s, tag, 1)? else { + unreachable!("the arity-1 check returned one field") + }; + OperationKind::CreateInstrument(CreateInstrumentOp { + instrument: TextValue::parse(instrument)?, + }) + } }) } } @@ -584,7 +596,7 @@ mod tests { #[test] fn every_operation_kind_round_trips_with_canonical_text() { let tags: Vec<_> = all_tags().collect(); - assert_eq!(tags.len(), 31, "the grammar has 31 kind productions"); + assert_eq!(tags.len(), 32, "the grammar has 32 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 ad1e2bd..cef65fd 100644 --- a/crates/epiphany-ops/src/v0.rs +++ b/crates/epiphany-ops/src/v0.rs @@ -103,6 +103,10 @@ pub enum V0OperationKind { /// Born at wire-disc 30 under major-0 layouts (Push 4a); no lossy v0 /// form, so it projects verbatim like the repeat-authoring pair. TransposeInterval(crate::payload::TransposeIntervalOp), + // Genesis tranche G1 — born at wire-disc 31; no lossy v0 form (v0 predates + // the catalog entirely), so it round-trips by identity like every other + // v1-native kind above. + CreateInstrument(crate::payload::CreateInstrumentOp), } /// 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 c9f83f2..08b51bf 100644 --- a/crates/epiphany-ops/src/valuegen.rs +++ b/crates/epiphany-ops/src/valuegen.rs @@ -361,6 +361,13 @@ pub fn staff(id: StaffId, instrument: epiphany_core::InstrumentId) -> epiphany_c } } +/// A minimal [`Instrument`](epiphany_core::Instrument) (genesis tranche G1) — +/// the value a `CreateInstrument` mints: named for its counter, every +/// schema-major-2 field at `Instrument::new`'s canonical default. +pub fn instrument(id: epiphany_core::InstrumentId) -> epiphany_core::Instrument { + epiphany_core::Instrument::new(id, format!("instrument-{}", id.counter())) +} + /// A well-formed `numerator`/4 [`TimeSignature`](epiphany_core::TimeSignature) /// (Phase-3 tranche): `numerator` quarter-note beat groups summing exactly to /// the measure duration, so [`epiphany_core::TimeSignature::new`]'s beat-group diff --git a/crates/epiphany-ops/src/vectors.rs b/crates/epiphany-ops/src/vectors.rs index 7ba133d..5e0fa71 100644 --- a/crates/epiphany-ops/src/vectors.rs +++ b/crates/epiphany-ops/src/vectors.rs @@ -16,11 +16,11 @@ //! The `class` string is informative, not normative: implementations need not //! agree on error taxonomy, only on the accept/reject verdict. -use epiphany_core::{EventId, OperationId, ReplicaId, TypedObjectId}; +use epiphany_core::{EventId, InstrumentId, OperationId, ReplicaId, TypedObjectId}; use epiphany_determinism::CanonicalEncode; use crate::{ - IntegrityAnomaly, IntegrityAnomalyKind, MaterializedState, ObjectState, + IntegrityAnomaly, IntegrityAnomalyKind, MaterializedState, ObjectState, OperationEnvelope, OperationKindRegistryId, OperationKindTag, PendingReason, }; @@ -255,6 +255,49 @@ pub fn decode_vectors() -> Vec { short_registered, )); + // --- OperationEnvelope carrying CreateInstrument (genesis tranche G1) -- + // + // `ops.operation_kind_tag` above pins only the bare, payload-free tag + // byte; nothing in this corpus previously exercised a *value-carrying* + // `OperationKind` payload's decode path at all. Committed here so a + // future encoder/decoder change to this payload moves this vector's + // bytes deliberately, in the diff (the 3b-i lesson the module doc names: + // round-trip locking alone cannot see a self-consistent reorder of both + // halves). + const OE: &str = "ops.operation_envelope"; + let envelope = OperationEnvelope { + id: OperationId::new(ReplicaId(1), 1), + author: crate::support::AuthorId(0), + stamp: crate::stamp::OperationStamp::new( + crate::stamp::HybridLogicalClock::new(epiphany_core::WallClockTime(1), 1), + OperationId::new(ReplicaId(1), 1), + ), + causal_context: crate::causal::CausalContext::new(), + transaction: None, + payload: crate::payload::OperationPayload::Primitive( + crate::payload::OperationKind::CreateInstrument(crate::payload::CreateInstrumentOp { + instrument: crate::valuegen::instrument(InstrumentId::new(ReplicaId(1), 1)), + }), + ), + }; + let envelope_bytes = envelope.to_canonical_bytes(); + v.push(row( + OE, + "accept", + "-", + "create_instrument", + envelope_bytes.clone(), + )); + let mut trailing = envelope_bytes; + trailing.push(0); + v.push(row( + OE, + "reject", + "trailing-bytes", + "create_instrument_trailing", + trailing, + )); + v } @@ -277,6 +320,10 @@ pub fn check(surface: &str, bytes: &[u8]) -> Option> { Err(e) => Err(format!("{e}")), }), "ops.operation_kind_tag" => Some(decode_tag(bytes)), + "ops.operation_envelope" => Some(match crate::envdecode::decode_envelope(bytes) { + Ok(env) => Ok(env.to_canonical_bytes() == bytes), + Err(e) => Err(format!("{e:?}")), + }), _ => None, } } @@ -315,7 +362,11 @@ mod tests { /// pinning half a contract. #[test] fn every_surface_carries_both_verdicts() { - for surface in ["ops.materialized_state", "ops.operation_kind_tag"] { + for surface in [ + "ops.materialized_state", + "ops.operation_kind_tag", + "ops.operation_envelope", + ] { let rows: Vec<_> = decode_vectors() .into_iter() .filter(|(s, ..)| *s == surface) @@ -340,4 +391,43 @@ mod tests { assert!(classes.contains(&"non-canonical-map-order")); assert!(classes.contains(&"non-canonical-vec-order")); } + + /// (i8) Genesis tranche G1 + /// (`spec/CONTRACT_GENESIS_G1_INSTRUMENT.md`): the `CreateInstrument` + /// envelope decode vector, pinned to a **literal byte array copied from + /// the committed corpus** (`spec/vectors/decode_vectors.txt`, + /// `ops.operation_envelope`/`create_instrument`) — not derived by calling + /// `.to_canonical_bytes()` here. `every_vector_gets_its_declared_verdict` + /// above checks `decode_vectors()`'s *own* output against `check`, which + /// cannot see a self-consistent encoder/decoder reorder: the 3b-i lesson + /// (`epiphany-core`'s `schema_major_3_tuning_context_wire_bytes_are_frozen`) + /// is that a swap applied identically to both halves passed 1283 tests + /// and 8/8 conformance, because every check in that failure mode compared + /// the live encoder against itself. Bytes written here by hand — as this + /// module's own `decode_vectors()` writes them, into the *committed* file + /// a future encoder change must move deliberately, in the diff — close + /// that hole for `CreateInstrument` specifically. + #[test] + fn create_instrument_envelope_decode_vector_is_pinned_to_literal_bytes() { + #[rustfmt::skip] + let bytes: Vec = vec![ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1f, 0x41, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x0c, 0x00, 0x00, + 0x00, 0x69, 0x6e, 0x73, 0x74, 0x72, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2d, 0x31, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x05, 0x08, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + 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/tests/text_projection_grammar.rs b/crates/epiphany-testkit/tests/text_projection_grammar.rs index 5a1672e..08e8e5f 100644 --- a/crates/epiphany-testkit/tests/text_projection_grammar.rs +++ b/crates/epiphany-testkit/tests/text_projection_grammar.rs @@ -301,10 +301,16 @@ fn the_kind_productions_are_the_operation_vocabulary() { // four bugs. .map(|t| t.catalog_name().to_string()) .collect(); + // The count is still hand-maintained, which is the same shape the comment + // 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`). 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(), - 31, - "30 payload-free kinds plus `Registered`" + 32, + "31 payload-free kinds plus `Registered`" ); let actual = alternatives("kind"); @@ -547,9 +553,16 @@ fn worked_example_header_is_the_implemented_companion_version() { .map(|(version, _)| version) }) .expect("the title page declares the companion version"); + // Pinned to the crate constant rather than a literal: the point of this + // assertion is that the spec's title page and the implemented version never + // drift apart, and a literal here made a companion bump edit two places by + // hand — exactly the "update only one of the two" failure the doc comment + // above warns about. Genesis G1 bumped 0.7.0 → 0.8.0 and tripped it. + let (major, minor, patch) = epiphany_textproj::COMPANION_VERSION; assert_eq!( - title_version, "0.7.0", - "this implementation targets exactly companion 0.7.0" + title_version, + format!("{major}.{minor}.{patch}"), + "the spec title page must declare the version this implementation targets" ); let expected = format!("(text-projection ({}))", title_version.replace('.', " ")); diff --git a/crates/epiphany-textproj/src/lib.rs b/crates/epiphany-textproj/src/lib.rs index c8bfa9e..478f353 100644 --- a/crates/epiphany-textproj/src/lib.rs +++ b/crates/epiphany-textproj/src/lib.rs @@ -19,7 +19,14 @@ use epiphany_ops::OperationEnvelope; /// /// A parser must reject every other version rather than migrating or /// normalizing it on read. -pub const COMPANION_VERSION: (u32, u32, u32) = (0, 7, 0); +/// +/// Bumped 0.7.0 → 0.8.0 by the genesis tranche G1, which appended +/// `create-instrument` to the `kind` production — the first operation kind +/// added since the header was gated to a single version. Extending the grammar +/// without moving this constant would leave two incompatible grammars both +/// claiming `(0 7 0)`. Cached projections do not migrate: a `TextProjection` +/// chunk is a non-canonical accelerator, so a stale one is regenerated. +pub const COMPANION_VERSION: (u32, u32, u32) = (0, 8, 0); /// A parsed canonical Text Projection document. /// diff --git a/crates/epiphany-textproj/src/parse.rs b/crates/epiphany-textproj/src/parse.rs index 1dac604..6051780 100644 --- a/crates/epiphany-textproj/src/parse.rs +++ b/crates/epiphany-textproj/src/parse.rs @@ -642,7 +642,11 @@ mod tests { out } - const HEADER: &str = "(text-projection (0 7 0))"; + // Bumped with `COMPANION_VERSION` (0.7.0 → 0.8.0, genesis G1). 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 8 0))"; const DOCUMENT: &str = "(document #x00000000000000000000000000000001)"; /// A minimal but complete valid projection: just the two mandatory lines. @@ -650,6 +654,19 @@ mod tests { projection(&[HEADER, DOCUMENT]) } + /// `HEADER` is a literal, so nothing but this test stops it drifting from + /// the version the crate actually implements. Without it a companion bump + /// leaves every fixture below silently rejected by the version gate, and + /// the failures point at section ordering rather than at the header. + #[test] + fn the_test_header_tracks_the_implemented_version() { + let (major, minor, patch) = COMPANION_VERSION; + assert_eq!( + HEADER, + format!("(text-projection ({major} {minor} {patch}))") + ); + } + /// A simple, independent (empty causal context) envelope, so several of /// these can be combined without any causal-order machinery beyond their /// HLC physical time. diff --git a/crates/epiphany-textproj/src/project.rs b/crates/epiphany-textproj/src/project.rs index b95685a..be1d64f 100644 --- a/crates/epiphany-textproj/src/project.rs +++ b/crates/epiphany-textproj/src/project.rs @@ -576,7 +576,11 @@ mod tests { #[test] fn header_matches_the_worked_example() { - assert_eq!(project_header().render(), "(text-projection (0 7 0))"); + let (major, minor, patch) = crate::COMPANION_VERSION; + assert_eq!( + project_header().render(), + format!("(text-projection ({major} {minor} {patch}))") + ); } #[test] diff --git a/crates/epiphany-textproj/src/vectors.rs b/crates/epiphany-textproj/src/vectors.rs index 4e7e66f..b7f9edc 100644 --- a/crates/epiphany-textproj/src/vectors.rs +++ b/crates/epiphany-textproj/src/vectors.rs @@ -322,16 +322,23 @@ pub fn document_vectors() -> Vec { .map(|(name, text)| (SURFACE, "accept", "-", *name, text.as_bytes().to_vec())) .collect(); + // The rejected version must be one this crate does NOT implement. Genesis + // G1 moved `COMPANION_VERSION` to 0.8.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.7.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 7 0))", "(text-projection (0 8 0))", + "(text-projection (0 7 0))", ); vectors.push(( SURFACE, "reject", "wrong-header-version", - "future_companion_version", + "superseded_companion_version", wrong_version.into_bytes(), )); diff --git a/spec/operation_catalog.pdf b/spec/operation_catalog.pdf index 593cbe9..6ceb537 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 c0f7554..0f2270c 100644 --- a/spec/operation_catalog.tex +++ b/spec/operation_catalog.tex @@ -1075,6 +1075,69 @@ anchor; there is no \texttt{DeleteStaff} in this catalogue revision --- an empty-only staff delete mirroring the container discipline is a later schema-fill). +\section{CreateInstrument} +\label{sec:k0:create-instrument} + +Ratified with the genesis operation tranche's first rung (G1; +\texttt{spec/CONTRACT\_GENESIS\_G1\_INSTRUMENT.md}, executing +\texttt{spec/RULING\_GENESIS\_PERSISTENCE.md}). \texttt{CreateStaff} +(Section~\ref{sec:k0:create-staff}) already requires a live +\texttt{Instrument}, and until this primitive existed nothing could mint one +except a canonical base --- so a document created empty +(\texttt{Score::empty}) and given only operations could reach a +\texttt{CreateStaff} but never satisfy it. \texttt{CreateInstrument} is the +single missing link between an empty score and a note: with it, the chain +\texttt{CreateInstrument} $\rightarrow$ \texttt{CreateStaff} $\rightarrow$ +\texttt{CreateRegion} $\rightarrow$ \texttt{CreateStaffInstance} +$\rightarrow$ \texttt{CreateVoice} $\rightarrow$ \texttt{InsertEvent} +materializes a note-bearing score from operations alone. + +\textbf{Payload schema.} \texttt{CreateInstrumentOp \{ instrument: Instrument +\}} --- the full abstract-instrument value (schema major 2): identity, name, +declared pitch range, abbreviation, sound configuration, transposition +interval, default clef, default staff-line configuration, and unpitched +members. Unlike every carried type in +Section~\ref{sec:k0:structural-containers} and +Section~\ref{sec:k0:create-staff}, \texttt{Instrument} holds no outbound +entity reference, so this primitive needs no referential precondition at +all --- only mint and idempotence. + +\textbf{Canonical encoding.} The length-framed canonical bytes of +\texttt{instrument}. \texttt{Instrument}'s schema-major-2 appends +(\texttt{sound\_config}, \texttt{default\_clef}, +\texttt{default\_staff\_lines}, and the rest) are mandatory fields, not +\texttt{Option}-hidden, so no lower-major layout for this payload exists: +under minimal stamping (Binary Format companion \sectionsc{Schema Major 2}) +a block carrying a \texttt{CreateInstrument} is \emph{born at v2}, exactly +as \texttt{CreateStaff} is --- not the value-dependent shape +\texttt{CreateRegion}/\texttt{SetStaffLayout} use. + +\textbf{Reduction rule.} Set-union creation of an \texttt{Instrument} on the +score root, mirroring \texttt{CreateStaff}'s discipline exactly since +neither carried type references another entity: a create mints the +instrument 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 \emph{differing} value is a precondition no-op with +\texttt{RecreateContentMismatch}; and a create naming a tombstoned id is a +precondition no-op with \texttt{TargetTombstoned}. There is no +graph-aware reference-resolution block, because 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 instrument; +\texttt{StrictInverse} conflicts if a live staff references it (tombstoning +it would strand the staff). + +\textbf{Re-anchoring.} Not applicable, for the same reason as +\texttt{CreateStaff}: an instrument mint references no tombstonable anchor. +There is no \texttt{DeleteInstrument} in this catalogue revision --- +\texttt{CreateStaff} ships today with no \texttt{DeleteStaff} either, and +full delete/modify coverage of the genesis-authored fields is later +tranche work (\texttt{spec/PLAN\_GENESIS\_OPS.md} G3). + \section{Repeat Structures} \label{sec:k0:repeat-structures} diff --git a/spec/text_projection.pdf b/spec/text_projection.pdf index 4c4e693..d0153f9 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 79c3bea..fb240cb 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.7.0 --- Canonical blob rejection and single-version header gating}\\[4pt] + {\normalsize\color{epiphanyink}Version 0.8.0 --- The genesis operation vocabulary reaches the grammar}\\[4pt] {\small\color{epiphanyslate}Normative for the text form it defines} \vfill \end{titlepage} @@ -467,7 +467,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 7 0)}, the version of the companion it implements. It + version: \texttt{(0 8 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 @@ -518,7 +518,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.7.0, neither a canonical operation nor canonical reduced state can + version~0.8.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} @@ -1041,6 +1041,7 @@ kind ::= "(insert-event " bytes " " value ")" | "(create-repeat-structure " value ")" | "(delete-repeat-structure " bytes ")" | "(transpose-interval (" bytes* ") " value ")" + | "(create-instrument " value ")" tuplet-comp ::= "not-in-tuplet" | "(replace-with-rest " value ")" | "(rewrite-tuplets (" bytes* "))" @@ -1117,7 +1118,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 7 0)) +(text-projection (0 8 0)) (document #x05050505050505050505050505050505) (profile full (0 1 0) (constraints 67108864 (retention 1 () true))) (canonical-base #x1f8b0000000000000000000000000000 #x 1 full (schema 0 1) #x0000) @@ -1305,6 +1306,25 @@ absorb it, exactly as the binary decoder does. version-keyed migration path rather than teaching the current parser to speculate. The worked example now uses the implemented header and contains only grammar-valid, canonical lines. \\ + \today & Chapter 5 & 0.8.0 --- The genesis operation vocabulary reaches the + grammar. The \texttt{kind} production gains + \texttt{"(create-instrument " value ")"}, the first operation kind appended + since the header was gated to a single version at 0.7.0 + (\texttt{req:textproj:operation-vocabulary}). + + The bump is forced rather than cosmetic. A parser \MUST{} accept exactly the + version of the companion it implements + (\texttt{req:textproj:header-version}), so extending the grammar while + holding the version would leave two mutually incompatible grammars both + claiming \texttt{(0 7 0)} --- an older parser meeting the new production + would fail with no version signal to explain why, which is the precise + failure the single-version gate exists to prevent. + + Cached projections at \texttt{(0 7 0)} do not migrate and are not expected + to. A \texttt{TextProjection} chunk is a non-canonical accelerator a writer + may discard, so a stale projection is regenerated from the canonical + document rather than converted. Multi-version acceptance and text + migrate-on-read remain deferred, unchanged. \\ \bottomrule \end{longtable} diff --git a/spec/vectors/decode_vectors.txt b/spec/vectors/decode_vectors.txt index eeda9b4..dae4efa 100644 --- a/spec/vectors/decode_vectors.txt +++ b/spec/vectors/decode_vectors.txt @@ -68,13 +68,18 @@ ops.operation_kind_tag accept - tag_27 1b ops.operation_kind_tag accept - tag_28 1c ops.operation_kind_tag accept - tag_29 1d ops.operation_kind_tag accept - tag_30 1e +ops.operation_kind_tag accept - tag_31 1f ops.operation_kind_tag accept - registered 1000000000000000000123456789abcdef -ops.operation_kind_tag reject unknown-discriminant tag_31_one_past_the_vocabulary 1f +ops.operation_kind_tag reject unknown-discriminant tag_32_one_past_the_vocabulary 20 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 ops.operation_kind_tag reject truncated registered_one_byte_short 10000000000000000000000000000000 +# ops.operation_envelope +ops.operation_envelope accept - create_instrument 000000000000000100000000000000010000000000000000000000000000000001000000000000000100000000000000000000010000000000000001000000000000000000001f4100000010000000000000000000000100000000000000010c000000696e737472756d656e742d31000000000000000002000508000000000000000000f03f000000000000 +ops.operation_envelope reject trailing-bytes create_instrument_trailing 000000000000000100000000000000010000000000000000000000000000000001000000000000000100000000000000000000010000000000000001000000000000000000001f4100000010000000000000000000000100000000000000010c000000696e737472756d656e742d31000000000000000002000508000000000000000000f03f00000000000000 + # bundle.manifest bundle.manifest accept - empty_manifest 6f9e7d11689ab113c4a1f05faf60fe60050505050505050505050505050505050000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000001000000000000000000000400000000010000000001000000000000 bundle.manifest accept - one_operation_root ae38d9cd408df9b59917c43a82c9d1880505050505050505050505050505050500000000000000000001000000111111111111111111111111111111111111111111111111111111111111111100000001004002000000000000400000000000000040000000000000000000111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000100000000000000000000000000000000000000000000000000000001000000000000000000000400000000010000000001000000000000 diff --git a/spec/vectors/textproj_document_vectors.txt b/spec/vectors/textproj_document_vectors.txt index 4cbf39b..57464bf 100644 --- a/spec/vectors/textproj_document_vectors.txt +++ b/spec/vectors/textproj_document_vectors.txt @@ -22,16 +22,16 @@ # document bytes are normative. `` is lowercase with no separators. # textproj.document -textproj.document accept - minimal 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783031303130313031303130313031303130313031303130313031303130313031290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a -textproj.document accept - lineage_custom_profile 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783032303230323032303230323032303230323032303230323032303230323032290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a -textproj.document accept - extension_base_two_envelopes 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783033303330333033303330333033303330333033303330333033303330333033290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a -textproj.document accept - rich_document 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783034303430343034303430343034303430343034303430343034303430343034290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a -textproj.document reject wrong-header-version future_companion_version 28746578742d70726f6a656374696f6e2028302038203029290a28646f63756d656e742023783031303130313031303130313031303130313031303130313031303130313031290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a -textproj.document reject blob-line unreferenced_blob 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783031303130313031303130313031303130313031303130313031303130313031290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28626c6f6220226170706c69636174696f6e2f6f637465742d73747265616d222028292023783031290a -textproj.document reject out-of-order-sections canonical_base_before_extension 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783033303330333033303330333033303330333033303330333033303330333033290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a -textproj.document reject repeated-singular-section lineage_repeated 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783032303230323032303230323032303230323032303230323032303230323032290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a -textproj.document reject operation-envelope-order envelopes_reversed 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783033303330333033303330333033303330333033303330333033303330333033290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a -textproj.document reject profile-declaration-order profiles_reversed 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783032303230323032303230323032303230323032303230323032303230323032290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a -textproj.document reject extension-declaration-order extensions_reversed 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783034303430343034303430343034303430343034303430343034303430343034290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a -textproj.document reject extension-chunk-order extension_chunks_reversed 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783033303330333033303330333033303330333033303330333033303330333033290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a -textproj.document reject missing-trailing-lf final_lf_missing 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783031303130313031303130313031303130313031303130313031303130313031290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e20312028292074727565292929 +textproj.document accept - minimal 28746578742d70726f6a656374696f6e2028302038203029290a28646f63756d656e742023783031303130313031303130313031303130313031303130313031303130313031290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a +textproj.document accept - lineage_custom_profile 28746578742d70726f6a656374696f6e2028302038203029290a28646f63756d656e742023783032303230323032303230323032303230323032303230323032303230323032290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a +textproj.document accept - extension_base_two_envelopes 28746578742d70726f6a656374696f6e2028302038203029290a28646f63756d656e742023783033303330333033303330333033303330333033303330333033303330333033290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a +textproj.document accept - rich_document 28746578742d70726f6a656374696f6e2028302038203029290a28646f63756d656e742023783034303430343034303430343034303430343034303430343034303430343034290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a +textproj.document reject wrong-header-version superseded_companion_version 28746578742d70726f6a656374696f6e2028302037203029290a28646f63756d656e742023783031303130313031303130313031303130313031303130313031303130313031290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a +textproj.document reject blob-line unreferenced_blob 28746578742d70726f6a656374696f6e2028302038203029290a28646f63756d656e742023783031303130313031303130313031303130313031303130313031303130313031290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28626c6f6220226170706c69636174696f6e2f6f637465742d73747265616d222028292023783031290a +textproj.document reject out-of-order-sections canonical_base_before_extension 28746578742d70726f6a656374696f6e2028302038203029290a28646f63756d656e742023783033303330333033303330333033303330333033303330333033303330333033290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a +textproj.document reject repeated-singular-section lineage_repeated 28746578742d70726f6a656374696f6e2028302038203029290a28646f63756d656e742023783032303230323032303230323032303230323032303230323032303230323032290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a +textproj.document reject operation-envelope-order envelopes_reversed 28746578742d70726f6a656374696f6e2028302038203029290a28646f63756d656e742023783033303330333033303330333033303330333033303330333033303330333033290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a +textproj.document reject profile-declaration-order profiles_reversed 28746578742d70726f6a656374696f6e2028302038203029290a28646f63756d656e742023783032303230323032303230323032303230323032303230323032303230323032290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a +textproj.document reject extension-declaration-order extensions_reversed 28746578742d70726f6a656374696f6e2028302038203029290a28646f63756d656e742023783034303430343034303430343034303430343034303430343034303430343034290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a +textproj.document reject extension-chunk-order extension_chunks_reversed 28746578742d70726f6a656374696f6e2028302038203029290a28646f63756d656e742023783033303330333033303330333033303330333033303330333033303330333033290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a +textproj.document reject missing-trailing-lf final_lf_missing 28746578742d70726f6a656374696f6e2028302038203029290a28646f63756d656e742023783031303130313031303130313031303130313031303130313031303130313031290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e20312028292074727565292929