diff --git a/crates/epiphany-core/src/codec.rs b/crates/epiphany-core/src/codec.rs index 971b985..d4392f1 100644 --- a/crates/epiphany-core/src/codec.rs +++ b/crates/epiphany-core/src/codec.rs @@ -3598,6 +3598,14 @@ canonical_value! { PartDefinition, AnalysisLayer, ViewDefinition, + // Genesis tranche G3b (`CONTRACT_GENESIS_G3B_MEASURE.md` pin 3) — + // CreateMeasure embeds the full value, mirroring CreateStaffInstance's + // `StaffInstance`. `Measure` already has a `Codec` (`struct_codec!` at + // `:1825`) and already ships inside `Score`; this makes that existing + // layout reachable per-value, same as every other entry here. No new wire + // layout, and no `textvalue_graph.rs` work — `struct_codec!` already + // generated `Measure`'s `TextValue` impl alongside its `Codec`. + Measure, } #[cfg(test)] @@ -3926,6 +3934,128 @@ mod tests { assert!(back.accidental_extensions.is_empty()); } + /// Genesis tranche G3b (`CONTRACT_GENESIS_G3B_MEASURE.md` pin 4, M12): + /// pins the exact frozen bytes of `Measure`'s `struct_codec!` wire form + /// (`id, start, time_signature, explicit_number, number_visibility`). + /// + /// The five field values are chosen so their encodings are MUTUALLY + /// DISTINCT in length (contract §3's `valuegen` trap, from G3a's + /// `analysis_layer` mistake where a same-width field pair made a + /// field-swap byte-invisible): `id` is a 20-byte id-leaf, `start` (a + /// `WallClock` anchor) is 13 bytes, `time_signature` (`Some`) is a + /// 21-byte id-leaf, `explicit_number` (`Some`) is a bare 5-byte `u32`, + /// and `number_visibility` is a 1-byte C-style enum tag. + /// + /// **Mutation:** reorder `struct_codec!(Measure { ... })`'s field list in + /// this file; must fail — a `struct_codec!` field reorder changes encode + /// and decode symmetrically and stays green under a round-trip-only + /// assertion, which is why this pins literal bytes instead. + #[test] + fn genesis_g3b_measure_wire_bytes_are_frozen() { + use crate::graph::{Measure, MeasureNumberVisibility}; + use crate::ids::{MeasureId, ReplicaId, TimeSignatureId}; + use crate::time::{TimeAnchor, WallClockTime}; + + fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() + } + + let measure = Measure { + id: MeasureId::new(ReplicaId(1), 2), + start: TimeAnchor::WallClock { + time: WallClockTime(3), + }, + time_signature: Some(TimeSignatureId::new(ReplicaId(1), 4)), + explicit_number: Some(5), + number_visibility: MeasureNumberVisibility::Always, + }; + let mut bytes = Vec::new(); + measure.enc(&mut bytes); + let expected = "100000000000000000000001000000000000000203080000000300000000000000011000000000000000000000010000000000000004010500000001"; + assert_eq!(hex(&bytes), expected, "the Measure wire form moved"); + assert!(!bytes.is_empty()); + + let decoded = Measure::dec(&mut Reader::new(&bytes)).expect("golden decodes"); + assert_eq!(decoded, measure); + } + + /// Genesis tranche G3b (M14): the strict per-value re-encode comparison + /// in `canonical_value!` is the ONLY thing standing between an + /// unreduced `RationalTime` spelling and acceptance. + /// + /// `Measure.start` carries an `Event`-anchored `Musical` offset whose + /// `RationalTime` is spelled UNREDUCED, `2/4`. `RationalTime::dec` ends in + /// `BigRational::new`, which reduces on construction, so the decoded + /// value is `1/2`; re-encoding it then emits `1/2`'s magnitudes, which + /// differ from the fed-in `2/4` bytes. A structurally-decodable but + /// NONCANONICAL encoding — exactly what the strict per-value check + /// (decode → `finish()` → re-encode → reject on mismatch) exists to + /// reject. + /// + /// **Mutation:** in `canonical_value!`, remove the + /// `v.canonical_bytes() != bytes` check (return `Ok(v)` unconditionally); + /// must fail. NOT "remove `Measure` from `canonical_value!`" — that does + /// not compile: the ops-crate envelope decode arm calls `value::`, + /// bound by `T: CanonicalValue`. + #[test] + fn genesis_g3b_measure_start_rejects_an_unreduced_rational_time() { + use crate::graph::{Measure, MeasureNumberVisibility}; + use crate::ids::{EventId, MeasureId, ReplicaId}; + use crate::time::{AnchorOffset, MusicalDuration, RationalTime, TimeAnchor}; + + // The canonical (reduced) 1/2 value this rung's fixture decodes to. + let reduced = Measure { + id: MeasureId::new(ReplicaId(1), 2), + start: TimeAnchor::Event { + id: EventId::new(ReplicaId(1), 3), + offset: AnchorOffset::Musical(MusicalDuration( + RationalTime::new(1, 2).expect("1/2 is representable"), + )), + }, + time_signature: None, + explicit_number: None, + number_visibility: MeasureNumberVisibility::Auto, + }; + let mut good_bytes = Vec::new(); + reduced.enc(&mut good_bytes); + + // The RationalTime wire pattern for 1/2 (Plus sign, 1-byte numerator + // magnitude `01`, 1-byte denominator magnitude `02`) and its + // UNREDUCED 2/4 counterpart — same byte length (11), so substituting + // one for the other does not disturb any surrounding length prefix. + let pattern_1_2: [u8; 11] = [1, 1, 0, 0, 0, 0x01, 1, 0, 0, 0, 0x02]; + let pattern_2_4: [u8; 11] = [1, 1, 0, 0, 0, 0x02, 1, 0, 0, 0, 0x04]; + + let occurrences = good_bytes + .windows(pattern_1_2.len()) + .filter(|w| *w == pattern_1_2) + .count(); + assert_eq!( + occurrences, 1, + "the 1/2 RationalTime pattern must appear exactly once, to substitute unambiguously" + ); + let at = good_bytes + .windows(pattern_1_2.len()) + .position(|w| w == pattern_1_2) + .unwrap(); + let mut bad_bytes = good_bytes.clone(); + bad_bytes[at..at + pattern_2_4.len()].copy_from_slice(&pattern_2_4); + assert_eq!(bad_bytes.len(), good_bytes.len()); + + // The reduced (canonical) bytes round-trip. + assert_eq!( + Measure::decode_canonical(&good_bytes).as_ref(), + Ok(&reduced) + ); + // The unreduced (2/4) bytes are structurally decodable (RationalTime + // itself never rejects an unreduced input — it normalizes), but the + // outer strict re-encode comparison MUST reject them. + assert!( + Measure::decode_canonical(&bad_bytes).is_err(), + "an unreduced RationalTime spelling must be rejected by the strict re-encode check" + ); + } + /// Genesis tranche G2b (`CONTRACT_GENESIS_G2B_TUNING.md` §1, pin/touch-row /// 3): `TuningContextSettings`'s canonical encoding is **byte-identical** /// to `ScoreTuningContext`'s existing five-field walk — a type-level diff --git a/crates/epiphany-core/src/graph.rs b/crates/epiphany-core/src/graph.rs index dbe7199..06e8bc7 100644 --- a/crates/epiphany-core/src/graph.rs +++ b/crates/epiphany-core/src/graph.rs @@ -584,6 +584,19 @@ impl Voice { /// A measure belonging to exactly one staff instance (Chapter 5 /// §"Staff-Based Content"). +/// +/// Genesis tranche G3b (`spec/CONTRACT_GENESIS_G3B_MEASURE.md`): `CreateMeasure` +/// is the sole authoring path, and it is append-only — a measure is always +/// pushed at the end of the owning [`StaffInstance::measures`], never +/// inserted. `time_signature: None` means *inherit* the effective grid's +/// active signature at this measure's start; that inherited meter still +/// governs boundary consistency (invariant 20's second clause) even though +/// `None` exempts a measure from the first clause (agreement). Invariant 20's +/// agreement and boundary checks ABSTAIN — emit no violation — where the +/// comparison or delta they need is not computable (contract pin 7): this is +/// deliberate, not a safety property, because base-ingested data may predate +/// the rule. Pickup/anacrusis (a partial first measure) is deferred +/// (P13-S19) and is never refused or flagged by this rung. #[derive(Clone, PartialEq, Eq, Debug)] pub struct Measure { pub id: MeasureId, @@ -607,7 +620,9 @@ pub struct StaffInstance { pub key_sequence: Vec, pub local_metric_grid: Option, /// Measures belonging to this instance, in order (per-staff; admits - /// polymeter). + /// polymeter). Append-only: `CreateMeasure` (genesis tranche G3b) always + /// pushes at the end, gated on the effective grid's agreement and + /// boundary-distance preconditions (contract pin 9). pub measures: Vec, pub instrument_override: Option, pub staff_lines_override: Option, diff --git a/crates/epiphany-editor-core/src/barriers.rs b/crates/epiphany-editor-core/src/barriers.rs index 10a5283..603da63 100644 --- a/crates/epiphany-editor-core/src/barriers.rs +++ b/crates/epiphany-editor-core/src/barriers.rs @@ -466,6 +466,17 @@ pub(crate) fn subjects_of(kind: &OperationKind, score: &Score) -> BarrierSubject OperationKind::CreateView(op) => { one(TypedObjectId::View(op.view_id()), EditContext::default()) } + // Genesis tranche G3b: `Measure` is a nested container child of a + // `StaffInstance` (not a root-level entity like the mints above), so + // — exactly like `CreateVoice` — it names the region/staff-instance + // context its parent resolves to. + OperationKind::CreateMeasure(op) => one( + TypedObjectId::Measure(op.measure_id()), + ctx( + region_of_staff_instance(score, op.instance), + Some(op.instance), + ), + ), 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 a3248c7..1f6ce84 100644 --- a/crates/epiphany-layout-ir/src/barrier.rs +++ b/crates/epiphany-layout-ir/src/barrier.rs @@ -1153,30 +1153,32 @@ mod tests { tag: 7 }) ); - // Operation-kind tag 39 is one past the vocabulary (the Phase-3 ops + // Operation-kind tag 40 is one past the vocabulary (the Phase-3 ops // tranche appended 24..=27, the repeat pair 28/29, `TransposeInterval` // 30, genesis G1's `CreateInstrument` 31, genesis G2a's // `SetCanvasLayoutDefaults` 32 and `SetSpellingPrecedence` 33, // genesis G2b's `SetTuningContext` 34, genesis G3a's // `CreateStaffGroup`/`CreatePartDefinition`/`CreateAnalysisLayer`/ - // `CreateView` 35..=38; encodings are append-only). + // `CreateView` 35..=38, genesis G3b's `CreateMeasure` 39; encodings + // are append-only). // // This assertion named 30 until Push 5 / P4, 31 until genesis G1, 32 - // until genesis G2a, 34 until genesis G2b, and 35 until genesis G3a — - // each time, by then, the number had become a real kind, so the test - // was pinning a bug: a barrier that prohibited the new operation - // encoded fine and would not read back. It must be bumped by every - // tranche that appends a tag, and it is deliberately a literal rather - // than `PAYLOAD_FREE.len()` so the bump is a conscious act. + // until genesis G2a, 34 until genesis G2b, 35 until genesis G3a, and + // 39 until genesis G3b — 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![39u8]])); + bytes.extend(set_blob(&[vec![40u8]])); bytes.push(0); assert_eq!( EditBarrier::decode_canonical_bytes(&bytes), Err(BarrierDecodeError::InvalidTag { kind: "OperationKindTag", - tag: 39 + tag: 40 }) ); } diff --git a/crates/epiphany-ops/src/decode.rs b/crates/epiphany-ops/src/decode.rs index 48a2796..ca3f16f 100644 --- a/crates/epiphany-ops/src/decode.rs +++ b/crates/epiphany-ops/src/decode.rs @@ -223,6 +223,14 @@ fn precondition_reason(reader: &mut Reader<'_>) -> Result Ok(PreconditionFailureReason::TempoMapMalformed), 12 => Ok(PreconditionFailureReason::SystemDerivedContentImmutable), 13 => Ok(PreconditionFailureReason::RecreateContentMismatch), + // Additive (Push 4a); a pre-existing decoder hole (P13-S20) — these + // two encode but could not decode until this rung. + 14 => Ok(PreconditionFailureReason::AcousticRealizationPinned), + 15 => Ok(PreconditionFailureReason::TranspositionOutOfRange), + // Additive (Genesis tranche G3b). + 16 => Ok(PreconditionFailureReason::MeasureMeterMismatch), + 17 => Ok(PreconditionFailureReason::MeasureOutOfOrder), + 18 => Ok(PreconditionFailureReason::MeasureOrderUnverifiable), tag => Err(MaterializedDecodeError::InvalidTag { kind: "PreconditionFailureReason", tag, @@ -618,10 +626,38 @@ mod tests { ReanchorReason::SameCanvasNearer ); // The vocabularies stay bounded: one past the append rejects. - assert!(exact(&[14], precondition_reason).is_err()); + assert!(exact(&[19], precondition_reason).is_err()); assert!(exact(&[7], reanchor_reason).is_err()); } + /// Genesis tranche G3b (contract row 12a / M64–M66): reasons 14 and 15 + /// (Push 4a's `AcousticRealizationPinned`/`TranspositionOutOfRange`) were + /// a pre-existing decoder hole — they encoded but could not decode + /// (P13-S20) — and this rung's own 16–18 must decode too. + #[test] + fn genesis_g3b_and_the_p13_s20_hole_decode() { + assert_eq!( + exact(&[14], precondition_reason).unwrap(), + PreconditionFailureReason::AcousticRealizationPinned + ); + assert_eq!( + exact(&[15], precondition_reason).unwrap(), + PreconditionFailureReason::TranspositionOutOfRange + ); + assert_eq!( + exact(&[16], precondition_reason).unwrap(), + PreconditionFailureReason::MeasureMeterMismatch + ); + assert_eq!( + exact(&[17], precondition_reason).unwrap(), + PreconditionFailureReason::MeasureOutOfOrder + ); + assert_eq!( + exact(&[18], precondition_reason).unwrap(), + PreconditionFailureReason::MeasureOrderUnverifiable + ); + } + #[test] fn decoder_rejects_truncation_and_trailing_bytes() { let bytes = MaterializedState::default().canonical_bytes(); diff --git a/crates/epiphany-ops/src/effect.rs b/crates/epiphany-ops/src/effect.rs index 43e5156..0ad643d 100644 --- a/crates/epiphany-ops/src/effect.rs +++ b/crates/epiphany-ops/src/effect.rs @@ -185,6 +185,20 @@ pub enum PreconditionFailureReason { /// past its `i8` bound (Push 4a). The frozen `Transpose` saturates here /// and reports success; this refuses. TranspositionOutOfRange, + /// A `CreateMeasure` (or a prospective preservation check) found a + /// resolving `time_signature` that disagrees with the effective grid's + /// active signature (genesis tranche G3b, contract pin 8b) — distinct + /// from the resolution failure, which is `TargetMissing`. + MeasureMeterMismatch, + /// A `CreateMeasure`'s carried `start` is comparable to the current last + /// measure's start (contract pin 6) and is not strictly after it (genesis + /// tranche G3b, contract pin 8b). + MeasureOutOfOrder, + /// A `CreateMeasure` ordering or boundary-distance check could not be + /// verified: the two starts are not comparable (contract pin 6), or the + /// delta between them is not computable (contract pin 6b) (genesis + /// tranche G3b, contract pin 8b). + MeasureOrderUnverifiable, } impl PreconditionFailureReason { @@ -210,6 +224,10 @@ impl PreconditionFailureReason { // Additive (Push 4a, TransposeInterval); appended past 13. PreconditionFailureReason::AcousticRealizationPinned => 14, PreconditionFailureReason::TranspositionOutOfRange => 15, + // Additive (Genesis tranche G3b); appended past 15. + PreconditionFailureReason::MeasureMeterMismatch => 16, + PreconditionFailureReason::MeasureOutOfOrder => 17, + PreconditionFailureReason::MeasureOrderUnverifiable => 18, } } } @@ -243,6 +261,10 @@ impl PreconditionFailureReason { // Minor 7 (Push 4a). PreconditionFailureReason::AcousticRealizationPinned => Some(7), PreconditionFailureReason::TranspositionOutOfRange => Some(7), + // Minor 12 (Genesis tranche G3b). + PreconditionFailureReason::MeasureMeterMismatch => Some(12), + PreconditionFailureReason::MeasureOutOfOrder => Some(12), + PreconditionFailureReason::MeasureOrderUnverifiable => Some(12), } } } @@ -512,6 +534,41 @@ mod tests { ); } + /// (M5/M6/M7, M8/M9/M10) Genesis tranche G3b: the three new + /// `PreconditionFailureReason` variants sit at discriminants 16, 17, 18 + /// (contract pin 8b), each at epoch `Some(12)`. + /// + /// **Mutation M5/M6/M7:** move any one discriminant to 19 (unused, so it + /// compiles); must fail. **Mutation M8/M9/M10:** move any one + /// `introduced_minor()` to `Some(11)`; must fail. + #[test] + fn genesis_g3b_reasons_are_16_17_18_at_epoch_12() { + assert_eq!( + PreconditionFailureReason::MeasureMeterMismatch.to_canonical_bytes(), + vec![16] + ); + assert_eq!( + PreconditionFailureReason::MeasureOutOfOrder.to_canonical_bytes(), + vec![17] + ); + assert_eq!( + PreconditionFailureReason::MeasureOrderUnverifiable.to_canonical_bytes(), + vec![18] + ); + assert_eq!( + PreconditionFailureReason::MeasureMeterMismatch.introduced_minor(), + Some(12) + ); + assert_eq!( + PreconditionFailureReason::MeasureOutOfOrder.introduced_minor(), + Some(12) + ); + assert_eq!( + PreconditionFailureReason::MeasureOrderUnverifiable.introduced_minor(), + Some(12) + ); + } + #[test] fn voice_promotion_repair_round_trips_shape() { let r = RepairRecord { diff --git a/crates/epiphany-ops/src/envdecode.rs b/crates/epiphany-ops/src/envdecode.rs index 01fe23b..268f39f 100644 --- a/crates/epiphany-ops/src/envdecode.rs +++ b/crates/epiphany-ops/src/envdecode.rs @@ -32,7 +32,7 @@ use std::collections::BTreeSet; use epiphany_core::{ - AnalysisLayer, CanvasLayoutDefaults, EventId, Instrument, InstrumentId, MetricGrid, + AnalysisLayer, CanvasLayoutDefaults, EventId, Instrument, InstrumentId, Measure, MetricGrid, MusicalPosition, OperationId, PartDefinition, PitchId, PitchSpelling, RegionId, RegionTimeModel, RepeatStructureId, ReplicaId, ScoreMetadata, SpellingPrecedence, Staff, StaffGroup, StaffInstance, StaffInstanceId, StaffLineConfiguration, TimeAnchor, TimeSignature, @@ -612,6 +612,10 @@ fn operation_kind(r: &mut Reader<'_>) -> Result { 38 => OperationKind::CreateView(CreateViewOp { view: value::(r, "ViewDefinition")?, }), + 39 => OperationKind::CreateMeasure(CreateMeasureOp { + instance: staff_instance_id(r)?, + measure: value::(r, "Measure")?, + }), tag => { return Err(EnvelopeDecodeError::InvalidTag { kind: "OperationKind", @@ -948,6 +952,16 @@ pub(crate) mod tests { ), }) } + OperationKindTag::CreateMeasure => { + OperationKind::CreateMeasure(crate::payload::CreateMeasureOp { + instance: si(), + measure: valuegen::measure( + epiphany_core::MeasureId::new(ReplicaId(7), 1), + epiphany_core::TimeSignatureId::new(ReplicaId(7), 1), + 1, + ), + }) + } } } diff --git a/crates/epiphany-ops/src/fuzz.rs b/crates/epiphany-ops/src/fuzz.rs index fa4b9a2..b22ae54 100644 --- a/crates/epiphany-ops/src/fuzz.rs +++ b/crates/epiphany-ops/src/fuzz.rs @@ -91,7 +91,7 @@ fn pitch(n: u64) -> PitchId { /// Generates a random payload over the shared id space. fn gen_payload(rng: &mut SplitMix64) -> OperationPayload { - let kind = match rng.below(36) { + let kind = match rng.below(37) { 0 => { let voice = VoiceId::new(ReplicaId(7), rng.below(3)); let position = MusicalPosition(RationalTime::from_int(rng.below(4) as i32)); @@ -329,6 +329,17 @@ fn gen_payload(rng: &mut SplitMix64) -> OperationPayload { vec![AnalysisLayerId::new(ReplicaId(7), rng.below(2))], ), }), + // Genesis tranche G3b: append a measure onto the shared staff-instance + // id space, over the shared measure/time-signature id spaces so + // mints/re-carries genuinely interact with them. + 36 => OperationKind::CreateMeasure(crate::payload::CreateMeasureOp { + instance: StaffInstanceId::new(ReplicaId(7), rng.below(3)), + measure: valuegen::measure( + epiphany_core::MeasureId::new(ReplicaId(7), rng.below(4)), + epiphany_core::TimeSignatureId::new(ReplicaId(7), rng.below(2)), + rng.below(4) as u32, + ), + }), _ => 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 9320123..192419f 100644 --- a/crates/epiphany-ops/src/lib.rs +++ b/crates/epiphany-ops/src/lib.rs @@ -125,17 +125,17 @@ pub use migrate::{migrate_v0_envelope, project_v1_to_v0, MigrationError}; pub use opset::{AcceptOutcome, OperationSet}; pub use payload::{ operation_block_introduced_minor, ChangeRegionTimeModelOp, CreateAnalysisLayerOp, - CreateCrossCuttingOp, CreateInstrumentOp, CreatePartDefinitionOp, CreateRegionOp, - CreateRepeatStructureOp, CreateStaffGroupOp, CreateStaffInstanceOp, CreateStaffOp, - CreateViewOp, CreateVoiceOp, CrossCuttingValue, DeleteCrossCuttingOp, DeleteEventOp, - DeleteIdentifiedPitchOp, DeleteRegionOp, DeleteRepeatStructureOp, DeleteStaffInstanceOp, - DeleteVoiceOp, InsertEventOp, InsertIdentifiedPitchOp, ModifyCrossCuttingOp, ModifyEventOp, - ModifyIdentifiedPitchOp, OperationKind, OperationKindTag, OperationPayload, PositionRemapping, - ResolveConflictPayload, ResolveEquivocationPayload, RespellPitchOp, SetCanvasLayoutDefaultsOp, - SetMetadataOp, SetMetricGridOp, SetSpellingPrecedenceOp, SetStaffLayoutOp, SetTempoSegmentOp, - SetTimeSignatureOp, SetTuningContextOp, SetUserPageBreakOp, SetUserSystemBreakOp, - TransactionCategory, TransactionDescriptor, TransposeIntervalOp, TransposeOp, - TupletCompensation, + CreateCrossCuttingOp, CreateInstrumentOp, CreateMeasureOp, CreatePartDefinitionOp, + CreateRegionOp, CreateRepeatStructureOp, CreateStaffGroupOp, CreateStaffInstanceOp, + CreateStaffOp, CreateViewOp, CreateVoiceOp, CrossCuttingValue, DeleteCrossCuttingOp, + DeleteEventOp, DeleteIdentifiedPitchOp, DeleteRegionOp, DeleteRepeatStructureOp, + DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, InsertIdentifiedPitchOp, + ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, OperationKind, OperationKindTag, + OperationPayload, PositionRemapping, ResolveConflictPayload, ResolveEquivocationPayload, + RespellPitchOp, SetCanvasLayoutDefaultsOp, SetMetadataOp, SetMetricGridOp, + SetSpellingPrecedenceOp, SetStaffLayoutOp, SetTempoSegmentOp, SetTimeSignatureOp, + SetTuningContextOp, 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 42c796f..cb4229f 100644 --- a/crates/epiphany-ops/src/migrate.rs +++ b/crates/epiphany-ops/src/migrate.rs @@ -197,6 +197,8 @@ fn project_kind(kind: &OperationKind) -> V0OperationKind { } OperationKind::CreateAnalysisLayer(op) => V0OperationKind::CreateAnalysisLayer(op.clone()), OperationKind::CreateView(op) => V0OperationKind::CreateView(op.clone()), + // Genesis tranche G3b: born past v0; projected verbatim. + OperationKind::CreateMeasure(op) => V0OperationKind::CreateMeasure(op.clone()), } } @@ -368,6 +370,8 @@ fn migrate_kind(kind: &V0OperationKind, context: &Score) -> Result OperationKind::CreateAnalysisLayer(op.clone()), V0OperationKind::CreateView(op) => OperationKind::CreateView(op.clone()), + // Genesis tranche G3b: identity round-trip (no lossy v0 form). + V0OperationKind::CreateMeasure(op) => OperationKind::CreateMeasure(op.clone()), }) } diff --git a/crates/epiphany-ops/src/payload.rs b/crates/epiphany-ops/src/payload.rs index 8a06c54..0f09771 100644 --- a/crates/epiphany-ops/src/payload.rs +++ b/crates/epiphany-ops/src/payload.rs @@ -33,13 +33,14 @@ use epiphany_core::{ AnalysisLayer, AnalysisLayerId, Beam, CanonicalValue, CanvasLayoutDefaults, Event, - EventDuration, EventId, EventPosition, IdentifiedPitch, Instrument, InstrumentId, MetricGrid, - MusicalDuration, MusicalPosition, OperationId, PartDefinition, PartDefinitionId, Pitch, - PitchId, PitchSpelling, Region, RegionId, RegionTimeModel, RepeatStructure, RepeatStructureId, - Rest, ScoreMetadata, Slur, Spanner, SpellingPrecedence, Staff, StaffGroup, StaffGroupId, - StaffId, StaffInstance, StaffInstanceId, StaffLineConfiguration, TempoSegment, Tie, TimeAnchor, - TimeSignature, TransactionId, TranspositionInterval, TuningContextSettings, TupletId, - TypedObjectId, ViewDefinition, ViewId, Voice, VoiceId, + EventDuration, EventId, EventPosition, IdentifiedPitch, Instrument, InstrumentId, Measure, + MeasureId, MetricGrid, MusicalDuration, MusicalPosition, OperationId, PartDefinition, + PartDefinitionId, Pitch, PitchId, PitchSpelling, Region, RegionId, RegionTimeModel, + RepeatStructure, RepeatStructureId, Rest, ScoreMetadata, Slur, Spanner, SpellingPrecedence, + Staff, StaffGroup, StaffGroupId, StaffId, StaffInstance, StaffInstanceId, + StaffLineConfiguration, TempoSegment, Tie, TimeAnchor, TimeSignature, TransactionId, + TranspositionInterval, TuningContextSettings, TupletId, TypedObjectId, ViewDefinition, ViewId, + Voice, VoiceId, }; use epiphany_determinism::{ sorted_canonical, CanonicalDecode, CanonicalEncode, CanonicalSet, DecodeError, @@ -294,6 +295,15 @@ pub enum OperationKind { /// reduction preconditions every carried active layer resolves to a live /// `AnalysisLayer`. CreateView(CreateViewOp), + // --- Genesis tranche G3b (`spec/CONTRACT_GENESIS_G3B_MEASURE.md`): the + // final rung of the genesis ladder. Discriminant extends additively past + // 38. --- + /// Append a measure to a live staff instance (append-only set-union + /// creation, contract pin 4/9). Carries the owning instance id beside the + /// full `Measure` value, mirroring `CreateStaffInstance`: `Measure` has no + /// back-pointer to its parent, so the parent must ride along in the + /// payload (and in the reducer's carried-value map, contract pin 5). + CreateMeasure(CreateMeasureOp), } impl OperationKind { @@ -415,6 +425,8 @@ impl OperationKind { OperationKind::CreatePartDefinition(_) => 36, OperationKind::CreateAnalysisLayer(_) => 37, OperationKind::CreateView(_) => 38, + // Genesis tranche G3b; appended past 38. + OperationKind::CreateMeasure(_) => 39, } } @@ -481,6 +493,9 @@ impl OperationKind { | OperationKind::CreatePartDefinition(_) | OperationKind::CreateAnalysisLayer(_) | OperationKind::CreateView(_) => Some(11), + // Minor 12 (Genesis tranche G3b), ratified + // `spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4. + OperationKind::CreateMeasure(_) => Some(12), } } @@ -536,6 +551,9 @@ impl OperationKind { OperationKind::CreatePartDefinition(_) => OperationKindTag::CreatePartDefinition, OperationKind::CreateAnalysisLayer(_) => OperationKindTag::CreateAnalysisLayer, OperationKind::CreateView(_) => OperationKindTag::CreateView, + // Genesis tranche G3b. Name-verbatim, as every genesis addition + // since G1 has been (contract pin 3's precedent). + OperationKind::CreateMeasure(_) => OperationKindTag::CreateMeasure, } } } @@ -586,6 +604,7 @@ impl CanonicalEncode for OperationKind { OperationKind::CreatePartDefinition(op) => op.encode_canonical(out), OperationKind::CreateAnalysisLayer(op) => op.encode_canonical(out), OperationKind::CreateView(op) => op.encode_canonical(out), + OperationKind::CreateMeasure(op) => op.encode_canonical(out), } } } @@ -650,6 +669,8 @@ pub enum OperationKindTag { CreateAnalysisLayer, /// Genesis tranche G3a. CreateView, + /// Genesis tranche G3b. + CreateMeasure, } /// The discriminant of [`OperationKindTag::Registered`], the one tag that @@ -769,6 +790,7 @@ operation_kind_tag_vocabulary! { CreatePartDefinition = 36 => "create-part-definition" @ Some(11), CreateAnalysisLayer = 37 => "create-analysis-layer" @ Some(11), CreateView = 38 => "create-view" @ Some(11), + CreateMeasure = 39 => "create-measure" @ Some(12), } impl CanonicalEncode for OperationKindTag { @@ -1856,6 +1878,39 @@ impl CanonicalEncode for CreateViewOp { } } +// --- Genesis tranche G3b (`spec/CONTRACT_GENESIS_G3B_MEASURE.md`): the final +// rung of the genesis ladder. `Measure` is a nested container child of +// `StaffInstance`, not a `Score`-root vector, so this rides +// `CreateStaffInstance`'s parent-carrying shape (contract pin 4), not the +// nine root-level mints' bare-value shape above. --- + +/// Append a `Measure` to a live `StaffInstance` (operation_catalog +/// §CreateMeasure). Carries the owning instance id beside the full `Measure` +/// value — `Measure` has no back-pointer to its parent (`graph.rs`), so the +/// parent must ride along, exactly like `CreateStaffInstanceOp::region`. +/// Append-only set-union creation (contract pin 9): the measure is pushed at +/// the end of `instance.measures`, gated on ordering, meter agreement, and +/// boundary-distance preconditions against the effective grid. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct CreateMeasureOp { + pub instance: StaffInstanceId, + pub measure: Measure, +} + +impl CreateMeasureOp { + /// The appended measure's identifier. + pub fn measure_id(&self) -> MeasureId { + self.measure.id + } +} + +impl CanonicalEncode for CreateMeasureOp { + fn encode_canonical(&self, out: &mut Vec) { + push_canon(out, &self.instance); + push_lp_bytes(out, &self.measure.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 @@ -2765,4 +2820,128 @@ mod tests { let rev = CrossCuttingValue::Slur(crate::valuegen::slur(s, b, a)); assert_ne!(fwd.to_canonical_bytes(), rev.to_canonical_bytes()); } + + // ----------------------------------------------------------------- + // Genesis tranche G3b (`spec/CONTRACT_GENESIS_G3B_MEASURE.md`). + // ----------------------------------------------------------------- + + fn g3b_sample_measure() -> Measure { + crate::valuegen::measure( + MeasureId::new(ReplicaId(1), 1), + epiphany_core::TimeSignatureId::new(ReplicaId(1), 1), + 1, + ) + } + + /// (M1/M2) `CreateMeasure`'s kind discriminant and tag discriminant are + /// both 39, in both discriminant spaces (contract pin 1). + /// + /// **Mutation M1:** move the kind discriminant to 40; must fail. + /// **Mutation M2:** move the tag discriminant to 40; must fail. (The two + /// spaces are asserted independently — a G-minor lesson from + /// `TransposeInterval`.) + #[test] + fn g3b_m1_m2_kind_and_tag_discriminant_are_39() { + let kind = OperationKind::CreateMeasure(CreateMeasureOp { + instance: StaffInstanceId::new(ReplicaId(1), 1), + measure: g3b_sample_measure(), + }); + let mut bytes = Vec::new(); + kind.encode_canonical(&mut bytes); + assert_eq!(bytes[0], 39, "the kind discriminant byte must lead"); + assert_eq!(kind.tag(), OperationKindTag::CreateMeasure); + assert_eq!(OperationKindTag::CreateMeasure.discriminant(), 39); + } + + /// (M3/M4) `CreateMeasure`'s epoch is `Some(12)` in BOTH the kind's own + /// `introduced_minor()` and the tag vocabulary's epoch. + /// + /// **Mutation M3:** change `OperationKind::introduced_minor()`'s + /// `CreateMeasure` arm to `Some(11)`; must fail. + /// **Mutation M4:** change the tag vocabulary's `@ Some(12)` to + /// `Some(11)`; must fail. + #[test] + fn g3b_m3_m4_epoch_is_12_in_both_spaces() { + let kind = OperationKind::CreateMeasure(CreateMeasureOp { + instance: StaffInstanceId::new(ReplicaId(1), 1), + measure: g3b_sample_measure(), + }); + assert_eq!(kind.introduced_minor(), Some(12)); + assert_eq!(OperationKindTag::CreateMeasure.introduced_minor(), Some(12)); + } + + /// (M11) `CreateMeasure` gains NO `schema_major()` arm (pin 2: `Measure` + /// is schema major 0, so `OperationEnvelopeBlock` stays at 3). A block + /// containing ONLY a `CreateMeasure` envelope stamps major 0 — the block + /// composition is load-bearing: a mixed block would stamp from another + /// kind and pass even with an errant arm present. + /// + /// **Mutation:** add `OperationKind::CreateMeasure(_) => 3` to + /// `schema_major()`; must fail. + #[test] + fn g3b_m11_create_measure_only_block_stamps_major_zero() { + let kind = OperationKind::CreateMeasure(CreateMeasureOp { + instance: StaffInstanceId::new(ReplicaId(1), 1), + measure: g3b_sample_measure(), + }); + assert_eq!( + kind.schema_major(), + 0, + "CreateMeasure falls through the catch-all _ => 0 arm" + ); + let envelopes = [envelope( + 1, + OperationPayload::Primitive(OperationKind::CreateMeasure(CreateMeasureOp { + instance: StaffInstanceId::new(ReplicaId(1), 1), + measure: g3b_sample_measure(), + })), + )]; + let major = envelopes + .iter() + .map(|e| e.schema_major()) + .max() + .expect("one envelope"); + assert_eq!( + major, 0, + "a block carrying only CreateMeasure stamps major 0" + ); + } + + /// (M13) `CreateMeasureOp::encode_canonical` pushes the parent + /// `StaffInstanceId` THEN the value — mirroring `CreateStaffInstanceOp` + /// (contract pin 4). Frozen literal bytes, not a round-trip assertion: + /// swapping the two fields' encode order round-trips fine on its own + /// (each half decodes to the right type), so only a byte literal catches + /// the reorder. + /// + /// **Mutation:** swap the two `push_canon`/`push_lp_bytes` calls in + /// `CreateMeasureOp::encode_canonical`; must fail. + #[test] + fn g3b_m13_create_measure_op_encodes_parent_then_value() { + let op = CreateMeasureOp { + instance: StaffInstanceId::new(ReplicaId(1), 7), + measure: g3b_sample_measure(), + }; + let mut bytes = Vec::new(); + op.encode_canonical(&mut bytes); + + // The instance id leads: its own canonical form (`push_canon`) is a + // fixed-width id encoding, and this exact prefix is what a + // parent/value swap would move. + let mut instance_bytes = Vec::new(); + push_canon(&mut instance_bytes, &op.instance); + assert!( + bytes.starts_with(&instance_bytes), + "the parent StaffInstanceId must be the first bytes encoded" + ); + + let mut value_bytes = Vec::new(); + push_lp_bytes(&mut value_bytes, &op.measure.canonical_bytes()); + assert_eq!( + bytes.len(), + instance_bytes.len() + value_bytes.len(), + "the payload is exactly parent-bytes then value-bytes, nothing else" + ); + assert_eq!(&bytes[instance_bytes.len()..], &value_bytes[..]); + } } diff --git a/crates/epiphany-ops/src/reduce.rs b/crates/epiphany-ops/src/reduce.rs index a040ea9..550b9ca 100644 --- a/crates/epiphany-ops/src/reduce.rs +++ b/crates/epiphany-ops/src/reduce.rs @@ -28,20 +28,21 @@ //! deferred to the Operation Catalog (§6.11); see `DECISIONS.md` for the exact //! boundary. -use std::cmp::Reverse; +use std::cmp::{Ordering, Reverse}; use std::collections::{BTreeMap, BTreeSet, BinaryHeap}; use epiphany_core::{ canonical_pitch_bytes, derive_promoted_voice_id, simplest_spelling, AnalysisLayer, AnalysisLayerId, AnchorOffset, AnnotationAnchor, CanonicalValue, CanvasLayoutDefaults, Event, - EventDuration, EventId, EventPosition, GestureAnchoring, Instrument, InstrumentId, MeterChange, - MetricGrid, MusicalDuration, MusicalPosition, OperationId, PartDefinition, PartDefinitionId, - Pitch, PitchId, PitchSpelling, RationalTime, RegionEdge, RegionId, RegionTimeModel, ReplicaId, - Score, ScoreMetadata, SpellingAttachment, SpellingDirective, SpellingPrecedence, SpellingScope, - SpellingSource, Staff, StaffGroup, StaffGroupId, StaffId, StaffInstance, StaffInstanceId, - StaffLineConfiguration, TempoMap, TempoSegment, TempoShape, TimeAnchor, TimeSignature, - TimeSignatureId, TransactionId, TransposeRefusal, TranspositionInterval, TuningContextSettings, - TypedObjectId, ViewDefinition, ViewId, Voice, VoiceId, VoiceOrigin, + EventDuration, EventId, EventPosition, GestureAnchoring, Instrument, InstrumentId, Measure, + MeasureId, MeasurePosition, MeterChange, MetricGrid, MusicalDuration, MusicalPosition, + OperationId, PartDefinition, PartDefinitionId, Pitch, PitchId, PitchSpelling, RationalTime, + RegionEdge, RegionId, RegionTimeModel, ReplicaId, Score, ScoreMetadata, SpellingAttachment, + SpellingDirective, SpellingPrecedence, SpellingScope, SpellingSource, Staff, StaffGroup, + StaffGroupId, StaffId, StaffInstance, StaffInstanceId, StaffLineConfiguration, TempoMap, + TempoSegment, TempoShape, TimeAnchor, TimeSignature, TimeSignatureId, TransactionId, + TransposeRefusal, TranspositionInterval, TuningContextSettings, TypedObjectId, ViewDefinition, + ViewId, Voice, VoiceId, VoiceOrigin, WallClockDuration, }; use epiphany_determinism::CanonicalEncode; @@ -58,10 +59,10 @@ use crate::envelope::OperationEnvelope; use crate::opset::OperationSet; use crate::payload::{ resolved_anchor_position, CreateAnalysisLayerOp, CreateCrossCuttingOp, CreateInstrumentOp, - CreatePartDefinitionOp, CreateRegionOp, CreateRepeatStructureOp, CreateStaffGroupOp, - CreateStaffInstanceOp, CreateStaffOp, CreateViewOp, CreateVoiceOp, CrossCuttingValue, - DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp, DeleteRegionOp, - DeleteRepeatStructureOp, DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, + CreateMeasureOp, CreatePartDefinitionOp, CreateRegionOp, CreateRepeatStructureOp, + CreateStaffGroupOp, CreateStaffInstanceOp, CreateStaffOp, CreateViewOp, CreateVoiceOp, + CrossCuttingValue, DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp, + DeleteRegionOp, DeleteRepeatStructureOp, DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, InsertIdentifiedPitchOp, ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, OperationKind, OperationPayload, RespellPitchOp, SetCanvasLayoutDefaultsOp, SetMetadataOp, SetMetricGridOp, SetSpellingPrecedenceOp, SetStaffLayoutOp, SetTempoSegmentOp, @@ -784,6 +785,42 @@ impl Predecessor { } } +/// Genesis tranche G3b (`spec/CONTRACT_GENESIS_G3B_MEASURE.md` pin 6c): how +/// recently (in canonical order) a chain wrote, for comparing two +/// INDEPENDENT chains' last writes (`metric_grid_chain` vs +/// `meter_change_chain`) — see [`Reducer::chain_recency`]. Declaration order +/// IS the intended `Ord`: a seeded-only base predates every real write, and +/// a prospective (not-yet-applied) write is always the latest. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] +enum Recency { + /// No recorded operational write — only a seeded base, or nothing. + Base, + /// A recorded write, keyed by its authoring operation's canonical + /// position. + Write(crate::stamp::StampTuple), + /// An in-flight, not-yet-applied prospective write (pin 6c, M31a) — + /// always the most recent. + Prospective, +} + +/// Genesis tranche G3b (`spec/CONTRACT_GENESIS_G3B_MEASURE.md` pin 6c): the +/// outcome of finding the governing element of a partially-ordered set under +/// [`Reducer::anchors_comparable_order`] — shared by the effective-grid +/// oracle's governing meter change and by `CreateMeasure`'s "current last +/// measure" lookup. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum GoverningElement { + /// No candidate exists (an empty, or wholly-filtered-away, set) — + /// vacuous, not a violation (pin 6c step 1). + None, + /// Exactly one candidate is the unique maximum (pin 6c step 2). + Unique(T), + /// Either some candidate is incomparable to the reference point (step 0), + /// or multiple maxima are mutually incomparable to each other (step 3) — + /// the caller must refuse/abstain, never guess. + Indeterminate, +} + /// The per-key outcome of undoing a transaction's write chain. enum ChainUndoVerdict { /// The transaction never wrote this key. @@ -1016,6 +1053,24 @@ struct Reducer<'a> { part_definition_values: BTreeMap, analysis_layer_values: BTreeMap, view_values: BTreeMap, + // Genesis tranche G3b (`CONTRACT_GENESIS_G3B_MEASURE.md` pin 5): carried + // values of append-only-minted measures, WITH the owning instance — + // `Measure` has no back-pointer to its parent, so a map keyed by + // `MeasureId` alone could not tell the graph-removal arm which + // `StaffInstance.measures` to remove from, nor support a re-carry's + // parent-mismatch check (a re-carry naming a *different* parent under an + // otherwise-identical measure is `RecreateContentMismatch`, not + // `AlreadyApplied` — the parent is part of identity here). + measure_values: BTreeMap, + // Genesis tranche G3b (contract pin 6c, disposition A): whether a staff + // instance authored a local metric-grid override, distinguishing override + // from "never told us" — `StaffInstance.local_metric_grid` overrides the + // enclosing region's default, but nothing tracked that distinction + // base-free before this. `Some(None)` = an authored explicit-inherit; + // `Some(Some(grid))` = an authored override; absent = never told (mint + // only ever inserts, so a live instance is always present after it is + // minted or base-seeded). + instance_grid: 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 @@ -1121,6 +1176,8 @@ struct WorkingSnapshot { part_definition_values: BTreeMap, analysis_layer_values: BTreeMap, view_values: BTreeMap, + measure_values: BTreeMap, + instance_grid: BTreeMap>, structures: BTreeMap>, region_instances: BTreeMap>, instance_voices: BTreeMap>, @@ -1407,6 +1464,8 @@ impl<'a> Reducer<'a> { part_definition_values: BTreeMap::new(), analysis_layer_values: BTreeMap::new(), view_values: BTreeMap::new(), + measure_values: BTreeMap::new(), + instance_grid: BTreeMap::new(), structures: BTreeMap::new(), region_instances: BTreeMap::new(), instance_voices: BTreeMap::new(), @@ -1582,6 +1641,12 @@ impl<'a> Reducer<'a> { instance.staff_lines_override.clone(), instance.visible, )); + // Genesis tranche G3b (contract pin 6c, disposition A): record + // whether this instance authored a local grid override, so a + // later CreateMeasure can distinguish override from + // inheritance without consulting the graph (base-free parity). + self.instance_grid + .insert(instance.id, instance.local_metric_grid.clone()); let voice_set = self.instance_voices.entry(instance.id).or_default(); for voice in &instance.voices { voice_set.insert(voice.id); @@ -1589,6 +1654,14 @@ impl<'a> Reducer<'a> { for measure in &instance.measures { self.objects .insert(TypedObjectId::Measure(measure.id), ObjectState::Live); + // Genesis tranche G3b (contract pin 5): the carried value + // backs CreateMeasure's byte-identical-re-carry + // idempotence check against a *base* measure — without + // this, `TypedObjectId::Measure` liveness is seeded but + // there is nothing to compare a re-carry against (the G1 + // `instrument_values` hazard). + self.measure_values + .insert(measure.id, (instance.id, measure.clone())); } for voice in &instance.voices { self.objects @@ -2909,6 +2982,7 @@ impl<'a> Reducer<'a> { OperationKind::CreatePartDefinition(op) => self.create_part_definition(env, op), OperationKind::CreateAnalysisLayer(op) => self.create_analysis_layer(env, op), OperationKind::CreateView(op) => self.create_view(env, op), + OperationKind::CreateMeasure(op) => self.create_measure(env, op), }, OperationPayload::ResolveConflict(op) => self.resolve_conflict(env, op), OperationPayload::UndoTransaction(op) => self.undo_transaction(env, op), @@ -4073,6 +4147,12 @@ impl<'a> Reducer<'a> { self.instance_voices.entry(op.instance_id()).or_default(); self.instance_staff .insert(op.instance_id(), op.instance.staff); + // Genesis tranche G3b (contract pin 6c, disposition A): record the + // minted instance's local grid override (or its absence) — this is + // the ONLY site that ever writes `local_metric_grid`, so the ledger's + // single write happens here. + self.instance_grid + .insert(op.instance_id(), op.instance.local_metric_grid.clone()); // Seed the layout-advisory chain with the minted instance's fields, so // a later SetStaffLayout's chain-predecessor is the created state. self.staff_layout_chain @@ -4458,6 +4538,577 @@ impl<'a> Reducer<'a> { OperationEffect::Applied } + // --- Genesis tranche G3b (`spec/CONTRACT_GENESIS_G3B_MEASURE.md`): the + // final rung of the genesis ladder. `Measure` is a nested container + // child, so `create_measure` rides `CreateStaffInstance`'s parent- + // carrying mint shape (contract pin 4), not the nine root-level mints' + // bare-value shape above. The predicates below (pins 6/6b/6c) are shared, + // reusable functions — graph invariant 20 (packet 2) reuses every one of + // them; nothing here is inlined into `create_measure`. --------------- + + /// The region enclosing `instance`, from reducer state alone (never + /// `self.graph`) — so it resolves identically in graph-aware and + /// base-free reduction (contract pin 6c, M30b): `region_instances` is + /// maintained in both modes (by `create_region`/`create_staff_instance` + /// and by `seed_from_graph`). + fn region_of_instance(&self, instance: StaffInstanceId) -> Option { + self.region_instances + .iter() + .find(|(_, instances)| instances.contains(&instance)) + .map(|(region, _)| *region) + } + + /// Pin 6: two `AnchorOffset`s are comparable iff they are the same clock, + /// or at least one is `Zero` — read as the additive identity of whichever + /// clock it is compared against. `Musical` against `WallClock` is never + /// comparable (the deferred wall-clock/musical reconciliation). + fn comparable_offset_order(a: &AnchorOffset, b: &AnchorOffset) -> Option { + match (a, b) { + (AnchorOffset::Musical(x), AnchorOffset::Musical(y)) => Some(x.cmp(y)), + (AnchorOffset::WallClock(x), AnchorOffset::WallClock(y)) => Some(x.cmp(y)), + (AnchorOffset::Zero, AnchorOffset::Zero) => Some(Ordering::Equal), + (AnchorOffset::Zero, AnchorOffset::Musical(y)) => Some(MusicalDuration::zero().cmp(y)), + (AnchorOffset::Musical(x), AnchorOffset::Zero) => Some(x.cmp(&MusicalDuration::zero())), + (AnchorOffset::Zero, AnchorOffset::WallClock(y)) => Some(WallClockDuration(0).cmp(y)), + (AnchorOffset::WallClock(x), AnchorOffset::Zero) => Some(x.cmp(&WallClockDuration(0))), + (AnchorOffset::Musical(_), AnchorOffset::WallClock(_)) + | (AnchorOffset::WallClock(_), AnchorOffset::Musical(_)) => None, + } + } + + /// c3's "vector index" ordering between two DISTINCT measures, both + /// anchored via `Measure{pos: Start, off: Zero}` (contract pin 6, c3): + /// their relative position within the SAME `StaffInstance.measures`, or + /// `None` if they are not both found in one instance's live measures. + /// + /// Graph-aware mode reads the actual `Vec` (ground truth). Base-free mode + /// has no graph at all — but base-free reduction has no *base* either, so + /// every live measure of an instance was minted by a `CreateMeasure` + /// processed in canonical order, and `minted_by`'s `OperationId` ordering + /// IS the append order; no separate order ledger is needed (contract §2 + /// row 5 lists exactly `measure_values`/`instance_grid`, not a third). + fn measure_vector_order(&self, a: MeasureId, b: MeasureId) -> Option { + if let Some(score) = &self.graph { + for region in &score.canvas.regions { + for instance in region.staff_instances() { + let pos_a = instance.measures.iter().position(|m| m.id == a); + let pos_b = instance.measures.iter().position(|m| m.id == b); + if let (Some(pa), Some(pb)) = (pos_a, pos_b) { + return Some(pa.cmp(&pb)); + } + } + } + None + } else { + let oa = self.minted_by.get(&TypedObjectId::Measure(a))?; + let ob = self.minted_by.get(&TypedObjectId::Measure(b))?; + Some(oa.cmp(ob)) + } + } + + /// Pin 6: the comparable relation over `TimeAnchor`s, EXACTLY the five + /// shapes c1–c5. Everything else is NOT comparable, and no other + /// relation may be invented — pin 6's prohibition. The boundary selector + /// (`MeasurePosition`/`RegionEdge`) must be IDENTICAL; it is never + /// ordered (draft 3's `Start < End` was unsound and is not reproduced + /// here). + fn anchors_comparable_order(&self, a: &TimeAnchor, b: &TimeAnchor) -> Option { + match (a, b) { + // c1: same Event id. + ( + TimeAnchor::Event { id: ia, offset: oa }, + TimeAnchor::Event { id: ib, offset: ob }, + ) if ia == ib => Self::comparable_offset_order(oa, ob), + // c2/c3: Measure anchors. + ( + TimeAnchor::Measure { + id: ia, + position: pa, + offset: oa, + }, + TimeAnchor::Measure { + id: ib, + position: pb, + offset: ob, + }, + ) => { + if pa != pb { + return None; + } + if ia == ib { + // c2: same id and same pos. + return Self::comparable_offset_order(oa, ob); + } + // c3: distinct ids, restricted to pos: Start, off: Zero (load- + // bearing — a nonzero offset or an `End` position can carry a + // point past its neighbour, so the index no longer bounds + // it). + if *pa != MeasurePosition::Start + || !matches!(oa, AnchorOffset::Zero) + || !matches!(ob, AnchorOffset::Zero) + { + return None; + } + self.measure_vector_order(*ia, *ib) + } + // c4: same Region id and same edge. + ( + TimeAnchor::Region { + id: ia, + edge: ea, + offset: oa, + }, + TimeAnchor::Region { + id: ib, + edge: eb, + offset: ob, + }, + ) if ia == ib && ea == eb => Self::comparable_offset_order(oa, ob), + // c5: WallClock, no referent id. + (TimeAnchor::WallClock { time: ta }, TimeAnchor::WallClock { time: tb }) => { + Some(ta.cmp(tb)) + } + // Never Event<->Measure, never Measure<->Region, never two Events + // with different ids, never Musical against WallClock, and never + // across differing pos/edge selectors. + _ => None, + } + } + + /// Pin 6b: the musical delta `b - a`, computable ONLY in shape c1, c2, or + /// c4 with BOTH offsets in the `Musical` clock (or `Zero`, normalized to + /// `Musical(0)`), and only when the boundary selector is identical. c3 + /// supplies no delta at all (a vector index gives order, never distance). + /// `WallClock` deltas are not musical durations and are never returned. + fn anchor_musical_delta(&self, a: &TimeAnchor, b: &TimeAnchor) -> Option { + fn musical(o: &AnchorOffset) -> Option { + match o { + AnchorOffset::Musical(d) => Some(d.clone()), + AnchorOffset::Zero => Some(MusicalDuration::zero()), + AnchorOffset::WallClock(_) => None, + } + } + match (a, b) { + ( + TimeAnchor::Event { id: ia, offset: oa }, + TimeAnchor::Event { id: ib, offset: ob }, + ) if ia == ib => Some(musical(ob)? - musical(oa)?), + ( + TimeAnchor::Measure { + id: ia, + position: pa, + offset: oa, + }, + TimeAnchor::Measure { + id: ib, + position: pb, + offset: ob, + }, + ) if ia == ib && pa == pb => Some(musical(ob)? - musical(oa)?), + ( + TimeAnchor::Region { + id: ia, + edge: ea, + offset: oa, + }, + TimeAnchor::Region { + id: ib, + edge: eb, + offset: ob, + }, + ) if ia == ib && ea == eb => Some(musical(ob)? - musical(oa)?), + _ => None, + } + } + + /// The outcome of finding the governing element of a partially-ordered + /// set under [`Self::anchors_comparable_order`] (contract pin 6c steps + /// 0–3) — shared by the effective-grid oracle's governing meter change + /// AND `CreateMeasure`'s "current last measure" (an append-only sequence + /// poses exactly the same "find the unique maximum" problem). + fn unique_maximum<'x, T: Copy>( + &self, + candidates: impl IntoIterator, + ) -> GoverningElement { + let items: Vec<(T, &TimeAnchor)> = candidates.into_iter().collect(); + if items.is_empty() { + return GoverningElement::None; + } + let mut maximal: Vec = Vec::new(); + for (key, anchor) in &items { + let dominated = items.iter().any(|(_, other)| { + self.anchors_comparable_order(other, anchor) == Some(Ordering::Greater) + }); + if !dominated { + maximal.push(*key); + } + } + if maximal.len() == 1 { + GoverningElement::Unique(maximal[0]) + } else { + GoverningElement::Indeterminate + } + } + + /// Pin 6c steps 0–3: the governing element among `candidates` relative to + /// `reference` (e.g. a meter change's signature relative to a measure's + /// start). **Step 0 comes before any candidate set**: if ANY candidate's + /// anchor is incomparable to `reference`, the whole selection is + /// indeterminate — even when the not-after-filtered set would have been + /// empty (an incomparable change is unplaced, not absent, and it might + /// have governed). + fn governing_by_anchor<'x, T: Copy>( + &self, + reference: &TimeAnchor, + candidates: impl IntoIterator, + ) -> GoverningElement { + let mut not_after: Vec<(T, &TimeAnchor)> = Vec::new(); + for (key, anchor) in candidates { + match self.anchors_comparable_order(anchor, reference) { + None => return GoverningElement::Indeterminate, + Some(Ordering::Greater) => {} + Some(_) => not_after.push((key, anchor)), + } + } + self.unique_maximum(not_after) + } + + /// Pin 6c, disposition A: the effective metric grid for a staff + /// instance — its own local override when present, else the enclosing + /// region's whole-grid write layered with per-key meter-change writes, + /// in canonical write order (the whole-grid chain first, then the + /// per-key overlay — a later per-key write overrides an earlier + /// whole-grid write, and a later whole-grid write supersedes an earlier + /// per-key write). Consults ONLY `self.instance_grid`, + /// `self.metric_grid_chain`, and `self.meter_change_chain` — never + /// `self.graph` — so graph-aware and base-free reduction run the + /// IDENTICAL reconstruction (pin 6c, M30b). + /// + /// `grid_override`/`meter_change_overrides` let a caller ask "what would + /// the grid be under this PROSPECTIVE write", substituting a value the + /// corresponding chain does not (yet) hold — used to evaluate an + /// in-flight undo restoration without mutating the chains (pin 6c, + /// M31a). `None` (respectively, an absent key) means "no prospective + /// override; consult the chain". + fn effective_grid( + &self, + instance: StaffInstanceId, + region: Option, + grid_override: Option<&Option>, + meter_change_overrides: &BTreeMap>, + ) -> Vec { + if let Some(Some(local)) = self.instance_grid.get(&instance) { + return local.meter_sequence.clone(); + } + + let whole_chain = region.and_then(|region| self.metric_grid_chain.get(®ion)); + let whole_recency = self.chain_recency(whole_chain, grid_override.is_some()); + let whole_value: Option = match grid_override { + Some(over) => over.clone(), + None => whole_chain + .and_then(|chain| chain.current()) + .cloned() + .flatten(), + }; + let mut sequence: Vec = + whole_value.map(|g| g.meter_sequence).unwrap_or_default(); + + // Every key either chain could speak to: positions the whole grid + // itself names, every per-key chain entry for this region, and any + // prospective per-key override. + let mut keys: BTreeSet = sequence + .iter() + .map(|c| resolved_anchor_position(&c.anchor)) + .collect(); + if let Some(region) = region { + keys.extend( + self.meter_change_chain + .keys() + .filter(|(r, _)| *r == region) + .map(|(_, pos)| pos.clone()), + ); + } + keys.extend(meter_change_overrides.keys().cloned()); + + // Canonical write-order interleaving (pin 6c, M30c): at each key, + // whichever chain wrote MORE RECENTLY governs. A later whole-grid + // write supersedes an earlier per-key change at that position (so + // the whole grid's own content there stands, and any stale per-key + // value is ignored); a later per-key write overlays the whole grid. + // `metric_grid_chain` and `meter_change_chain` are independent — + // `set_metric_grid` never touches `meter_change_chain` — so this + // cannot be shortcut to "whole grid, then always overlay per-key". + for key in keys { + let per_key_chain = + region.and_then(|region| self.meter_change_chain.get(&(region, key.clone()))); + let per_key_is_prospective = meter_change_overrides.contains_key(&key); + let per_key_recency = self.chain_recency(per_key_chain, per_key_is_prospective); + + if per_key_recency > whole_recency { + let value: Option = if per_key_is_prospective { + meter_change_overrides.get(&key).cloned().flatten() + } else { + per_key_chain + .and_then(|chain| chain.current()) + .cloned() + .flatten() + }; + sequence.retain(|c| resolved_anchor_position(&c.anchor) != key); + if let Some(change) = value { + sequence.push(change); + } + } + // Else: the whole grid's own content at this position (already + // reflected in `sequence`, or absent) governs — the per-key + // write, if any, predates it and is superseded. + } + sequence + } + + /// The write-recency of a chain's last write, for comparing which of two + /// INDEPENDENT chains wrote more recently in canonical order (pin 6c, + /// M30c) — `(physical, logical, replica, counter)`, the same tuple + /// `canonical_reduction_order` sorts by. A chain with no recorded write + /// (only a seeded base, or absent) is [`Recency::Base`], which sorts + /// before every real write; `prospective` (an in-flight, not-yet-applied + /// restoration, pin 6c M31a) always sorts last. + fn chain_recency(&self, chain: Option<&WriteChain>, prospective: bool) -> Recency { + if prospective { + return Recency::Prospective; + } + match chain.and_then(|c| c.last_write()) { + Some(write) => self + .env_of(write.op) + .map(|env| Recency::Write(env.stamp.reduction_tuple())) + .unwrap_or(Recency::Base), + None => Recency::Base, + } + } + + /// Pin 6c: the effective grid's governing time signature at `start`, + /// from an already-reconstructed `sequence` ([`Self::effective_grid`]). + fn governing_time_signature( + &self, + sequence: &[MeterChange], + start: &TimeAnchor, + ) -> GoverningElement { + self.governing_by_anchor( + start, + sequence.iter().map(|c| (c.time_signature, &c.anchor)), + ) + } + + /// All LIVE measures of `instance`, from `measure_values` filtered + /// through `self.objects` (a retained-but-tombstoned entry must not + /// contribute — contract pin 10.4's retention discipline, applied here to + /// the ordering computation too). + fn live_measure_starts(&self, instance: StaffInstanceId) -> Vec<(MeasureId, &TimeAnchor)> { + self.measure_values + .iter() + .filter_map(|(id, (parent, measure))| { + if *parent != instance { + return None; + } + if !matches!( + self.objects.get(&TypedObjectId::Measure(*id)), + Some(ObjectState::Live) + ) { + return None; + } + Some((*id, &measure.start)) + }) + .collect() + } + + fn graph_create_measure(&mut self, instance: StaffInstanceId, measure: &Measure) { + let Some(score) = self.graph.as_mut() else { + return; + }; + for region in &mut score.canvas.regions { + if let Some(instances) = region.content.staff_instances_mut() { + if let Some(inst) = instances.iter_mut().find(|i| i.id == instance) { + inst.measures.push(measure.clone()); + return; + } + } + } + } + + /// Append-only set-union creation of a `Measure` onto a live + /// `StaffInstance` (operation_catalog §CreateMeasure, contract pins + /// 4/5/8/9). Fresh id mints; a byte-identical re-carry under the SAME + /// parent is idempotent; a differing value, OR the same value under a + /// DIFFERENT parent (pin 5 — the parent is part of identity), is a + /// precondition no-op; a tombstoned id refuses. + fn create_measure(&mut self, env: &OperationEnvelope, op: &CreateMeasureOp) -> OperationEffect { + fn unverifiable() -> OperationEffect { + OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::MeasureOrderUnverifiable, + }, + } + } + + // Pin 8.1: the parent StaffInstance must be live — ungated (both + // graph-aware and base-free reduction; a mint into a non-existent + // parent has nowhere to go even base-free). + if !matches!( + self.objects.get(&TypedObjectId::StaffInstance(op.instance)), + Some(ObjectState::Live) + ) { + return OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::TargetMissing, + }, + }; + } + + let mobj = TypedObjectId::Measure(op.measure_id()); + match self.objects.get(&mobj) { + Some(ObjectState::Live) => { + let identical = self + .measure_values + .get(&op.measure_id()) + .is_some_and(|(parent, known)| *parent == op.instance && known == &op.measure); + 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 => {} + } + + // Pins 8.2/8.3: reference-resolution preconditions are graph-aware + // only (base-free reduction has no universe to check against). + if self.graph.is_some() { + if let Some(sig) = op.measure.time_signature { + if !matches!( + self.objects.get(&TypedObjectId::TimeSignature(sig)), + Some(ObjectState::Live) + ) { + return OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::TargetMissing, + }, + }; + } + } + let start_resolves = match &op.measure.start { + TimeAnchor::Event { id, .. } => matches!( + self.objects.get(&TypedObjectId::Event(*id)), + Some(ObjectState::Live) + ), + TimeAnchor::Measure { id, .. } => matches!( + self.objects.get(&TypedObjectId::Measure(*id)), + Some(ObjectState::Live) + ), + TimeAnchor::Region { id, .. } => matches!( + self.objects.get(&TypedObjectId::Region(*id)), + Some(ObjectState::Live) + ), + TimeAnchor::WallClock { .. } => true, + }; + if !start_resolves { + return OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::TargetMissing, + }, + }; + } + } + + let region = self.region_of_instance(op.instance); + + // Pin 9 clauses 1 & 3: the current last live measure of this + // instance — vacuous for the first measure (pin 9's pickup + // deferral, P13-S19). + let predecessor: Option = + match self.unique_maximum(self.live_measure_starts(op.instance)) { + GoverningElement::None => None, + GoverningElement::Unique(id) => self + .measure_values + .get(&id) + .map(|(_, measure)| measure.start.clone()), + GoverningElement::Indeterminate => return unverifiable(), + }; + + // Pin 9 clause 1: ordering. + if let Some(prev_start) = &predecessor { + match self.anchors_comparable_order(&op.measure.start, prev_start) { + None => return unverifiable(), + Some(Ordering::Greater) => {} + Some(_) => { + return OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::MeasureOutOfOrder, + }, + }; + } + } + } + + // Pin 9 clause 2: agreement — ONLY when `time_signature` is `Some` + // (pin 9b: `None` avoids only this clause, not clause 3). + let sequence = self.effective_grid(op.instance, region, None, &BTreeMap::new()); + if let Some(sig) = op.measure.time_signature { + match self.governing_time_signature(&sequence, &op.measure.start) { + GoverningElement::Unique(active) if active == sig => {} + GoverningElement::Unique(_) | GoverningElement::None => { + return OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::MeasureMeterMismatch, + }, + }; + } + GoverningElement::Indeterminate => return unverifiable(), + } + } + + // Pin 9 clause 3: boundary distance — vacuous for the first measure. + if let Some(prev_start) = &predecessor { + let prev_duration: Option = + match self.governing_time_signature(&sequence, prev_start) { + GoverningElement::Unique(sig) => self + .time_signature_values + .get(&sig) + .map(|ts| ts.measure_duration().clone()), + GoverningElement::None | GoverningElement::Indeterminate => None, + }; + match ( + self.anchor_musical_delta(prev_start, &op.measure.start), + prev_duration, + ) { + (Some(delta), Some(expected)) if delta == expected => {} + (Some(_), Some(_)) => { + return OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::MeasureMeterMismatch, + }, + }; + } + _ => return unverifiable(), + } + } + + self.graph_create_measure(op.instance, &op.measure); + self.mint_container(env, mobj); + self.measure_values + .insert(op.measure_id(), (op.instance, op.measure.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 @@ -7954,6 +8605,8 @@ impl<'a> Reducer<'a> { part_definition_values: self.part_definition_values.clone(), analysis_layer_values: self.analysis_layer_values.clone(), view_values: self.view_values.clone(), + measure_values: self.measure_values.clone(), + instance_grid: self.instance_grid.clone(), structures: self.structures.clone(), region_instances: self.region_instances.clone(), instance_voices: self.instance_voices.clone(), @@ -7998,6 +8651,8 @@ impl<'a> Reducer<'a> { self.part_definition_values = s.part_definition_values; self.analysis_layer_values = s.analysis_layer_values; self.view_values = s.view_values; + self.measure_values = s.measure_values; + self.instance_grid = s.instance_grid; self.structures = s.structures; self.region_instances = s.region_instances; self.instance_voices = s.instance_voices; @@ -11512,6 +12167,15 @@ mod tests { // arm), and `MaterializedState` still embeds no `Score` field value // for any of the four carried types, so there remains no surface on // this type for a leak to appear on. + // + // Re-pinned again at genesis tranche G3b + // (`spec/CONTRACT_GENESIS_G3B_MEASURE.md`): `gen_payload` gained + // `CreateMeasure` (arm 36), and `rng.below(36)` became `below(37)` — + // the same reshuffle, same reasoning. `CreateMeasure` is schema major + // 0 unconditionally (pin 2: no `schema_major()` arm), and + // `MaterializedState` still embeds no `Score` field value for the + // carried `Measure`, so there remains no surface on this type for a + // leak to appear on. let mut rng = epiphany_determinism::fuzz::SplitMix64::new(0xBA5E); let envelopes = crate::fuzz::gen_envelope_set(&mut rng, 200); let mut set = OperationSet::new(); @@ -11521,7 +12185,7 @@ mod tests { let hex: String = digest.iter().map(|b| format!("{b:02x}")).collect(); assert_eq!( hex, - "0c60a686097819d2c91a65b5bce4ad09c9ce2e640760608124fc999f1470e839" + "aefd8ecd6df3abecb84229d5b77585dead9575b6f6554097bbd6907c7b0329d7" ); } @@ -16337,4 +17001,1383 @@ mod tests { entry, got {recreate_effect:?}" ); } + + // ========================================================================= + // Genesis tranche G3b (`spec/CONTRACT_GENESIS_G3B_MEASURE.md`): CreateMeasure, + // the comparable relation (pin 6), the musical delta (pin 6b), and the + // effective-grid oracle (pin 6c). + // ========================================================================= + + fn g3b_measure_env( + replica: u64, + counter: u64, + physical: i64, + instance: StaffInstanceId, + measure: Measure, + ) -> OperationEnvelope { + prim_env( + replica, + counter, + physical, + CausalContext::new(), + OperationKind::CreateMeasure(CreateMeasureOp { instance, measure }), + ) + } + + fn g3b_region_and_instance_envs( + replica: u64, + region: RegionId, + instance: StaffInstanceId, + staff: StaffId, + ) -> Vec { + vec![ + prim_env( + replica, + 0, + 0, + CausalContext::new(), + OperationKind::CreateRegion(CreateRegionOp { + region: crate::valuegen::region(region), + }), + ), + prim_env( + replica, + 1, + 1, + CausalContext::new(), + OperationKind::CreateStaffInstance(CreateStaffInstanceOp { + region, + instance: crate::valuegen::staff_instance(instance, staff), + }), + ), + ] + } + + fn g3b_wallclock_measure(id: MeasureId, nanos: i64) -> Measure { + Measure { + id, + start: TimeAnchor::WallClock { + time: WallClockTime(nanos), + }, + time_signature: None, + explicit_number: None, + number_visibility: epiphany_core::MeasureNumberVisibility::Auto, + } + } + + fn g3b_effect_of(state: &MaterializedState, id: OperationId) -> Option { + state + .effects + .iter() + .find(|(e, _)| *e == id) + .map(|(_, eff)| eff.clone()) + } + + /// (M16, M17, M18 base-free leg) `CreateMeasure`'s mint discipline: fresh + /// id mints; a byte-identical re-carry under the SAME parent is + /// idempotent (`AlreadyApplied`); a differing value under a live id is a + /// precondition no-op (`RecreateContentMismatch`, M16); the SAME value + /// under a DIFFERENT parent is ALSO `RecreateContentMismatch` — the + /// parent is part of identity (pin 5, M17); a dead parent refuses + /// (`TargetMissing`) base-free too (M18's ungated leg). + #[test] + fn g3b_create_measure_mint_discipline_base_free() { + let region = RegionId::new(ReplicaId(1), 1); + let instance_a = StaffInstanceId::new(ReplicaId(1), 2); + let instance_b = StaffInstanceId::new(ReplicaId(1), 3); + let staff = StaffId::new(ReplicaId(1), 4); + let mut envs = g3b_region_and_instance_envs(1, region, instance_a, staff); + envs.push(prim_env( + 1, + 2, + 2, + CausalContext::new(), + OperationKind::CreateStaffInstance(CreateStaffInstanceOp { + region, + instance: crate::valuegen::staff_instance(instance_b, staff), + }), + )); + + let measure_id = MeasureId::new(ReplicaId(1), 10); + let measure = g3b_wallclock_measure(measure_id, 1_000); + let mut differing = measure.clone(); + differing.explicit_number = Some(99); + + let create = g3b_measure_env(1, 10, 10, instance_a, measure.clone()); + let recarry_identical = g3b_measure_env(1, 11, 11, instance_a, measure.clone()); + let recarry_differing = g3b_measure_env(1, 12, 12, instance_a, differing); + let recarry_other_parent = g3b_measure_env(1, 13, 13, instance_b, measure); + let dead_parent = g3b_measure_env( + 1, + 14, + 14, + StaffInstanceId::new(ReplicaId(1), 999), + g3b_wallclock_measure(MeasureId::new(ReplicaId(1), 11), 2_000), + ); + + envs.extend([ + create.clone(), + recarry_identical.clone(), + recarry_differing.clone(), + recarry_other_parent.clone(), + dead_parent.clone(), + ]); + let mut set = OperationSet::new(); + set.accept_all(envs); + let state = set.reduce(); + + assert_eq!( + g3b_effect_of(&state, create.id), + Some(OperationEffect::Applied) + ); + assert_eq!( + g3b_effect_of(&state, recarry_identical.id), + Some(OperationEffect::NoOp { + reason: NoOpReason::AlreadyApplied + }) + ); + assert_eq!( + g3b_effect_of(&state, recarry_differing.id), + Some(OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::RecreateContentMismatch + } + }) + ); + assert_eq!( + g3b_effect_of(&state, recarry_other_parent.id), + Some(OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::RecreateContentMismatch + } + }), + "the parent is part of identity (pin 5): a re-carry under a different \ + parent must not be AlreadyApplied" + ); + assert_eq!( + g3b_effect_of(&state, dead_parent.id), + Some(OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::TargetMissing + } + }), + "a dead parent must refuse base-free too (pin 8.1, ungated)" + ); + } + + /// (M18 graph-aware leg) A dead parent refuses under a graph too. + #[test] + fn g3b_create_measure_dead_parent_graph_aware() { + let identity = IdentityContext::new(ReplicaId(1)); + let dead_parent = g3b_measure_env( + 1, + 0, + 0, + StaffInstanceId::new(ReplicaId(1), 999), + g3b_wallclock_measure(MeasureId::new(ReplicaId(1), 1), 1_000), + ); + let mut set = OperationSet::new(); + set.accept_all(vec![dead_parent.clone()]); + let out = reduce_operation_set_onto(&set, &Score::empty(identity)); + assert_eq!( + g3b_effect_of(&out.state, dead_parent.id), + Some(OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::TargetMissing + } + }) + ); + } + + /// (M19, M20 x3) Reference-resolution preconditions are graph-aware: + /// `time_signature: Some(id)` must resolve (M19); `start`'s referent must + /// resolve for each non-`WallClock` variant — `Event`, `Measure`, + /// `Region` (M20, three observations). + #[test] + fn g3b_create_measure_referential_preconditions_graph_aware() { + let identity = IdentityContext::new(ReplicaId(1)); + let region = RegionId::new(ReplicaId(1), 1); + let instance = StaffInstanceId::new(ReplicaId(1), 2); + let staff = StaffId::new(ReplicaId(1), 3); + let instrument = epiphany_core::InstrumentId::new(ReplicaId(1), 4); + // Graph-aware mode preconditions CreateStaffInstance on a live + // Staff, which in turn preconditions a live Instrument — mint both + // first so the parent instance genuinely becomes Live, otherwise + // every CreateMeasure below would refuse on pin 8.1 (parent + // liveness) before ever reaching pins 8.2/8.3. + let mut envs = vec![ + prim_env( + 1, + 0, + 0, + CausalContext::new(), + OperationKind::CreateInstrument(CreateInstrumentOp { + instrument: crate::valuegen::instrument(instrument), + }), + ), + prim_env( + 1, + 1, + 1, + CausalContext::new(), + OperationKind::CreateStaff(CreateStaffOp { + staff: crate::valuegen::staff(staff, instrument), + }), + ), + prim_env( + 1, + 2, + 2, + CausalContext::new(), + OperationKind::CreateRegion(CreateRegionOp { + region: crate::valuegen::region(region), + }), + ), + prim_env( + 1, + 3, + 3, + CausalContext::new(), + OperationKind::CreateStaffInstance(CreateStaffInstanceOp { + region, + instance: crate::valuegen::staff_instance(instance, staff), + }), + ), + ]; + + // M19: an unresolvable time_signature. + let bad_signature = g3b_measure_env( + 1, + 10, + 10, + instance, + Measure { + id: MeasureId::new(ReplicaId(1), 20), + start: TimeAnchor::WallClock { + time: WallClockTime(1), + }, + time_signature: Some(TimeSignatureId::new(ReplicaId(1), 999)), + explicit_number: None, + number_visibility: epiphany_core::MeasureNumberVisibility::Auto, + }, + ); + // M20 (Event): an unresolvable Event referent. + let bad_event = g3b_measure_env( + 1, + 11, + 11, + instance, + Measure { + id: MeasureId::new(ReplicaId(1), 21), + start: TimeAnchor::Event { + id: EventId::new(ReplicaId(1), 999), + offset: AnchorOffset::Zero, + }, + time_signature: None, + explicit_number: None, + number_visibility: epiphany_core::MeasureNumberVisibility::Auto, + }, + ); + // M20 (Measure): an unresolvable Measure referent. + let bad_measure_ref = g3b_measure_env( + 1, + 12, + 12, + instance, + Measure { + id: MeasureId::new(ReplicaId(1), 22), + start: TimeAnchor::Measure { + id: MeasureId::new(ReplicaId(1), 999), + position: MeasurePosition::Start, + offset: AnchorOffset::Zero, + }, + time_signature: None, + explicit_number: None, + number_visibility: epiphany_core::MeasureNumberVisibility::Auto, + }, + ); + // M20 (Region): an unresolvable Region referent. + let bad_region_ref = g3b_measure_env( + 1, + 13, + 13, + instance, + Measure { + id: MeasureId::new(ReplicaId(1), 23), + start: TimeAnchor::Region { + id: RegionId::new(ReplicaId(1), 999), + edge: RegionEdge::Start, + offset: AnchorOffset::Zero, + }, + time_signature: None, + explicit_number: None, + number_visibility: epiphany_core::MeasureNumberVisibility::Auto, + }, + ); + + envs.extend([ + bad_signature.clone(), + bad_event.clone(), + bad_measure_ref.clone(), + bad_region_ref.clone(), + ]); + let mut set = OperationSet::new(); + set.accept_all(envs); + let out = reduce_operation_set_onto(&set, &Score::empty(identity)); + + let target_missing = Some(OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::TargetMissing, + }, + }); + assert_eq!( + g3b_effect_of(&out.state, bad_signature.id), + target_missing, + "M19: an unresolvable time_signature must refuse graph-aware" + ); + assert_eq!( + g3b_effect_of(&out.state, bad_event.id), + target_missing, + "M20 (Event): an unresolvable start referent must refuse graph-aware" + ); + assert_eq!( + g3b_effect_of(&out.state, bad_measure_ref.id), + target_missing, + "M20 (Measure): an unresolvable start referent must refuse graph-aware" + ); + assert_eq!( + g3b_effect_of(&out.state, bad_region_ref.id), + target_missing, + "M20 (Region): an unresolvable start referent must refuse graph-aware" + ); + } + + /// (M15) A byte-identical re-carry against a *base*-seeded measure is + /// idempotent — a re-carry test that only ever reduces from empty cannot + /// see a missing `measure_values` base seed at all. + #[test] + fn g3b_create_measure_base_recarry_is_idempotent() { + let measure = g3b_wallclock_measure(MeasureId::new(ReplicaId(1), 1), 5_000); + let instance_id = StaffInstanceId::new(ReplicaId(1), 2); + let mut instance = + crate::valuegen::staff_instance(instance_id, StaffId::new(ReplicaId(1), 3)); + instance.measures.push(measure.clone()); + let mut region = crate::valuegen::region(RegionId::new(ReplicaId(1), 4)); + if let epiphany_core::RegionContent::StaffBased(content) = &mut region.content { + content.staff_instances.push(instance); + } else { + panic!("valuegen::region is staff-based"); + } + let mut base = Score::empty(IdentityContext::new(ReplicaId(1))); + base.canvas.regions.push(region); + + let recarry = g3b_measure_env(2, 0, 10, instance_id, measure); + let mut set = OperationSet::new(); + set.accept_all(vec![recarry.clone()]); + let out = reduce_operation_set_onto(&set, &base); + assert_eq!( + g3b_effect_of(&out.state, recarry.id), + Some(OperationEffect::NoOp { + reason: NoOpReason::AlreadyApplied + }), + "a base-seeded measure re-carried byte-identically must be AlreadyApplied \ + (pin 5 site 4 — the G1 instrument_values hazard)" + ); + } + + /// (M23–M26) Pin 6's comparable relation, exactly the five shapes; pin + /// 6b's musical delta. Exercised directly on a bare `Reducer` (a + /// white-box test of the shared, reusable predicates — invariant 20, + /// packet 2, reuses these same functions). + #[test] + fn g3b_comparable_offsets_and_anchor_shapes() { + let op_set = OperationSet::new(); + let mut r = Reducer::new(&op_set); + + let ev_a = EventId::new(ReplicaId(1), 1); + let ev_b = EventId::new(ReplicaId(1), 2); + let m_a = MeasureId::new(ReplicaId(1), 10); + let m_b = MeasureId::new(ReplicaId(1), 11); + let rg_a = RegionId::new(ReplicaId(1), 20); + let rg_b = RegionId::new(ReplicaId(1), 21); + + // c1: same Event id, comparable by offset. + let e1 = TimeAnchor::Event { + id: ev_a, + offset: AnchorOffset::Musical(MusicalDuration(RationalTime::from_int(1))), + }; + let e2 = TimeAnchor::Event { + id: ev_a, + offset: AnchorOffset::Musical(MusicalDuration(RationalTime::from_int(2))), + }; + assert_eq!(r.anchors_comparable_order(&e1, &e2), Some(Ordering::Less)); + assert_eq!( + r.anchor_musical_delta(&e1, &e2), + Some(MusicalDuration(RationalTime::from_int(1))) + ); + // Never two Events with different ids. + let e3 = TimeAnchor::Event { + id: ev_b, + offset: AnchorOffset::Musical(MusicalDuration(RationalTime::from_int(1))), + }; + assert_eq!(r.anchors_comparable_order(&e1, &e3), None); + + // M23: Musical vs WallClock offsets are never comparable, even under + // the same Event id. + let e_wc = TimeAnchor::Event { + id: ev_a, + offset: AnchorOffset::WallClock(WallClockDuration(5)), + }; + assert_eq!( + r.anchors_comparable_order(&e1, &e_wc), + None, + "M23: Musical against WallClock must never be comparable" + ); + + // c2: same Measure id AND same pos, comparable by offset. + let ms1 = TimeAnchor::Measure { + id: m_a, + position: MeasurePosition::Start, + offset: AnchorOffset::Zero, + }; + let ms2 = TimeAnchor::Measure { + id: m_a, + position: MeasurePosition::Start, + offset: AnchorOffset::Musical(MusicalDuration(RationalTime::from_int(3))), + }; + assert_eq!(r.anchors_comparable_order(&ms1, &ms2), Some(Ordering::Less)); + assert_eq!( + r.anchor_musical_delta(&ms1, &ms2), + Some(MusicalDuration(RationalTime::from_int(3))) + ); + // Same id but DIFFERING pos: never comparable (the selector must be + // identical — never ordered). + let ms_end = TimeAnchor::Measure { + id: m_a, + position: MeasurePosition::End, + offset: AnchorOffset::Zero, + }; + assert_eq!(r.anchors_comparable_order(&ms1, &ms_end), None); + assert_eq!(r.anchor_musical_delta(&ms1, &ms_end), None); + + // c3: distinct Measure ids, both Start+Zero, in the same instance's + // (base-free) mint order — via `minted_by` OperationId comparison. + let instance = StaffInstanceId::new(ReplicaId(1), 99); + r.objects + .insert(TypedObjectId::Measure(m_a), ObjectState::Live); + r.objects + .insert(TypedObjectId::Measure(m_b), ObjectState::Live); + r.minted_by.insert( + TypedObjectId::Measure(m_a), + OperationId::new(ReplicaId(1), 1), + ); + r.minted_by.insert( + TypedObjectId::Measure(m_b), + OperationId::new(ReplicaId(1), 2), + ); + let _ = instance; // c3 does not need the parent directly here; minted_by suffices. + let c3_a = TimeAnchor::Measure { + id: m_a, + position: MeasurePosition::Start, + offset: AnchorOffset::Zero, + }; + let c3_b = TimeAnchor::Measure { + id: m_b, + position: MeasurePosition::Start, + offset: AnchorOffset::Zero, + }; + assert_eq!( + r.anchors_comparable_order(&c3_a, &c3_b), + Some(Ordering::Less), + "c3: distinct measure ids, Start+Zero, order by mint sequence" + ); + assert_eq!( + r.anchor_musical_delta(&c3_a, &c3_b), + None, + "c3 supplies no delta — a vector index gives order, never distance" + ); + + // M24: widening c3 to admit a nonzero offset or an `End` position + // must NOT be comparable (a nonzero offset or End can carry the + // point past its neighbour). + let c3_nonzero = TimeAnchor::Measure { + id: m_b, + position: MeasurePosition::Start, + offset: AnchorOffset::Musical(MusicalDuration(RationalTime::from_int(1))), + }; + assert_eq!( + r.anchors_comparable_order(&c3_a, &c3_nonzero), + None, + "M24: a nonzero-offset measure anchor must not be c3-comparable" + ); + let c3_end = TimeAnchor::Measure { + id: m_b, + position: MeasurePosition::End, + offset: AnchorOffset::Zero, + }; + // (Same-pos guard already forces None here since c3_a is Start; this + // documents that End never qualifies for c3 even at Zero offset.) + assert_eq!(r.anchors_comparable_order(&c3_a, &c3_end), None); + + // c4: same Region id AND same edge, comparable by offset. + let rg1 = TimeAnchor::Region { + id: rg_a, + edge: RegionEdge::Start, + offset: AnchorOffset::Zero, + }; + let rg2 = TimeAnchor::Region { + id: rg_a, + edge: RegionEdge::Start, + offset: AnchorOffset::Musical(MusicalDuration(RationalTime::from_int(4))), + }; + assert_eq!(r.anchors_comparable_order(&rg1, &rg2), Some(Ordering::Less)); + assert_eq!( + r.anchor_musical_delta(&rg1, &rg2), + Some(MusicalDuration(RationalTime::from_int(4))) + ); + // Never Region<->Region with different ids. + let rg3 = TimeAnchor::Region { + id: rg_b, + edge: RegionEdge::Start, + offset: AnchorOffset::Zero, + }; + assert_eq!(r.anchors_comparable_order(&rg1, &rg3), None); + + // M25/M26: same Region id, DIFFERING edge (Start vs End) — never + // comparable, and never a delta, regardless of offset magnitude. + // Draft 3's `Start < End` ordering (and its offset subtraction) was + // unsound: with a nonzero offset the selector does not bound the + // point without the region's own (deferred) length. + let rg_start_100 = TimeAnchor::Region { + id: rg_a, + edge: RegionEdge::Start, + offset: AnchorOffset::Musical(MusicalDuration(RationalTime::from_int(100))), + }; + let rg_end_0 = TimeAnchor::Region { + id: rg_a, + edge: RegionEdge::End, + offset: AnchorOffset::Zero, + }; + assert_eq!( + r.anchors_comparable_order(&rg_start_100, &rg_end_0), + None, + "M25: Region{{Start, Musical(100)}} vs Region{{End, Zero}} must be unverifiable" + ); + assert_eq!( + r.anchor_musical_delta(&rg_start_100, &rg_end_0), + None, + "M26: the delta across differing edges must not be computable" + ); + + // Never Event<->Measure, never Measure<->Region. + assert_eq!(r.anchors_comparable_order(&e1, &ms1), None); + assert_eq!(r.anchors_comparable_order(&ms1, &rg1), None); + + // c5: WallClock, no referent id, comparable by time. + let wc1 = TimeAnchor::WallClock { + time: WallClockTime(1), + }; + let wc2 = TimeAnchor::WallClock { + time: WallClockTime(2), + }; + assert_eq!(r.anchors_comparable_order(&wc1, &wc2), Some(Ordering::Less)); + // c5 offers no offsets to subtract; WallClock deltas are never + // musical durations. + assert_eq!(r.anchor_musical_delta(&wc1, &wc2), None); + } + + fn g3b_region_anchor(region: RegionId, n: i32) -> TimeAnchor { + TimeAnchor::Region { + id: region, + edge: RegionEdge::Start, + offset: AnchorOffset::Musical(MusicalDuration(RationalTime::from_int(n))), + } + } + + /// (M27, M28, M29) Pin 6c steps 0–3, direct on a bare `Reducer`. + #[test] + fn g3b_grid_oracle_governing_selection() { + let op_set = OperationSet::new(); + let r = Reducer::new(&op_set); + let region = RegionId::new(ReplicaId(1), 1); + let sig_a = TimeSignatureId::new(ReplicaId(1), 10); + let sig_b = TimeSignatureId::new(ReplicaId(1), 11); + + // M29: a genuinely empty sequence is vacuous, not indeterminate. + assert_eq!( + r.governing_by_anchor( + &g3b_region_anchor(region, 0), + Vec::<(TimeSignatureId, &TimeAnchor)>::new() + ), + GoverningElement::None, + "M29: an empty candidate set must be vacuous" + ); + + // Step 0 / M28: a change incomparable to the reference makes the + // WHOLE selection indeterminate, even though the (comparable-only) + // candidate set is empty. Use an Event-anchored change against a + // Region-anchored reference (never comparable, c1 vs c4). + let incomparable_anchor = TimeAnchor::Event { + id: EventId::new(ReplicaId(1), 1), + offset: AnchorOffset::Zero, + }; + assert_eq!( + r.governing_by_anchor( + &g3b_region_anchor(region, 0), + vec![(sig_a, &incomparable_anchor)] + ), + GoverningElement::Indeterminate, + "M28: an incomparable change must be indeterminate, not vacuous \ + (it is unplaced, not absent, and might have governed)" + ); + + // A unique maximum not-after the reference. + let a0 = g3b_region_anchor(region, 0); + let a2 = g3b_region_anchor(region, 2); + let reference = g3b_region_anchor(region, 5); + assert_eq!( + r.governing_by_anchor(&reference, vec![(sig_a, &a0), (sig_b, &a2)]), + GoverningElement::Unique(sig_b), + "the later not-after candidate must govern" + ); + + // M27: two maxima, mutually incomparable to each other (but each + // individually comparable to the reference) — via one c4 (Region) + // and one c1 (Event) candidate that each happen to also be + // comparable to a WallClock reference... Simpler: two candidates at + // the exact SAME resolved point via different, mutually-incomparable + // shapes is hard to stage without a synthetic reference; instead + // stage the canonical "tie" case pin 6c step 3 names directly: two + // Region-anchored candidates at the SAME offset (so both are + // `Ordering::Equal` to each other — neither dominates the other, so + // neither is uniquely maximal) is not directly expressible with two + // *distinct* MeterChanges at hand here without a c3-style id split; + // use the simplest faithful staging: a WallClock reference cannot + // relate to a Region candidate, so instead compare two candidates + // that are each comparable to the reference via c4 but not to each + // other because they are anchored to two DIFFERENT regions that + // BOTH happen to be "comparable to the reference" only through an + // artificial helper — not expressible under pin 6's real shapes. + // The faithful staging pin 6c step 3 describes is two maxima tied at + // Equal: both anchors identical in content to each other but + // reported as two distinct entries (a duplicate write at the same + // key from two different signatures is the realistic shape). + let tie_a = g3b_region_anchor(region, 3); + let tie_b = g3b_region_anchor(region, 3); + assert_eq!( + r.governing_by_anchor(&reference, vec![(sig_a, &tie_a), (sig_b, &tie_b)]), + GoverningElement::Indeterminate, + "M27: tied (mutually non-dominating) maxima must be indeterminate, \ + never picked by document order" + ); + } + + /// (M30, M31) `instance_grid`'s base seed, and the local override + /// winning over the region default. + #[test] + fn g3b_grid_oracle_instance_override_wins_and_base_seed() { + let op_set = OperationSet::new(); + let mut r = Reducer::new(&op_set); + let instance = StaffInstanceId::new(ReplicaId(1), 1); + let region = RegionId::new(ReplicaId(1), 2); + let sig_local = TimeSignatureId::new(ReplicaId(1), 10); + let sig_region = TimeSignatureId::new(ReplicaId(1), 11); + + let local_grid = MetricGrid { + meter_sequence: vec![MeterChange { + anchor: g3b_region_anchor(region, 0), + time_signature: sig_local, + }], + }; + r.instance_grid.insert(instance, Some(local_grid.clone())); + + let mut whole_chain = WriteChain::new(); + whole_chain.seed(Some(MetricGrid { + meter_sequence: vec![MeterChange { + anchor: g3b_region_anchor(region, 0), + time_signature: sig_region, + }], + })); + r.metric_grid_chain.insert(region, whole_chain); + + assert_eq!( + r.effective_grid(instance, Some(region), None, &BTreeMap::new()), + local_grid.meter_sequence, + "M31: an authored local override must win over the region default" + ); + + // M30 (seed_from_graph): base-seeded, from-empty parity is exercised + // end-to-end by `g3b_create_measure_base_recarry_is_idempotent` + // and the `instance_grid` seeding in `seed_from_graph` (contract + // pin 6c, disposition A) — here confirmed directly: an instance + // with NO recorded local override (the seed never having run) falls + // through to the region default. + r.instance_grid.remove(&instance); + assert_eq!( + r.effective_grid(instance, Some(region), None, &BTreeMap::new()), + vec![MeterChange { + anchor: g3b_region_anchor(region, 0), + time_signature: sig_region, + }], + "with no instance_grid entry at all, the region default must govern" + ); + } + + /// (M30) `instance_grid`'s BASE seed (`seed_from_graph`), end-to-end: a + /// base Score whose `StaffInstance.local_metric_grid` is `Some(...)` + /// must be visible to a `CreateMeasure` reduced onto it, distinguishing + /// override from the region's (disagreeing) default — without this seed, + /// the override is invisible and the region default wins wrongly. + #[test] + fn g3b_grid_oracle_instance_grid_base_seed_end_to_end() { + let instance_id = StaffInstanceId::new(ReplicaId(1), 1); + let region_id = RegionId::new(ReplicaId(1), 2); + let sig_local = TimeSignatureId::new(ReplicaId(1), 10); + let sig_region = TimeSignatureId::new(ReplicaId(1), 11); + + let mut instance = + crate::valuegen::staff_instance(instance_id, StaffId::new(ReplicaId(1), 3)); + instance.local_metric_grid = Some(MetricGrid { + meter_sequence: vec![MeterChange { + anchor: g3b_region_anchor(region_id, 0), + time_signature: sig_local, + }], + }); + let mut region = crate::valuegen::region(region_id); + if let epiphany_core::RegionContent::StaffBased(content) = &mut region.content { + content.staff_instances.push(instance); + content.default_metric_grid = Some(MetricGrid { + meter_sequence: vec![MeterChange { + anchor: g3b_region_anchor(region_id, 0), + time_signature: sig_region, + }], + }); + } + let mut base = Score::empty(IdentityContext::new(ReplicaId(1))); + base.time_signatures + .push(crate::valuegen::time_signature(sig_local, 3)); + base.time_signatures + .push(crate::valuegen::time_signature(sig_region, 5)); + base.canvas.regions.push(region); + + // Agree with the LOCAL override (sig_local) — must Apply only if the + // base seed made the override visible; otherwise the region default + // (sig_region) governs and this disagrees. + let measure = g3b_measure_env( + 2, + 0, + 0, + instance_id, + Measure { + id: MeasureId::new(ReplicaId(1), 100), + start: g3b_region_anchor(region_id, 0), + time_signature: Some(sig_local), + explicit_number: None, + number_visibility: epiphany_core::MeasureNumberVisibility::Auto, + }, + ); + let mut set = OperationSet::new(); + set.accept_all(vec![measure.clone()]); + let out = reduce_operation_set_onto(&set, &base); + assert_eq!( + g3b_effect_of(&out.state, measure.id), + Some(OperationEffect::Applied), + "M30: the base-seeded local override must be visible, not the region default" + ); + } + + /// (M31a) A prospective grid/meter-change override — the grid a + /// restoration WOULD install — must govern the reconstruction, standing + /// in for the chain's own (older) recorded state without mutating it. + #[test] + fn g3b_grid_oracle_prospective_overrides_govern() { + let op_set = OperationSet::new(); + let mut r = Reducer::new(&op_set); + let region = RegionId::new(ReplicaId(1), 1); + let instance = StaffInstanceId::new(ReplicaId(1), 2); + let sig_current = TimeSignatureId::new(ReplicaId(1), 10); + let sig_prospective = TimeSignatureId::new(ReplicaId(1), 11); + + let mut whole_chain = WriteChain::new(); + whole_chain.seed(Some(MetricGrid { + meter_sequence: vec![MeterChange { + anchor: g3b_region_anchor(region, 0), + time_signature: sig_current, + }], + })); + r.metric_grid_chain.insert(region, whole_chain); + + let prospective_sequence = vec![MeterChange { + anchor: g3b_region_anchor(region, 0), + time_signature: sig_prospective, + }]; + let prospective_grid = Some(MetricGrid { + meter_sequence: prospective_sequence.clone(), + }); + let with_override = r.effective_grid( + instance, + Some(region), + Some(&prospective_grid), + &BTreeMap::new(), + ); + assert_eq!( + with_override, prospective_sequence, + "M31a: the prospective whole-grid override must govern the reconstruction" + ); + + // The chain itself is untouched — a second call with no override + // still sees the CURRENT (unrestored) state. + let without_override = r.effective_grid(instance, Some(region), None, &BTreeMap::new()); + assert_eq!( + without_override, + vec![MeterChange { + anchor: g3b_region_anchor(region, 0), + time_signature: sig_current, + }], + "a prospective override must not mutate the chain" + ); + + // Prospective per-key override, same idea. + let key = MusicalPosition(RationalTime::from_int(0)); + let mut overrides = BTreeMap::new(); + overrides.insert( + key, + Some(MeterChange { + anchor: g3b_region_anchor(region, 0), + time_signature: sig_prospective, + }), + ); + let with_key_override = r.effective_grid(instance, Some(region), None, &overrides); + assert_eq!( + with_key_override, + vec![MeterChange { + anchor: g3b_region_anchor(region, 0), + time_signature: sig_prospective, + }], + "M31a: a prospective per-key override must also govern" + ); + } + + /// (M30b) The effective-grid oracle consults ONLY the ledgers + /// (`instance_grid`, `metric_grid_chain`, `meter_change_chain`) — never + /// `self.graph` — so graph-aware and base-free reduction run the + /// IDENTICAL reconstruction. Proven by deliberately desynchronizing a + /// bare `Reducer`'s `graph` from its ledgers (something the real + /// reduction paths never do — `set_metric_grid`/`create_staff_instance` + /// always write both together) and confirming the oracle answers from + /// the ledger, not the graph. + #[test] + fn g3b_grid_oracle_never_reads_self_graph() { + let op_set = OperationSet::new(); + let mut r = Reducer::new(&op_set); + let region = RegionId::new(ReplicaId(1), 1); + let instance = StaffInstanceId::new(ReplicaId(1), 2); + let sig_ledger = TimeSignatureId::new(ReplicaId(1), 10); + let sig_graph_only = TimeSignatureId::new(ReplicaId(1), 11); + + let mut whole_chain = WriteChain::new(); + whole_chain.seed(Some(MetricGrid { + meter_sequence: vec![MeterChange { + anchor: g3b_region_anchor(region, 0), + time_signature: sig_ledger, + }], + })); + r.metric_grid_chain.insert(region, whole_chain); + + // A graph present, with a DIFFERENT (fictitious) default grid at the + // same region/position — graph-aware mode's presence alone must not + // change the answer. + let mut instance_value = + crate::valuegen::staff_instance(instance, StaffId::new(ReplicaId(1), 3)); + instance_value.local_metric_grid = None; + let mut region_value = crate::valuegen::region(region); + if let epiphany_core::RegionContent::StaffBased(content) = &mut region_value.content { + content.staff_instances.push(instance_value); + content.default_metric_grid = Some(MetricGrid { + meter_sequence: vec![MeterChange { + anchor: g3b_region_anchor(region, 0), + time_signature: sig_graph_only, + }], + }); + } + let mut score = Score::empty(IdentityContext::new(ReplicaId(1))); + score.canvas.regions.push(region_value); + r.graph = Some(score); + + assert_eq!( + r.effective_grid(instance, Some(region), None, &BTreeMap::new()), + vec![MeterChange { + anchor: g3b_region_anchor(region, 0), + time_signature: sig_ledger, + }], + "M30b: the oracle must answer from the ledger, never from self.graph, \ + even when a (deliberately desynchronized) graph is present" + ); + } + + /// (M30c) Canonical write-order interleaving between `metric_grid_chain` + /// and `meter_change_chain`, BOTH directions: a `SetTimeSignature` + /// **before** a later `SetMetricGrid` (the whole-grid write supersedes + /// the earlier per-key change), and **after** it (the per-key change + /// overlays). End-to-end through the real operations, so the test does + /// not encode its own assumptions about internal wiring. + #[test] + fn g3b_grid_oracle_canonical_write_order_both_interleavings() { + let region1 = RegionId::new(ReplicaId(1), 1); + let instance1 = StaffInstanceId::new(ReplicaId(1), 2); + let region2 = RegionId::new(ReplicaId(1), 3); + let instance2 = StaffInstanceId::new(ReplicaId(1), 4); + let staff = StaffId::new(ReplicaId(1), 5); + let sig_a = TimeSignatureId::new(ReplicaId(1), 10); + let sig_b = TimeSignatureId::new(ReplicaId(1), 11); + let sig_irrelevant = TimeSignatureId::new(ReplicaId(1), 12); + + fn ctx(seen_through: u64) -> CausalContext { + CausalContext::new().with_seen(ReplicaId(1), seen_through) + } + + let mut counter = 0u64; + let mut next = |kind: OperationKind, seen_through: u64| -> (OperationEnvelope, u64) { + let this_counter = counter; + let env = prim_env( + 1, + this_counter, + this_counter as i64, + ctx(seen_through), + kind, + ); + counter += 1; + (env, this_counter) + }; + + let mut envs = Vec::new(); + let (env, c) = next( + OperationKind::CreateRegion(CreateRegionOp { + region: crate::valuegen::region(region1), + }), + 0, + ); + envs.push(env); + let (env, c) = next( + OperationKind::CreateStaffInstance(CreateStaffInstanceOp { + region: region1, + instance: crate::valuegen::staff_instance(instance1, staff), + }), + c, + ); + envs.push(env); + let (env, c) = next( + OperationKind::CreateRegion(CreateRegionOp { + region: crate::valuegen::region(region2), + }), + c, + ); + envs.push(env); + let (env, c) = next( + OperationKind::CreateStaffInstance(CreateStaffInstanceOp { + region: region2, + instance: crate::valuegen::staff_instance(instance2, staff), + }), + c, + ); + envs.push(env); + // Mint sig_irrelevant, sig_a, sig_b once each, at throwaway positions + // in region1, before the interleaving scenarios begin. + let (env, c) = next( + OperationKind::SetTimeSignature(SetTimeSignatureOp { + region: region1, + anchor: g3b_region_anchor(region1, 999), + time_signature: Some(crate::valuegen::time_signature(sig_irrelevant, 3)), + }), + c, + ); + envs.push(env); + let (env, c) = next( + OperationKind::SetTimeSignature(SetTimeSignatureOp { + region: region1, + anchor: g3b_region_anchor(region1, 998), + time_signature: Some(crate::valuegen::time_signature(sig_a, 3)), + }), + c, + ); + envs.push(env); + let (env, c) = next( + OperationKind::SetTimeSignature(SetTimeSignatureOp { + region: region1, + anchor: g3b_region_anchor(region1, 997), + time_signature: Some(crate::valuegen::time_signature(sig_b, 3)), + }), + c, + ); + envs.push(env); + + // --- Scenario 1 (region1): SetTimeSignature(pos 0, sig_a) BEFORE a + // later SetMetricGrid(pos 0, sig_b) — the whole-grid write must + // supersede the earlier per-key change. + let (s1_set_time_sig, c) = next( + OperationKind::SetTimeSignature(SetTimeSignatureOp { + region: region1, + anchor: g3b_region_anchor(region1, 0), + time_signature: Some(crate::valuegen::time_signature(sig_a, 3)), + }), + c, + ); + let (s1_set_grid, c) = next( + OperationKind::SetMetricGrid(SetMetricGridOp { + region: region1, + grid: Some(MetricGrid { + meter_sequence: vec![MeterChange { + anchor: g3b_region_anchor(region1, 0), + time_signature: sig_b, + }], + }), + }), + c, + ); + let (s1_measure_expect_b, c) = next( + OperationKind::CreateMeasure(CreateMeasureOp { + instance: instance1, + measure: Measure { + id: MeasureId::new(ReplicaId(1), 100), + start: g3b_region_anchor(region1, 0), + time_signature: Some(sig_b), + explicit_number: None, + number_visibility: epiphany_core::MeasureNumberVisibility::Auto, + }, + }), + c, + ); + + // --- Scenario 2 (region2): SetMetricGrid(pos 0, sig_a) BEFORE a + // later SetTimeSignature(pos 0, sig_b) — the per-key change must + // overlay the (now-superseded) whole grid. + let (s2_set_grid, c) = next( + OperationKind::SetMetricGrid(SetMetricGridOp { + region: region2, + grid: Some(MetricGrid { + meter_sequence: vec![MeterChange { + anchor: g3b_region_anchor(region2, 0), + time_signature: sig_a, + }], + }), + }), + c, + ); + let (s2_set_time_sig, c) = next( + OperationKind::SetTimeSignature(SetTimeSignatureOp { + region: region2, + anchor: g3b_region_anchor(region2, 0), + time_signature: Some(crate::valuegen::time_signature(sig_b, 3)), + }), + c, + ); + let (s2_measure_expect_b, _c) = next( + OperationKind::CreateMeasure(CreateMeasureOp { + instance: instance2, + measure: Measure { + id: MeasureId::new(ReplicaId(1), 101), + start: g3b_region_anchor(region2, 0), + time_signature: Some(sig_b), + explicit_number: None, + number_visibility: epiphany_core::MeasureNumberVisibility::Auto, + }, + }), + c, + ); + + envs.extend([ + s1_set_time_sig, + s1_set_grid, + s1_measure_expect_b.clone(), + s2_set_grid, + s2_set_time_sig, + s2_measure_expect_b.clone(), + ]); + + let mut set = OperationSet::new(); + set.accept_all(envs); + let state = set.reduce(); + + assert_eq!( + g3b_effect_of(&state, s1_measure_expect_b.id), + Some(OperationEffect::Applied), + "scenario 1 (SetTimeSignature before SetMetricGrid): the LATER \ + whole-grid write (sig_b) must supersede the earlier per-key \ + change (sig_a)" + ); + assert_eq!( + g3b_effect_of(&state, s2_measure_expect_b.id), + Some(OperationEffect::Applied), + "scenario 2 (SetMetricGrid before SetTimeSignature): the LATER \ + per-key change (sig_b) must overlay the earlier whole-grid write \ + (sig_a)" + ); + } + + /// (M21, M22, M32, M33) `CreateMeasure`'s append-only ordering (pin 9 + /// clause 1), agreement (clause 2), and boundary-distance (clause 3) + /// checks — base-free, so only pin 8.1's ungated parent-liveness + /// precondition applies and referent resolution (pin 8.3) is skipped. + #[test] + fn g3b_create_measure_ordering_agreement_boundary() { + let region = RegionId::new(ReplicaId(1), 1); + let staff = StaffId::new(ReplicaId(1), 2); + let mut envs = + g3b_region_and_instance_envs(1, region, StaffInstanceId::new(ReplicaId(1), 3), staff); + + let sig_a = TimeSignatureId::new(ReplicaId(1), 10); + let sig_b = TimeSignatureId::new(ReplicaId(1), 11); + // sig_a's measure_duration is 3/4 (numerator 3 over 4). + let set_sig_a = prim_env( + 1, + 5, + 5, + CausalContext::new(), + OperationKind::SetTimeSignature(SetTimeSignatureOp { + region, + anchor: g3b_region_anchor(region, 0), + time_signature: Some(crate::valuegen::time_signature(sig_a, 3)), + }), + ); + let set_sig_b = prim_env( + 1, + 6, + 6, + CausalContext::new(), + OperationKind::SetTimeSignature(SetTimeSignatureOp { + region, + anchor: g3b_region_anchor(region, 900), + time_signature: Some(crate::valuegen::time_signature(sig_b, 5)), + }), + ); + envs.extend([set_sig_a.clone(), set_sig_b.clone()]); + + // A distinct StaffInstance per sub-scenario, all in the SAME region, + // so they all see the SAME effective grid (sig_a at position 0, + // duration 3/4) without interfering with each other's ordering. + let instances: Vec = (10..17) + .map(|n| StaffInstanceId::new(ReplicaId(1), n)) + .collect(); + for (n, id) in instances.iter().enumerate() { + envs.push(prim_env( + 1, + 20 + n as u64, + 20 + n as i64, + CausalContext::new(), + OperationKind::CreateStaffInstance(CreateStaffInstanceOp { + region, + instance: crate::valuegen::staff_instance(*id, staff), + }), + )); + } + + fn measure(id: u64, start: TimeAnchor, sig: Option) -> Measure { + Measure { + id: MeasureId::new(ReplicaId(1), id), + start, + time_signature: sig, + explicit_number: None, + number_visibility: epiphany_core::MeasureNumberVisibility::Auto, + } + } + + // Scenario 0: the first measure of instances[0] — clauses 1 & 3 are + // vacuous; agreement (clause 2) still applies and passes. + let first = g3b_measure_env( + 1, + 40, + 40, + instances[0], + measure(200, g3b_region_anchor(region, 0), Some(sig_a)), + ); + + // Scenario A (M21/M22): a second measure whose start is COMPARABLE + // to the predecessor but not strictly after it — MeasureOutOfOrder. + let base_first_a = g3b_measure_env( + 1, + 41, + 41, + instances[1], + measure(201, g3b_region_anchor(region, 0), Some(sig_a)), + ); + let reversed = g3b_measure_env( + 1, + 42, + 42, + instances[1], + measure(202, g3b_region_anchor(region, -1), None), + ); + + // Scenario B: a second measure INCOMPARABLE to the predecessor + // (different anchor shape entirely) — MeasureOrderUnverifiable. + let base_first_b = g3b_measure_env( + 1, + 43, + 43, + instances[2], + measure(203, g3b_region_anchor(region, 0), Some(sig_a)), + ); + let incomparable = g3b_measure_env( + 1, + 44, + 44, + instances[2], + measure( + 204, + TimeAnchor::Event { + id: EventId::new(ReplicaId(1), 999), + offset: AnchorOffset::Musical(MusicalDuration(RationalTime::from_int(1))), + }, + None, + ), + ); + + // Scenario C: correct order, correct distance (3/4), agreeing + // signature — Applied. + let base_first_c = g3b_measure_env( + 1, + 45, + 45, + instances[3], + measure(205, g3b_region_anchor(region, 0), Some(sig_a)), + ); + let correct = g3b_measure_env( + 1, + 46, + 46, + instances[3], + measure( + 206, + TimeAnchor::Region { + id: region, + edge: RegionEdge::Start, + offset: AnchorOffset::Musical(MusicalDuration( + RationalTime::new(3, 4).unwrap(), + )), + }, + Some(sig_a), + ), + ); + + // Scenario D (M32): correct order and distance, but a DISAGREEING + // signature — MeasureMeterMismatch. + let base_first_d = g3b_measure_env( + 1, + 47, + 47, + instances[4], + measure(207, g3b_region_anchor(region, 0), Some(sig_a)), + ); + let disagreeing = g3b_measure_env( + 1, + 48, + 48, + instances[4], + measure( + 208, + TimeAnchor::Region { + id: region, + edge: RegionEdge::Start, + offset: AnchorOffset::Musical(MusicalDuration( + RationalTime::new(3, 4).unwrap(), + )), + }, + Some(sig_b), + ), + ); + + // Scenario E (M33): correct order, agreeing (`None`, inherits), but + // the WRONG distance — MeasureMeterMismatch (the immediate-violation + // gap: comparable, correctly ordered, agreeing, and still wrong). + let base_first_e = g3b_measure_env( + 1, + 49, + 49, + instances[5], + measure(209, g3b_region_anchor(region, 0), Some(sig_a)), + ); + let wrong_distance = g3b_measure_env( + 1, + 50, + 50, + instances[5], + measure( + 210, + TimeAnchor::Region { + id: region, + edge: RegionEdge::Start, + offset: AnchorOffset::Musical(MusicalDuration(RationalTime::from_int(1))), + }, + None, + ), + ); + + envs.extend([ + first.clone(), + base_first_a.clone(), + reversed.clone(), + base_first_b.clone(), + incomparable.clone(), + base_first_c.clone(), + correct.clone(), + base_first_d.clone(), + disagreeing.clone(), + base_first_e.clone(), + wrong_distance.clone(), + ]); + + let mut set = OperationSet::new(); + set.accept_all(envs); + let state = set.reduce(); + + assert_eq!( + g3b_effect_of(&state, first.id), + Some(OperationEffect::Applied), + "the first measure of an instance: clauses 1 & 3 are vacuous" + ); + assert_eq!( + g3b_effect_of(&state, reversed.id), + Some(OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::MeasureOutOfOrder + } + }), + "M21/M22: a comparable-but-reversed start must be MeasureOutOfOrder \ + (not silently appended, and not the wrong reason)" + ); + assert_eq!( + g3b_effect_of(&state, incomparable.id), + Some(OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::MeasureOrderUnverifiable + } + }), + "an incomparable start must be MeasureOrderUnverifiable" + ); + assert_eq!( + g3b_effect_of(&state, correct.id), + Some(OperationEffect::Applied), + "correct order, correct distance, agreeing signature: Applied" + ); + assert_eq!( + g3b_effect_of(&state, disagreeing.id), + Some(OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::MeasureMeterMismatch + } + }), + "M32: a disagreeing signature must be MeasureMeterMismatch" + ); + assert_eq!( + g3b_effect_of(&state, wrong_distance.id), + Some(OperationEffect::NoOp { + reason: NoOpReason::PreconditionFailedUnderReduction { + reason: PreconditionFailureReason::MeasureMeterMismatch + } + }), + "M33: comparable, correctly ordered, agreeing (None inherits), and \ + STILL the wrong distance must be MeasureMeterMismatch — the \ + immediate-violation gap" + ); + } } diff --git a/crates/epiphany-ops/src/textproj_kind.rs b/crates/epiphany-ops/src/textproj_kind.rs index 18c59c4..a1a9fa9 100644 --- a/crates/epiphany-ops/src/textproj_kind.rs +++ b/crates/epiphany-ops/src/textproj_kind.rs @@ -9,15 +9,15 @@ use unicode_normalization::UnicodeNormalization; use crate::payload::{ ChangeRegionTimeModelOp, CreateAnalysisLayerOp, CreateCrossCuttingOp, CreateInstrumentOp, - CreatePartDefinitionOp, CreateRegionOp, CreateRepeatStructureOp, CreateStaffGroupOp, - CreateStaffInstanceOp, CreateStaffOp, CreateViewOp, CreateVoiceOp, DeleteCrossCuttingOp, - DeleteEventOp, DeleteIdentifiedPitchOp, DeleteRegionOp, DeleteRepeatStructureOp, - DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, InsertIdentifiedPitchOp, - ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, OperationKind, OperationKindTag, - RespellPitchOp, SetCanvasLayoutDefaultsOp, SetMetadataOp, SetMetricGridOp, - SetSpellingPrecedenceOp, SetStaffLayoutOp, SetTempoSegmentOp, SetTimeSignatureOp, - SetTuningContextOp, SetUserPageBreakOp, SetUserSystemBreakOp, TransactionDescriptor, - TransposeIntervalOp, TransposeOp, + CreateMeasureOp, CreatePartDefinitionOp, CreateRegionOp, CreateRepeatStructureOp, + CreateStaffGroupOp, CreateStaffInstanceOp, CreateStaffOp, CreateViewOp, CreateVoiceOp, + DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp, DeleteRegionOp, + DeleteRepeatStructureOp, DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, + InsertIdentifiedPitchOp, ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, + OperationKind, OperationKindTag, RespellPitchOp, SetCanvasLayoutDefaultsOp, SetMetadataOp, + SetMetricGridOp, SetSpellingPrecedenceOp, SetStaffLayoutOp, SetTempoSegmentOp, + SetTimeSignatureOp, SetTuningContextOp, SetUserPageBreakOp, SetUserSystemBreakOp, + TransactionDescriptor, TransposeIntervalOp, TransposeOp, }; use crate::support::OperationKindRegistryId; @@ -241,6 +241,10 @@ impl TextValue for OperationKind { production(self.tag(), vec![op.layer.project()]) } OperationKind::CreateView(op) => production(self.tag(), vec![op.view.project()]), + OperationKind::CreateMeasure(op) => production( + self.tag(), + vec![op.instance.project(), op.measure.project()], + ), } } @@ -618,6 +622,15 @@ impl TextValue for OperationKind { view: TextValue::parse(view)?, }) } + OperationKindTag::CreateMeasure => { + let [instance, measure] = fields(s, tag, 2)? else { + unreachable!("the arity-2 check returned two fields") + }; + OperationKind::CreateMeasure(CreateMeasureOp { + instance: TextValue::parse(instance)?, + measure: TextValue::parse(measure)?, + }) + } }) } } @@ -671,7 +684,7 @@ mod tests { #[test] fn every_operation_kind_round_trips_with_canonical_text() { let tags: Vec<_> = all_tags().collect(); - assert_eq!(tags.len(), 39, "the grammar has 39 kind productions"); + assert_eq!(tags.len(), 40, "the grammar has 40 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 7a65776..72d0421 100644 --- a/crates/epiphany-ops/src/v0.rs +++ b/crates/epiphany-ops/src/v0.rs @@ -123,6 +123,10 @@ pub enum V0OperationKind { CreatePartDefinition(crate::payload::CreatePartDefinitionOp), CreateAnalysisLayer(crate::payload::CreateAnalysisLayerOp), CreateView(crate::payload::CreateViewOp), + // Genesis tranche G3b — born at wire-disc 39; no lossy v0 form (v0 + // predates the catalog entirely), so it round-trips by identity like + // every other v1-native kind above. + CreateMeasure(crate::payload::CreateMeasureOp), } /// 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 f1660f9..69a7614 100644 --- a/crates/epiphany-ops/src/valuegen.rs +++ b/crates/epiphany-ops/src/valuegen.rs @@ -17,14 +17,14 @@ use std::collections::BTreeMap; use epiphany_core::{ AcousticPitch, AcousticRealization, AleatoricAnchoringDiscipline, AleatoricTimeModel, AnalysisLayerId, AnchorOffset, Beam, BeamId, CmnNominal, Event, EventId, EventOrderingDAG, - EventPosition, IdentifiedPitch, MetricTimeModel, MusicalDuration, MusicalPosition, - PartDefinitionId, Pitch, PitchId, PitchSpaceId, PitchSpacePosition, PitchSpelling, - PitchedEvent, ProportionalTimeModel, Region, RegionContent, RegionEdge, RegionId, - RegionTimeModel, RepeatKind, RepeatStructure, RepeatStructureId, Rest, ScalePosition, Slur, - SlurId, SpellingAttachment, SpellingDirective, SpellingScope, SpellingSource, - StaffBasedContent, StaffExtent, StaffGroupId, StaffId, StaffInstance, StaffInstanceId, - StemConfiguration, Tie, TieClass, TieId, TimeAnchor, TimeExtent, ViewId, Voice, VoiceId, - VoiceOrigin, Volta, WallClockDuration, WallClockTime, + EventPosition, IdentifiedPitch, Measure, MeasureId, MeasureNumberVisibility, MetricTimeModel, + MusicalDuration, MusicalPosition, PartDefinitionId, Pitch, PitchId, PitchSpaceId, + PitchSpacePosition, PitchSpelling, PitchedEvent, ProportionalTimeModel, Region, RegionContent, + RegionEdge, RegionId, RegionTimeModel, RepeatKind, RepeatStructure, RepeatStructureId, Rest, + ScalePosition, Slur, SlurId, SpellingAttachment, SpellingDirective, SpellingScope, + SpellingSource, StaffBasedContent, StaffExtent, StaffGroupId, StaffId, StaffInstance, + StaffInstanceId, StemConfiguration, Tie, TieClass, TieId, TimeAnchor, TimeExtent, + TimeSignatureId, ViewId, Voice, VoiceId, VoiceOrigin, Volta, WallClockDuration, WallClockTime, }; /// A deterministic, fully-specified C4 pitch in the cmn-12 space — the neutral @@ -424,6 +424,38 @@ pub fn view(id: ViewId, active_layers: Vec) -> epiphany_core::V } } +/// A minimal [`Measure`] (genesis tranche G3b) — the value a `CreateMeasure` +/// appends: anchored to a `WallClock` point (deliberately, over an id-carrying +/// shape — see below), with an explicit signature reference and number. +/// +/// **Field-length distinguishability (contract §3's `valuegen` trap, from +/// G3a's `analysis_layer` mistake — a name that happened to share its +/// encoded byte width with the 16-byte `id` field made a field-swap mutation +/// byte-invisible):** `Measure` has five fields; this fixture's chosen +/// encodings are MUTUALLY DISTINCT in length — `id` (an identifier, a +/// length-prefixed leaf: 4-byte length + 16-byte payload = 20 bytes), +/// `start` (a `WallClock` anchor: 1-byte variant tag + a `WallClockTime` leaf, +/// 4+8 = 12 bytes, so 13 total — deliberately not an `Event`/`Measure`/ +/// `Region` anchor, whose embedded id-leaf would collide with `id`'s own +/// width), `time_signature` (`Some`: 1-byte tag + a 20-byte id-leaf = 21 +/// bytes), `explicit_number` (`Some`: 1-byte tag + a bare 4-byte `u32`, NOT +/// leaf-framed = 5 bytes), and `number_visibility` (a C-style enum: 1 tag +/// byte, no leaf framing) — 20, 13, 21, 5, 1 are pairwise distinct, so a +/// `struct_codec!` field reorder (M12) changes the byte layout, not merely +/// reinterprets it. Verified against `leaf_codec!`/`Option`/`u32`'s actual +/// `Codec` impls (`codec.rs`), not assumed. +pub fn measure(id: MeasureId, time_signature: TimeSignatureId, explicit_number: u32) -> Measure { + Measure { + id, + start: TimeAnchor::WallClock { + time: WallClockTime(1_000_000_000 * i64::from(explicit_number)), + }, + time_signature: Some(time_signature), + explicit_number: Some(explicit_number), + number_visibility: MeasureNumberVisibility::Auto, + } +} + /// Canvas layout defaults with an `nth`-distinct page width (genesis tranche /// G2a) — distinct `nth` give distinct `CanvasLayoutDefaults` values so a /// harness can drive concurrent `SetCanvasLayoutDefaults`s, an advisory LWW diff --git a/crates/epiphany-ops/src/vectors.rs b/crates/epiphany-ops/src/vectors.rs index 1ddeb78..1db6df6 100644 --- a/crates/epiphany-ops/src/vectors.rs +++ b/crates/epiphany-ops/src/vectors.rs @@ -564,6 +564,45 @@ pub fn decode_vectors() -> Vec { view_trailing, )); + // Genesis tranche G3b (kind 39, `spec/CONTRACT_GENESIS_G3B_MEASURE.md`). + let measure_envelope = OperationEnvelope { + id: OperationId::new(ReplicaId(1), 9), + author: crate::support::AuthorId(0), + stamp: crate::stamp::OperationStamp::new( + crate::stamp::HybridLogicalClock::new(epiphany_core::WallClockTime(1), 1), + OperationId::new(ReplicaId(1), 9), + ), + causal_context: crate::causal::CausalContext::new(), + transaction: None, + payload: crate::payload::OperationPayload::Primitive( + crate::payload::OperationKind::CreateMeasure(crate::payload::CreateMeasureOp { + instance: epiphany_core::StaffInstanceId::new(ReplicaId(1), 1), + measure: crate::valuegen::measure( + epiphany_core::MeasureId::new(ReplicaId(1), 1), + epiphany_core::TimeSignatureId::new(ReplicaId(1), 1), + 1, + ), + }), + ), + }; + let measure_envelope_bytes = measure_envelope.to_canonical_bytes(); + v.push(row( + OE, + "accept", + "-", + "create_measure", + measure_envelope_bytes.clone(), + )); + let mut measure_trailing = measure_envelope_bytes; + measure_trailing.push(0); + v.push(row( + OE, + "reject", + "trailing-bytes", + "create_measure_trailing", + measure_trailing, + )); + v } diff --git a/crates/epiphany-testkit/src/generators.rs b/crates/epiphany-testkit/src/generators.rs index 95d50a4..641825a 100644 --- a/crates/epiphany-testkit/src/generators.rs +++ b/crates/epiphany-testkit/src/generators.rs @@ -34,23 +34,23 @@ use epiphany_ops::{ AnomalousReplicaSegment, AuthorId, CausalContext, ChangeRegionTimeModelOp, ConflictId, ConflictKind, ConflictKindRegistryId, ConflictRecord, ConflictRegistry, ConflictResolutionState, CreateAnalysisLayerOp, CreateCrossCuttingOp, CreateInstrumentOp, - CreatePartDefinitionOp, CreateRegionOp, CreateRepeatStructureOp, CreateStaffGroupOp, - CreateStaffInstanceOp, CreateStaffOp, CreateViewOp, CreateVoiceOp, CrossCuttingValue, - DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp, DeleteRegionOp, - DeleteRepeatStructureOp, DeleteStaffInstanceOp, DeleteVoiceOp, ExtensionPreconditionId, - FieldPath, HybridLogicalClock, InsertEventOp, InsertIdentifiedPitchOp, IntegrityAnomaly, - IntegrityAnomalyKind, IntegrityAnomalyRegistryId, MaterializedState, ModifyCrossCuttingOp, - ModifyEventOp, ModifyIdentifiedPitchOp, NoOpReason, ObjectKind, ObjectState, OperationEffect, - OperationEnvelope, OperationKind, OperationKindRegistryId, OperationPayload, OperationSet, - OperationStamp, PendingReason, PositionRemapping, PreconditionFailureReason, - PreconditionFailureRegistryId, ReanchorReason, ReanchorReasonRegistryId, ReanchorResult, - RepairKind, RepairKindRegistryId, RepairRecord, ReplicaAnomalyReason, ReplicaAnomalyRegistryId, - ResolutionAction, ResolutionRegistryId, ResolveConflictPayload, RespellPitchOp, - SerializedCanonicalInputs, SetCanvasLayoutDefaultsOp, SetMetadataOp, SetMetricGridOp, - SetSpellingPrecedenceOp, SetStaffLayoutOp, SetTempoSegmentOp, SetTimeSignatureOp, - SetTuningContextOp, SetUserPageBreakOp, SetUserSystemBreakOp, TransactionCategory, - TransactionDescriptor, TransposeIntervalOp, TransposeOp, TupletCompensation, - TupletCompensationKind, UndoPolicy, UndoTransactionPayload, + CreateMeasureOp, CreatePartDefinitionOp, CreateRegionOp, CreateRepeatStructureOp, + CreateStaffGroupOp, CreateStaffInstanceOp, CreateStaffOp, CreateViewOp, CreateVoiceOp, + CrossCuttingValue, DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp, + DeleteRegionOp, DeleteRepeatStructureOp, DeleteStaffInstanceOp, DeleteVoiceOp, + ExtensionPreconditionId, FieldPath, HybridLogicalClock, InsertEventOp, InsertIdentifiedPitchOp, + IntegrityAnomaly, IntegrityAnomalyKind, IntegrityAnomalyRegistryId, MaterializedState, + ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, NoOpReason, ObjectKind, + ObjectState, OperationEffect, OperationEnvelope, OperationKind, OperationKindRegistryId, + OperationPayload, OperationSet, OperationStamp, PendingReason, PositionRemapping, + PreconditionFailureReason, PreconditionFailureRegistryId, ReanchorReason, + ReanchorReasonRegistryId, ReanchorResult, RepairKind, RepairKindRegistryId, RepairRecord, + ReplicaAnomalyReason, ReplicaAnomalyRegistryId, ResolutionAction, ResolutionRegistryId, + ResolveConflictPayload, RespellPitchOp, SerializedCanonicalInputs, SetCanvasLayoutDefaultsOp, + SetMetadataOp, SetMetricGridOp, SetSpellingPrecedenceOp, SetStaffLayoutOp, SetTempoSegmentOp, + SetTimeSignatureOp, SetTuningContextOp, SetUserPageBreakOp, SetUserSystemBreakOp, + TransactionCategory, TransactionDescriptor, TransposeIntervalOp, TransposeOp, + TupletCompensation, TupletCompensationKind, UndoPolicy, UndoTransactionPayload, }; use crate::rng::Rng; @@ -414,7 +414,7 @@ pub fn conflict_registry(rng: &mut Rng) -> ConflictRegistry { /// A typed precondition failure (every core and registered variant). pub fn precondition_failure_reason(rng: &mut Rng) -> PreconditionFailureReason { - match rng.below(14) { + match rng.below(19) { 0 => PreconditionFailureReason::TargetMissing, 1 => PreconditionFailureReason::TargetTombstoned, 2 => PreconditionFailureReason::WrongRegionTimeModel, @@ -427,7 +427,15 @@ pub fn precondition_failure_reason(rng: &mut Rng) -> PreconditionFailureReason { 9 => PreconditionFailureReason::TempoMapMalformed, 10 => PreconditionFailureReason::SystemDerivedContentImmutable, 11 => PreconditionFailureReason::RecreateContentMismatch, - 12 => PreconditionFailureReason::ExtensionPrecondition(ExtensionPreconditionId( + // Push 4a (previously a decoder hole this generator never reached — + // P13-S20, closed by genesis tranche G3b's row 12a repair). + 12 => PreconditionFailureReason::AcousticRealizationPinned, + 13 => PreconditionFailureReason::TranspositionOutOfRange, + // Genesis tranche G3b. + 14 => PreconditionFailureReason::MeasureMeterMismatch, + 15 => PreconditionFailureReason::MeasureOutOfOrder, + 16 => PreconditionFailureReason::MeasureOrderUnverifiable, + 17 => PreconditionFailureReason::ExtensionPrecondition(ExtensionPreconditionId( rng.next_u64() as u128, )), _ => PreconditionFailureReason::Registered(PreconditionFailureRegistryId( @@ -647,7 +655,7 @@ pub fn operation_payload(rng: &mut Rng, events: u64, pitches: u64) -> OperationP } _ => {} } - let kind = match rng.below(39) { + let kind = match rng.below(40) { 0 => { let pitches = if rng.boolean() { vec![obj_pitch(rng.below(pitches))] @@ -899,6 +907,16 @@ pub fn operation_payload(rng: &mut Rng, events: u64, pitches: u64) -> OperationP vec![AnalysisLayerId::new(OBJ_REPLICA, rng.below(2))], ), }), + // Genesis tranche G3b: append a measure onto the shared + // staff-instance id space. + 38 => OperationKind::CreateMeasure(CreateMeasureOp { + instance: StaffInstanceId::new(OBJ_REPLICA, rng.below(2)), + measure: valuegen::measure( + MeasureId::new(OBJ_REPLICA, rng.below(2)), + TimeSignatureId::new(OBJ_REPLICA, rng.below(2)), + rng.below(4) as u32, + ), + }), _ => OperationKind::Registered( OperationKindRegistryId(rng.next_u64() as u128), rng.byte_vec(0, 16), @@ -1929,7 +1947,7 @@ mod tests { /// Push-4a debt) and `CreateInstrument` (kind 31, G1 debt) went missing /// from every corpus this generator feeds despite every downstream suite /// staying green. Assert a bounded draw actually reaches every kind - /// appended past the historically-tested range (discriminants 30..=38), + /// appended past the historically-tested range (discriminants 30..=39), /// not just that the function does not panic. #[test] fn operation_payload_emits_every_appended_kind() { @@ -1939,6 +1957,7 @@ mod tests { let mut saw_tuning_context = false; let (mut saw_create_staff_group, mut saw_create_part_definition) = (false, false); let (mut saw_create_analysis_layer, mut saw_create_view) = (false, false); + let mut saw_create_measure = false; for _ in 0..2000 { let OperationPayload::Primitive(kind) = operation_payload(&mut rng, 8, 8) else { continue; @@ -1953,6 +1972,7 @@ mod tests { OperationKind::CreatePartDefinition(_) => saw_create_part_definition = true, OperationKind::CreateAnalysisLayer(_) => saw_create_analysis_layer = true, OperationKind::CreateView(_) => saw_create_view = true, + OperationKind::CreateMeasure(_) => saw_create_measure = true, _ => {} } } @@ -1992,6 +2012,54 @@ mod tests { saw_create_view, "CreateView (kind 38, G3a debt) never drawn in 2000 samples" ); + assert!( + saw_create_measure, + "CreateMeasure (kind 39, G3b debt) never drawn in 2000 samples" + ); + } + + /// (M66, row 23) `precondition_failure_reason`'s doc comment claims + /// "every core and registered variant" — assert the bounded draw + /// actually reaches every one, including the genesis tranche G3b + /// additions (16–18) and the previously-untested Push 4a pair (14–15, + /// P13-S20). + /// + /// **Mutation:** revert `rng.below(19)` to `rng.below(14)`; must fail — + /// the generator would then never reach discriminants 14 through 18. + #[test] + fn precondition_failure_reason_reaches_every_variant() { + let mut rng = Rng::new(23); + let mut seen: std::collections::BTreeSet = std::collections::BTreeSet::new(); + for _ in 0..2000 { + let reason = precondition_failure_reason(&mut rng); + let discriminant = match reason { + PreconditionFailureReason::TargetMissing => 0, + PreconditionFailureReason::TargetTombstoned => 1, + PreconditionFailureReason::WrongRegionTimeModel => 2, + PreconditionFailureReason::TupletCompensationInvalid => 3, + PreconditionFailureReason::EventDurationInvalid => 4, + PreconditionFailureReason::PositionOutsideRegion => 5, + PreconditionFailureReason::PitchSpaceMismatch => 6, + PreconditionFailureReason::VoiceMissing => 7, + PreconditionFailureReason::ContainerNotEmpty => 8, + PreconditionFailureReason::TempoMapMalformed => 9, + PreconditionFailureReason::SystemDerivedContentImmutable => 10, + PreconditionFailureReason::RecreateContentMismatch => 11, + PreconditionFailureReason::AcousticRealizationPinned => 12, + PreconditionFailureReason::TranspositionOutOfRange => 13, + PreconditionFailureReason::MeasureMeterMismatch => 14, + PreconditionFailureReason::MeasureOutOfOrder => 15, + PreconditionFailureReason::MeasureOrderUnverifiable => 16, + PreconditionFailureReason::ExtensionPrecondition(_) => 17, + PreconditionFailureReason::Registered(_) => 18, + }; + seen.insert(discriminant); + } + let expected: std::collections::BTreeSet = (0..=18).collect(); + assert_eq!( + seen, expected, + "the bounded draw must reach every core and registered variant" + ); } #[test] diff --git a/crates/epiphany-testkit/src/layout_stub.rs b/crates/epiphany-testkit/src/layout_stub.rs index ab20b04..7d64010 100644 --- a/crates/epiphany-testkit/src/layout_stub.rs +++ b/crates/epiphany-testkit/src/layout_stub.rs @@ -1372,7 +1372,7 @@ mod tests { /// (s10, row 29) `gen_operation_kind_tag`'s draw must cover exactly /// `PAYLOAD_FREE` ∪ `{Registered}` — **not** merely the appended - /// discriminants 30..=38, which would pass even if the `Registered` + /// discriminants 30..=39, which would pass even if the `Registered` /// append (structurally different from every built-in: it is the one /// variant `PAYLOAD_FREE` excludes by design, `payload.rs:469`) were /// deleted from the generator entirely. diff --git a/crates/epiphany-testkit/tests/text_projection_grammar.rs b/crates/epiphany-testkit/tests/text_projection_grammar.rs index 2fa9867..146a819 100644 --- a/crates/epiphany-testkit/tests/text_projection_grammar.rs +++ b/crates/epiphany-testkit/tests/text_projection_grammar.rs @@ -306,15 +306,16 @@ fn the_kind_productions_are_the_operation_vocabulary() { // appends a tag must bump it (genesis G1 took it from 31 to 32 by adding // `CreateInstrument`; genesis G2a took it from 32 to 34 by adding // `SetCanvasLayoutDefaults` and `SetSpellingPrecedence`; genesis G2b took - // it from 34 to 35 by adding `SetTuningContext`; genesis G3a takes it + // it from 34 to 35 by adding `SetTuningContext`; genesis G3a took it // from 35 to 39 by adding `CreateStaffGroup`, `CreatePartDefinition`, - // `CreateAnalysisLayer`, and `CreateView`). It stays a literal on - // purpose: deriving it from `PAYLOAD_FREE.len()` would make the assertion + // `CreateAnalysisLayer`, and `CreateView`; genesis G3b takes it from 39 + // to 40 by adding `CreateMeasure`). 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(), - 39, - "38 payload-free kinds plus `Registered`" + 40, + "39 payload-free kinds plus `Registered`" ); let actual = alternatives("kind"); diff --git a/crates/epiphany-textproj/src/lib.rs b/crates/epiphany-textproj/src/lib.rs index b1e152f..5a59283 100644 --- a/crates/epiphany-textproj/src/lib.rs +++ b/crates/epiphany-textproj/src/lib.rs @@ -56,7 +56,7 @@ use epiphany_ops::OperationEnvelope; /// and `create-view` to the `kind` production — the same reasoning as every /// prior kind append: extending the grammar without moving this constant /// would leave two incompatible grammars both claiming `(0 11 0)`. -pub const COMPANION_VERSION: (u32, u32, u32) = (0, 12, 0); +pub const COMPANION_VERSION: (u32, u32, u32) = (0, 13, 0); /// A parsed canonical Text Projection document. /// diff --git a/crates/epiphany-textproj/src/parse.rs b/crates/epiphany-textproj/src/parse.rs index 31f157f..4a5c03f 100644 --- a/crates/epiphany-textproj/src/parse.rs +++ b/crates/epiphany-textproj/src/parse.rs @@ -650,11 +650,12 @@ mod tests { // Bumped with `COMPANION_VERSION` (0.7.0 → 0.8.0, genesis G1; 0.8.0 → // 0.9.0, genesis G2a; 0.9.0 → 0.10.0, G-minor; 0.10.0 → 0.11.0, genesis - // G2b; 0.11.0 → 0.12.0, genesis G3a). Kept a literal because `projection` - // takes `&[&str]` and a formatted String would ripple through every call - // site; `the_test_header_tracks_the_implemented_version` below fails - // loudly if the two ever drift. - const HEADER: &str = "(text-projection (0 12 0))"; + // G2b; 0.11.0 → 0.12.0, genesis G3a; 0.12.0 → 0.13.0, genesis G3b). 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 13 0))"; const DOCUMENT: &str = "(document #x00000000000000000000000000000001 (schema 0 1))"; /// A minimal but complete valid projection: just the two mandatory lines. diff --git a/crates/epiphany-textproj/src/vectors.rs b/crates/epiphany-textproj/src/vectors.rs index 0b671ca..2726a76 100644 --- a/crates/epiphany-textproj/src/vectors.rs +++ b/crates/epiphany-textproj/src/vectors.rs @@ -22,13 +22,13 @@ use epiphany_bundle::{ ReductionAlgorithmVersion, SchemaVersion, SemVer, SnapshotId, }; use epiphany_core::{ - AnalysisLayerId, OperationId, PartDefinitionId, RegionId, ReplicaId, StaffGroupId, StaffId, - ViewId, WallClockTime, + AnalysisLayerId, MeasureId, OperationId, PartDefinitionId, RegionId, ReplicaId, StaffGroupId, + StaffId, StaffInstanceId, TimeSignatureId, ViewId, WallClockTime, }; use epiphany_ops::{ - AuthorId, CausalContext, CreateAnalysisLayerOp, CreatePartDefinitionOp, CreateStaffGroupOp, - CreateViewOp, DeleteRegionOp, HybridLogicalClock, OperationEnvelope, OperationKind, - OperationPayload, OperationStamp, SetTuningContextOp, + AuthorId, CausalContext, CreateAnalysisLayerOp, CreateMeasureOp, CreatePartDefinitionOp, + CreateStaffGroupOp, CreateViewOp, DeleteRegionOp, HybridLogicalClock, OperationEnvelope, + OperationKind, OperationPayload, OperationStamp, SetTuningContextOp, }; use crate::parse::parse_document; @@ -258,6 +258,27 @@ fn view_envelope(counter: u64, physical_time: i64) -> OperationEnvelope { } } +/// Genesis tranche G3b (kind 39, `spec/CONTRACT_GENESIS_G3B_MEASURE.md`). +/// Same rationale as `staff_group_envelope` above. +fn measure_envelope(counter: u64, physical_time: i64) -> OperationEnvelope { + let id = OperationId::new(ReplicaId(1), counter); + OperationEnvelope { + id, + author: AuthorId(0xAB), + stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(physical_time), 0), id), + causal_context: CausalContext::new(), + transaction: None, + payload: OperationPayload::Primitive(OperationKind::CreateMeasure(CreateMeasureOp { + instance: StaffInstanceId::new(ReplicaId(1), 1), + measure: epiphany_ops::valuegen::measure( + MeasureId::new(ReplicaId(1), 1), + TimeSignatureId::new(ReplicaId(1), 1), + 1, + ), + })), + } +} + fn profiles(custom: bool) -> Vec { let mut profiles = vec![ProfileDeclaration::full()]; if custom { @@ -416,6 +437,19 @@ fn accept_documents() -> Vec<(&'static str, String)> { envelopes: vec![view_envelope(11, 1100)], }; + // Genesis G3b: kind 39 in the committed corpus. Same manifest-version + // discipline as `staff_group` etc above. + let measure = TextDocument { + document_id: DocumentId([10; 16]), + manifest_schema_version: SchemaVersion::V0, + lineage_id: None, + profiles: profiles(false), + extensions: Vec::new(), + canonical_base: None, + blobs: Vec::new(), + envelopes: vec![measure_envelope(12, 1200)], + }; + vec![ ("minimal", project_text_document(&minimal)), ("set_tuning_context", project_text_document(&tuning_context)), @@ -438,6 +472,7 @@ fn accept_documents() -> Vec<(&'static str, String)> { project_text_document(&analysis_layer), ), ("create_view", project_text_document(&view)), + ("create_measure", project_text_document(&measure)), ] } @@ -522,16 +557,16 @@ pub fn document_vectors() -> Vec { .collect(); // The rejected version must be one this crate does NOT implement. Genesis - // tranche G3a moved `COMPANION_VERSION` to 0.12.0, which had been this + // tranche G3b moved `COMPANION_VERSION` to 0.13.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.11.0, the immediately superseded companion, which is the better test + // 0.12.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 13 0))", "(text-projection (0 12 0))", - "(text-projection (0 11 0))", ); vectors.push(( SURFACE, @@ -851,7 +886,7 @@ mod tests { #[test] fn the_reference_implementation_agrees_with_every_vector() { match verify(COMMITTED) { - Ok(count) => assert_eq!(count, 18, "the corpus has unexpectedly thinned"), + Ok(count) => assert_eq!(count, 19, "the corpus has unexpectedly thinned"), Err(failures) => panic!( "{} disagreement(s):\n{}", failures.len(), @@ -890,23 +925,15 @@ mod tests { ); } - /// (t11) Genesis tranche G3a: text projection round-trips all four new + /// (t11) Genesis tranche G3a: text projection round-trips all four /// kinds (`create-staff-group`, `create-part-definition`, - /// `create-analysis-layer`, `create-view`), and the companion version is - /// **0.12.0**, with the negative vector rejecting **0.11.0**. + /// `create-analysis-layer`, `create-view`) it added. /// /// **Mutation:** drop one parse arm (e.g. /// `OperationKindTag::CreateStaffGroup` from `OperationKind::parse` in - /// `textproj_kind.rs`); must fail. Separately, leave `COMPANION_VERSION` - /// at `(0, 11, 0)`; the negative vector must fail. + /// `textproj_kind.rs`); must fail. #[test] - fn t11_g3a_kinds_round_trip_and_companion_is_0_12_0_rejecting_0_11_0() { - assert_eq!( - crate::COMPANION_VERSION, - (0, 12, 0), - "the companion version must be 0.12.0" - ); - + fn t11_g3a_kinds_round_trip() { for name in [ "create_staff_group", "create_part_definition", @@ -927,9 +954,41 @@ mod tests { "{name}: project(serialize(parse(T))) == T must hold" ); } + } + + /// (t12) Genesis tranche G3b: text projection round-trips the new + /// `create-measure` kind, and the companion version is **0.13.0**, with + /// the negative vector rejecting **0.12.0** (the immediately superseded + /// companion). + /// + /// **Mutation:** drop `OperationKindTag::CreateMeasure` from + /// `OperationKind::parse` in `textproj_kind.rs`; must fail. Separately, + /// leave `COMPANION_VERSION` at `(0, 12, 0)`; the negative vector must + /// fail. + #[test] + fn t12_g3b_kinds_round_trip_and_companion_is_0_13_0_rejecting_0_12_0() { + assert_eq!( + crate::COMPANION_VERSION, + (0, 13, 0), + "the companion version must be 0.13.0" + ); + + let name = "create_measure"; + let text = accept_documents() + .into_iter() + .find(|(n, _)| *n == name) + .unwrap_or_else(|| panic!("accept document is absent: {name}")) + .1; + let document = parse_document(&text).unwrap_or_else(|e| panic!("{name} must parse: {e}")); + assert_eq!(document.envelopes.len(), 1, "{name} carries one envelope"); + let reprojected = project_text_document(&document); + assert_eq!( + reprojected, text, + "{name}: project(serialize(parse(T))) == T must hold" + ); // The negative vector must reject exactly the immediately superseded - // companion, 0.11.0. + // companion, 0.12.0. let rows = parse(COMMITTED).expect("the committed corpus parses"); let superseded = rows .iter() @@ -938,8 +997,8 @@ mod tests { assert_eq!(superseded.verdict, "reject"); let text = String::from_utf8(superseded.text.clone()).expect("utf8"); assert!( - text.contains("(text-projection (0 11 0))"), - "the negative vector must name the immediately superseded companion 0.11.0, got: {text}" + text.contains("(text-projection (0 12 0))"), + "the negative vector must name the immediately superseded companion 0.12.0, got: {text}" ); assert!( parse_document(&text).is_err(), diff --git a/spec/CONTRACT_GENESIS_G3B_MEASURE.md b/spec/CONTRACT_GENESIS_G3B_MEASURE.md new file mode 100644 index 0000000..0e214a3 --- /dev/null +++ b/spec/CONTRACT_GENESIS_G3B_MEASURE.md @@ -0,0 +1,1041 @@ +# Contract: genesis tranche G3b — `CreateMeasure`, invariant 20, and the close of the genesis ladder + +**Status:** DRAFT — awaiting ratification. +**Governs:** the final rung of `spec/PLAN_GENESIS_OPS.md`. One kind, one +invariant, three precondition reasons, one epoch. +**Predecessors:** G3a `6c5e69f`; the G3a undo repair `4b0abaf`; P13-S17 +`6170015`. + +--- + +## §0. What was verified before drafting + +Read out of the tree, not assumed. Every number below was checked. + +**`Measure` is a nested container child, not a root-level entity.** It lives +in `StaffInstance.measures` (`crates/epiphany-core/src/graph.rs:611`), not in +a `Score` root vector. **Every one of G3a's four families was root-level**, so +G3a's shape does not transfer: G3b's precedent is `CreateStaffInstance` +(`payload.rs:1510`, reducer `reduce.rs:4026`), which carries a parent id +beside the value: + +```rust +pub struct CreateStaffInstanceOp { + pub region: RegionId, + pub instance: StaffInstance, +} +``` + +**`Measure` is schema major 0, so G3b moves no wire bound.** `struct_codec!(Measure { id, start, time_signature, explicit_number, number_visibility })` +at `codec.rs:1825` is a plain unversioned walk, and its only non-scalar field +`start: TimeAnchor` has a hand-written `Codec` (`codec.rs:750`–`:800`) with +**no version branching of any kind** — I read both. `MeasureNumberVisibility` +is a `cstyle_enum_codec!` (`codec.rs:1526`). Therefore +`OperationKind::schema_major()` (`payload.rs:311`) gains **no arm**; +`CreateMeasure` falls through `_ => 0`, and `OperationEnvelopeBlock` stays at +**3** where G2b left it. G3b is *not* a G2b-shaped rung. + +**No id-vocabulary append.** `TypedObjectId::Measure(MeasureId)` already +exists (`ids.rs:500`) with kind byte **9** (`ids.rs:536`), round-trip at +`:572`/`:666`. + +**`Measure` is absent from `canonical_value!`.** It has a `Codec` and — via +`struct_codec!` — a `TextValue`, but the strict-decode list +(`codec.rs:3597`ff, where G3a added its four) does not contain it. Verified by +scanning the macro invocation body: zero occurrences. + +**Measures currently reach the graph only through base ingest.** +`create_staff_instance` explicitly rejects a carried instance bearing +measures (`reduce.rs:4047`: `!op.instance.measures.is_empty()` → +`container_not_empty()`). This is the same from-empty condition that made +G3a's four families base-ingest-only, and it is what `CreateMeasure` retires. + +**Discriminants 16, 17 and 18 are free on `PreconditionFailureReason`.** The +`discriminant()` match (`effect.rs:190`ff) ends at +`TranspositionOutOfRange => 15`. `introduced_minor()` is exhaustive with no +wildcard arm, so a new variant **cannot compile** without an epoch — the +G-minor control, working as designed. + +**Invariant 20 is free.** `GraphInvariant::number()` +(`invariants.rs:96`–`:119`) runs 1–19, ending `BarlineGroupSameRegion => 19`. +`all()` returns **`[GraphInvariant; 19]`** (`invariants.rs:121`) — a +hand-maintained count literal — under a doc comment reading "All 19 +invariants." + +**Invariant 10 already checks measure-signature *resolution*.** Its body +covers a measure's signature reference at `invariants.rs:1180`–`:1212`, with +a direct test `inv10_flags_unresolved_time_signature_reference` at `:3596`. +Invariant 20 therefore MUST NOT re-check resolution (pin 9b). + +**The effective grid, from the actual field types.** +`StaffInstance.local_metric_grid: Option` (`graph.rs:608`), +falling back to the enclosing region's +`default_metric_grid: Option` (`graph.rs:655`). +`MetricGrid { meter_sequence: Vec }` (`graph.rs:358`), and +`MeterChange { anchor: TimeAnchor, time_signature: TimeSignatureId }` +(`graph.rs:237`) — the change is positioned by a **`TimeAnchor`**, not a +scalar offset, which pins 6 and 6b must account for. + +**`Measure` has a live inbound-reference surface.** Cross-cutting structures +anchor to measures through `TimeAnchor::Measure { id, .. }` — see +`payload.rs:1051` (spanner endpoints → `TypedObjectId::Measure`), +`indexes.rs:56`, and `invariants.rs:452`. **This is the Packet A defect class, +live for G3b before a line is written** (pin 10). + +**The six boundary crossings, at their post-G3a values.** One compiles +loudly; five fail silently: + +| # | Site | Now | Becomes | +|---|---|---|---| +| 1 | `epiphany-editor-core/src/barriers.rs` `subjects_of` | exhaustive match | one new arm — **compiles loudly** | +| 2 | `epiphany-layout-ir/src/barrier.rs:1179` | `tag: 39` | `tag: 40` | +| 3 | `epiphany-testkit/tests/text_projection_grammar.rs:316`–`:317` | `39`, `"38 payload-free kinds plus `Registered`"` | `40`, `"39 payload-free kinds…"` | +| 4 | `epiphany-testkit/src/generators.rs:1932` + test body | comment `30..=38`; `saw_*` flags per kind | `30..=39` + a `saw_create_measure` flag and its assertion | +| 5 | `epiphany-testkit/src/layout_stub.rs:1375` | comment `30..=38` | `30..=39` | +| 6 | `epiphany-textproj/src/vectors.rs:533`–`:534`, `:941` | `(0 12 0)` / `(0 11 0)` | `(0 13 0)` / `(0 12 0)` | + +Sites 4 and 5 are **not** comment-only: both test bodies enumerate appended +kinds explicitly, so site 4 needs a new boolean and assertion, not a literal +bump. `COMPANION_VERSION` is `(0, 12, 0)` at +`epiphany-textproj/src/lib.rs:59`. + +**The golden lock still stops at 30.** `payload.rs:2165` reads +`let table: [(OperationKind, u8); 30] = [`. Kinds 30–39 stay outside it — +**P13-S15 remains open by design**; G3b does not extend it (pin 12). + +**Packet B's own guard now constrains this rung.** The history guard +(`epiphany-testkit/tests/binary_format_history.rs`) asserts a principal +marker per standalone-row rung. G3b must add its Revision History row *and* +its marker to `PRINCIPAL_MARKERS`, or the guard fails — by construction +(pin 14). + +--- + +## §1. Pins + +**Pin 1 — kind and tag 39, epoch 12, in both discriminant spaces.** The two +spaces are unaligned and both hand-touched: `OperationKind::discriminant()` +(hand-written match, `payload.rs:253`ff) and `OperationKindTag` (macro +`operation_kind_tag_vocabulary!`, `payload.rs:440`ff). Add +`CreateMeasure => 39` to the first and +`CreateMeasure = 39 => "create-measure" @ Some(12)` to the second. + +**Pin 2 — `schema_major()` gains no arm.** Per §0, `Measure` is major 0. +`OperationEnvelopeBlock` stays at 3. If an arm for `CreateMeasure` appears in +`schema_major()`, the packet is wrong. + +**Pin 3 — `Measure` joins `canonical_value!`** (`codec.rs:3597`ff), one +entry. This introduces **no new wire layout** — the macro delegates to the +existing `Codec` and generates strict `decode_canonical` (decode → +`finish()` → re-encode → reject on byte mismatch). No `textvalue_graph.rs` +work: `struct_codec!` already generated `Measure`'s `TextValue`. + +**Pin 4 — the operation shape follows `CreateStaffInstance`, not G3a.** + +```rust +pub struct CreateMeasureOp { + pub instance: StaffInstanceId, + pub measure: Measure, +} +``` + +with `measure_id()` returning `self.measure.id`, and `encode_canonical` +pushing the parent id then the value — mirroring `CreateStaffInstanceOp` +(`payload.rs:1522`ff). The parent is the `StaffInstance`, because +`measures` is per-instance and admits polymeter (`graph.rs:609`–`:611`). + +**Pin 5 — the carried-value map must carry the OWNING INSTANCE.** A +`BTreeMap` is **insufficient**: `Measure` carries no +back-pointer to its parent (`graph.rs:588`–`:594`), so a map keyed by +`MeasureId` alone cannot tell the graph-removal arm (pin 10) which +`StaffInstance.measures` to remove from, and cannot support any per-instance +ordering check. Every G3a family was root-level and had no parent to lose; +`Measure` does. The map is therefore + +```rust +measure_values: BTreeMap, +``` + +threaded through the **same seven sites** — reducer state declaration, +`WorkingSnapshot` declaration, initialization, **base seeding in +`seed_from_graph`** (recording the enclosing instance id as it walks), mint +insertion, snapshot, restore. Site 4 fails silently when omitted (the G1 +`instrument_values` hazard). + +Set-union discipline: byte-identical re-carry → `AlreadyApplied`; differing +value under a live id → `RecreateContentMismatch`; tombstoned → +`TargetTombstoned`. **A re-carry naming a *different* parent instance with an +otherwise identical measure is `RecreateContentMismatch`, not +`AlreadyApplied`** — the parent is part of identity here, and the comparison +MUST include it. + +**Pin 6 — the comparable relation: exact shapes, exhaustively.** + +The first two drafts were both wrong about this. Draft 1 said only +cross-variant comparison failed. Draft 2 said "same referent, compare +offsets", which ignores that offsets themselves are not always comparable and +that three anchor shapes carry no referent id at all. + +**Offsets first.** `AnchorOffset` (`time.rs:557`) is +`Musical(MusicalDuration) | WallClock(WallClockDuration) | Zero` — three +clocks with **no cross-clock ordering**. Normalization: `Zero` is read as the +additive identity **of whichever clock it is compared against**, so +`Zero` ≟ `Musical(d)` and `Zero` ≟ `WallClock(d)` are both defined. Two +offsets are **comparable** iff they are the same variant, or at least one is +`Zero`. `Musical` against `WallClock` is **never** comparable — that is the +deferred wall-clock/musical reconciliation, not an implementation detail. + +**Anchor shapes.** `MeasurePosition` and `RegionEdge` are each +`Start | End` (`time.rs:540`, `:547`). **The boundary selector must be +IDENTICAL — it is never ordered.** Two anchors are **comparable** iff they +match one of these five shapes and their offsets are comparable: + +| # | Shape | Order by | +|---|---|---| +| c1 | `Event{id: a, off}` vs `Event{id: a, off}` — **same id** | offset | +| c2 | `Measure{id: a, pos: p, off}` vs `Measure{id: a, pos: p, off}` — same id **and same `pos`** | offset | +| c3 | `Measure{id: a, pos: Start, off: Zero}` vs `Measure{id: b, pos: Start, off: Zero}`, `a`/`b` **in the same `StaffInstance.measures`** | vector index | +| c4 | `Region{id: a, edge: e, off}` vs `Region{id: a, edge: e, off}` — same id **and same `edge`** | offset | +| c5 | `WallClock{time}` vs `WallClock{time}` — **no referent id** | `time` | + +**Draft 3 ordered `Start < End` and then compared offsets. That is unsound, +and core says so.** With a nonzero offset the selector does not bound the +point: `Region{edge: Start, off: Musical(100)}` is **not** provably before +`Region{edge: End, off: Zero}` without knowing the region's length. The same +holds for `Measure` Start vs End — and +`crates/epiphany-core/src/invariants.rs:400`ff records exactly this: the +prototype anchor resolver places `Measure` **start** anchors and `Region` +edges but returns `None` for a `Measure` **end**, because a coordinate +"cannot be placed without the deferred tempo/measure-length machinery." +Cross-boundary comparison therefore stays **unverifiable** unless the +boundary's own duration is resolved — which is the deferred machinery, not +this rung's business. + +**c3's restriction to `pos: Start` and `off: Zero` is load-bearing.** A +vector index orders measure *reference points*, not arbitrary points near +them: a nonzero offset or an `End` position can carry a point past its +neighbour, so the index no longer bounds it. + +**Everything not in that table is NOT comparable**, and no other relation may +be invented — defining one is a specification question this rung has no +authority over. In particular: never `Event`↔`Measure`, never +`Measure`↔`Region`, never two `Event`s with different ids (that needs each +event's resolved position, the deferred **P11-C5** machinery — see +`PositionOutsideRegion`'s own "Reserved" note, `effect.rs:139`–`:142`), never +`Musical` against `WallClock`, and **never across differing `pos`/`edge` +selectors**. + +**Pin 6b — boundary consistency needs a musical DELTA, not an ordering.** +Pin 6 answers "before or after"; invariant 20's second clause needs "exactly +how far", compared against `TimeSignature::measure_duration()` +(`graph.rs:346`). Ordering does not supply that, and this was conflated in +both prior drafts. + +The delta between two measure starts is computable **only** in shape c1, c2, +or c4 with **both offsets in the `Musical` clock** (or `Zero`, normalized to +`Musical(0)`), **and only when the boundary selector is identical** — same +referent id *and* same `pos`/`edge`. Then the shared reference point cancels +and the difference of the offsets is exact. + +**Draft 3 subtracted offsets across differing `pos`/`edge`, which is the same +unsoundness as pin 6's ordering error.** Across a boundary the reference +points differ by the boundary's own duration, which is precisely the +unresolved quantity (`invariants.rs:400`ff). Subtracting the offsets then +omits that duration and yields a wrong delta, not merely an unordered one. + +`WallClock` deltas are not musical durations and MUST NOT be compared with +`measure_duration`. **c3 supplies no delta at all**: a vector index gives +order, never distance. + +Where the delta is not computable, boundary consistency **abstains** (pin 7). + +**Pin 6c — the effective-grid oracle, and the instance-grid ledger the +reducer does not have.** + +Every prior draft said "the active signature at the measure's start" without +defining *selection*. Over a **partially comparable** `meter_sequence` that is +not a function. Define it: + +**Step 0 comes before any candidate set.** If **any** `MeterChange` in the +effective grid has an anchor **incomparable** to the measure's start (pin 6), +selection is **indeterminate** whenever a governing signature is needed — the +operation refuses (`MeasureOrderUnverifiable`), the invariant abstains (pin 7). +This holds **even when the candidate set is empty**, and draft 4 got it wrong: +it let "no candidates" win, so a sequence in which *every* change is +incomparable read as "no active signature, agreement vacuous" instead of +"cannot tell." An incomparable change is not absent; it is unplaced, and it +might have governed. + +Only once step 0 passes — every change comparable — let `C` = those changes +**not after** the measure's start. Then: + +1. `C` empty → **no active signature.** Agreement is vacuous; the boundary + clause abstains. This is *not* a violation. Reachable only from a + genuinely empty or wholly-later sequence, never from incomparability. +2. `C` has a unique maximum under pin 6's relation → that is the governing + change. +3. `C` has **multiple maxima mutually incomparable to each other** → + indeterminate, as step 0. Two changes can each be comparable to the + measure's start yet not to one another, so this rule is not subsumed by + step 0. Never pick one by document order, id, or vector position — that + fabricates a total order pin 6 refuses to define. + +**The ledger gap, which blocks base-free correctness.** +`StaffInstance.local_metric_grid` **overrides** the region default +(`graph.rs:608`, `:655`), but **the reducer retains no instance-local grid +state.** `create_staff_instance` accepts the field and seeds only the layout +advisory chain (`reduce.rs:4026`ff). So base-free, a `CreateMeasure`, +`SetMetricGrid`, or `SetTimeSignature` **cannot tell a local override from an +inherited default** — it would silently read the region default and could +refuse or permit wrongly. + +This rung MUST resolve that, and there are exactly two admissible answers. +**Ratify one:** + +**Disposition (A) is RATIFIED** (2026-07-29): it preserves graph-aware / +base-free parity and avoids knowingly authoring states that become invalid the +moment they are materialized. + +- **(A) Add an instance-grid ledger** — `instance_grid: BTreeMap>` + with the full seven-site lifecycle (declaration, snapshot decl, init, base + seed, mint/write, snapshot, restore), seeded by `create_staff_instance` and + by `seed_from_graph`. + + **The ledger alone is NOT the oracle.** It distinguishes *override* from + *inheritance*; it does not reconstruct what is inherited. When an instance + inherits, the effective grid MUST be reconstructed from the **existing** + ledgers, in canonical order: + + 1. `metric_grid_chain` (`reduce.rs:974`, + `BTreeMap>>`) — whole-grid writes + for the enclosing region; + 2. `meter_change_chain` (`:993`, + `WriteChain>` per `(RegionId, MusicalPosition)`) — + per-key changes layered over that grid, `None` meaning explicit removal. + + Both are `WriteChain`s, so the reconstruction MUST also fold in **prospective + undo restorations** (pin 9c.3) when evaluating an undo: the grid the + restoration would install, not the one currently in place. + + **The same oracle runs in both reduction modes.** Graph-aware reduction must + not shortcut to `score` while base-free reduction uses the chains — that would + reintroduce the parity gap (A) exists to close, and the two paths would + disagree on exactly the cases this rung refuses. +- **(B) Ratify an explicit graph-only divergence** — the agreement and + boundary preconditions are graph-aware only, and base-free reduction + **skips them entirely** rather than consulting the region default. Cheaper, + but it means a base-free stream can author a measure that violates + invariant 20 the moment a base is supplied. + +Silently reading the region default base-free is **not** an option: it is +neither (A) nor (B), and it produces wrong answers rather than absent ones. + +**Pin 7 — fail-closed at the operation, abstain at the invariant.** These are +different things, and draft 1 mislabelled abstention as fail-closed. The split +is deliberate: + +- **At the operation — fail closed.** `CreateMeasure` and the two grid + setters (pin 9c) *refuse* when the comparison or delta they need is not + computable. Nothing incomputable enters through this rung. +- **At the invariant — abstain, and name it.** Invariant 20 emits no + violation where agreement or delta is not computable, because base-ingested + data may predate the rule and flagging it would make the invariant useless + on real scores. This is **abstention**, stated as such in the doc comment, + **not** a safety property. + +File the residue as **P13-S18**: invariant 20's agreement and boundary checks +are partial, and P11-C5 resolved positions are what would close them. +**Status: open, deliberately.** + +**Pin 8 — referential preconditions.** +1. The parent `StaffInstance` must be live — **ungated**, matching + `create_staff_instance`'s region check (`reduce.rs:4031`): a mint into a + non-existent parent has nowhere to go even base-free. +2. `measure.time_signature`, when `Some(id)`, must resolve to a live + `TimeSignature` — graph-aware. +3. **`measure.start`'s referents must resolve** — `start` is a `TimeAnchor` + (`graph.rs:590`), so each non-`WallClock` variant preconditions a live + referent of the right kind. Omitted from draft 1 entirely; an unresolvable + `start` would have minted a measure anchored to nothing. Graph-aware. + +**Pin 8b — three new `PreconditionFailureReason` variants, all epoch 12.** +Draft 2 had two, and used `MeasureOrderUnverifiable` for a +comparable-but-reversed start, which is untruthful — that case is perfectly +verifiable and simply wrong. + +| Discriminant | Variant | Meaning | +|---|---|---| +| 16 | `MeasureMeterMismatch` | a resolving `time_signature` **disagrees** with the effective grid's active signature (distinct from the resolution failure, which is `TargetMissing`) | +| 17 | `MeasureOutOfOrder` | the carried `start` is **comparable** to the current last measure's start and is not strictly after it | +| 18 | `MeasureOrderUnverifiable` | the two starts are **not comparable** (pin 6), or the delta is not computable (pin 6b) | + +All three gain `introduced_minor()` arms; none compiles without one. + +**Pin 9 — `CreateMeasure` is append-only, and must check BOTH clauses +prospectively.** `StaffInstance.measures` is documented "in order" +(`graph.rs:609`–`:611`), so the measure is pushed at the end and the operation +refuses unless: + +1. the carried `start` is **comparable** to the current last measure's start + (else `MeasureOrderUnverifiable`) and **strictly after** it (else + `MeasureOutOfOrder`); +2. agreement holds against the effective grid (else `MeasureMeterMismatch`); +3. **the delta from the previous measure's start equals that measure's + governing `measure_duration()`** (else `MeasureMeterMismatch`), or the + delta is not computable (else `MeasureOrderUnverifiable`). + +**Clause 3 is the gap draft 2 left open:** a strictly-later start at the +*wrong distance* is comparable, correctly ordered, agreeing — and violates +invariant 20 the instant it lands. An operation that can create an immediate +invariant violation is not preserving the invariant. + +The first measure of an instance has no predecessor: clauses 1 and 3 are +vacuous for it, and **pickup/anacrusis is deferred** — a partial first measure +MUST NOT be refused or flagged. File as **P13-S19**, status open. + +**Pin 9b — invariant 20 covers agreement and boundary ONLY, and `None` is +narrower than draft 2 said.** Invariant 10 already checks that a measure's +signature reference *resolves* (`invariants.rs:1180`–`:1212`, test `:3596`); +invariant 20 MUST NOT duplicate it. Invariant 20 checks: + +1. **agreement** — a resolving `Some(id)` equals the effective grid's active + signature at that measure's start, within pin 6's relation; +2. **boundary consistency** — consecutive starts differ by the governing + `measure_duration()`, within pin 6b's delta. + +**`time_signature: None` avoids ONLY the agreement clause.** Draft 2 said +"never a violation", which is wrong: `None` means *inherit*, and the inherited +meter still governs **boundary consistency**. A `None` measure at the wrong +distance from its predecessor IS an invariant-20 violation. + +**Pin 9c — the invariant must be PRESERVED by everything that can break it.** +An invariant nothing maintains is decoration. Two existing operations write +the effective grid: `SetMetricGrid` (kind 22, `payload.rs:388`) and +`SetTimeSignature` (kind 25, `:392`). This rung MUST add **prospective checks +for both invariant-20 clauses** — agreement *and* boundary consistency; draft +2 specified only agreement — to: + +1. both setters' forward paths, refusing with `MeasureMeterMismatch` (or + `MeasureOrderUnverifiable` when incomputable) if the *resulting* grid would + violate invariant 20 for any **live** measure; +2. **All invariant checks MUST run BEFORE any mint.** `set_time_signature` + mints its carried signature at `reduce.rs:4518`–`:4522`, *before* it writes + the meter change. Appending a prospective refusal after that point **leaks a + freshly minted `TimeSignature`** from an operation that reported a + precondition no-op — and from a *non-transactional* operation there is no + undo to reclaim it. Restructure so every check precedes `mint_time_signature`, + and test that a refusal leaves **no residue in `objects`, no residue in any + carried-value map, and no residue in the graph**. + +3. both setters' **undo restoration** paths, because a restoration reinstating + a prior grid breaks agreement exactly as a forward write does — and + **both undo policies**, evaluated in **aggregate**: + + A single undo can collect a whole-grid restoration *and* one or more + meter-change restorations together, and **their safety is not + independent**: individually-unsafe restorations can be jointly safe (a grid + restore plus the meter-change restore that re-agrees with it), and + individually-safe ones can be jointly unsafe. Per-restoration evaluation is + therefore wrong in both directions. + + - `StrictInverse` / `Cascade`: evaluate invariant 20 against the + **prospective post-undo state with the whole restoration set applied**, + and `Conflicted` if that state violates it. Never accept or reject + restorations one at a time. + - `BestEffort`: apply the **maximal safe subset**, chosen by a + **deterministic, documented rule** — the canonical-order greedy: consider + restorations in the reduction's existing canonical order, admit each one + whose addition keeps the accumulated prospective state invariant-20-clean, + skip the rest. "Maximal" here means maximal under that rule, not + set-theoretically maximum; state that plainly, because the true maximum + subset is not uniquely defined and a search for it would not be + deterministic. It must not refuse wholesale, and it must not apply a + violating set. +3. `undo_strand_block`'s **existing `TimeSignature` arm** (`reduce.rs:5476`ff), + which today consults only `meter_change_chain`, extended so that a minted + `TimeSignature` still named by a live `Measure.time_signature` also blocks. + **That hole exists today**, before G3b, and is the class Packet A closed + three instances of. + +**Pin 10 — undo coverage, and TWO ledgers the rung must build.** + +1. `materialize_graph_tombstones` (`reduce.rs:2768`ff) gains a + `TypedObjectId::Measure(id)` arm. Structurally harder than G3a's five + root-vector `retain`s: it must reach the owning instance across + `score.canvas.regions[*].content.staff_instances`. Pin 5's parent-carrying + map is what makes that possible without a search. + +2. The `Measure` strand guard **cannot use `structures`.** That index is + **event-only by design**: the endpoint walk filters `_ => None` + (`reduce.rs:1755`–`:1757`) and the comment at `:1763`–`:1765` says so + outright — "Non-event anchorings (region, measure, wall-clock, free) + contribute no entry." A guard written against it is **born green**. + +3. **For spanners:** read `cross_cutting_modify_chain` (`reduce.rs:973`), + which holds the full `CrossCuttingValue` and therefore real anchors, and be + **restoration-aware** — the inverse of Packet A's pin A6, because + `ModifyCrossCutting` rewrites those values and + `ValueRestoration::CrossCutting` (`reduce.rs:842`) restores them, so a write + chain genuinely exists. **Both directions:** a modify that *removed* a + measure anchor, undone, reinstates the reference and MUST block; one that + *added* an anchor, undone, removes it and MUST NOT block. + +4. **For repeat structures, that chain is structurally unusable.** + `CrossCuttingValue` is `Tie | Slur | Beam | Spanner` + (`payload.rs:980`–`:985`) — **`RepeatStructure` is not a variant**, so + `cross_cutting_modify_chain` can never contain one. And + `create_repeat_structure` retains no value: it inserts liveness only + (`reduce.rs:3727`ff), pushing the value into the graph when there is one. + Draft 2's instruction to "cover RepeatStructure via that chain" was + impossible. + + The rung MUST therefore add **`repeat_values: BTreeMap`** + — or a generalized full-anchor ledger — with pin 5's **seven** sites: + declaration, `WorkingSnapshot` declaration, initialization, base seed, + create insertion, snapshot, restore. **No delete site.** + + **Draft 6 claimed an eighth, delete site on a false premise, and it is + withdrawn.** The stated justification — that retaining a deleted repeat's + value would make the strand guard "block on a corpse" — contradicts the + guard itself: pin 10.5 requires the *owning* object to be `Live`, and + `delete_repeat_structure` sets `objects` to `Tombstoned` + (`reduce.rs:3766`–`:3768`) rather than removing the entry. A retained + tombstoned value therefore **cannot** block. The premise was wrong, so the + site is unnecessary. + + **Retention is also the ratified project discipline, and this rung must not + diverge from it.** Packet A's pin A7 ruled that value maps are *not* pruned + — safe precisely because every reader consults `self.objects` first — and + the tree bears that out: there is **no `.remove` on any value map anywhere + in the reducer** (verified: zero occurrences across `staff_values`, + `instrument_values`, `time_signature_values`, `staff_group_values`). An + eighth site would make `repeat_values` the sole exception to a uniform rule, + for no benefit. + + **And retention is what makes M62's repeat observation reachable at all.** + The missing-`Live`-conjunct mutant needs a state where a tombstoned repeat's + value is still present and still names the measure. Pruning on delete would + destroy exactly that state, and M62's repeat row would be **born green** — + the failure mode this contract keeps rediscovering. `measure_values` reaches + seven sites by a different route (measures are mint-only, §6 ruling 1); both + maps land on seven, and the totals are unchanged at **75**. Use the helpers that already walk every anchor kind: + `RepeatStructure::anchor_sites()` (`graph.rs:1249`) and + `anchor_object_refs` (`reduce.rs:1290`), which maps `Event`, `Measure` + **and** `Region` — `create_repeat_structure` already relies on exactly this + pair (`reduce.rs:3717`). + +5. **The inbound surface is SEVEN surface classes.** Drafts 1–3 covered two + (spanner, repeat) and stopped; draft 4 said "five ledgers" and conflated + the additions with the total. The seven are: **spanner, repeat, measure, + meter change, system break, page break, tempo segment.** The five beyond + spanner and repeat sit together in the reducer's declarations at + `reduce.rs:988`ff — so the omission was visible at the site the contract + was already citing: + + | Surface | Ledger | Holds | + |---|---|---| + | another measure's `start` | `measure_values` (pin 5) | `Measure.start: TimeAnchor` | + | meter changes | `meter_change_chain` (`:993`) | `Option` → `anchor: TimeAnchor` | + | system breaks | `break_chain` (`:988`) | `(TimeAnchor, bool)` | + | page breaks | `page_break_chain` (`:989`) | `(TimeAnchor, bool)` | + | tempo segments | `tempo_segment_chain` (`:994`) | `Option` → **two** anchor sites: `start: TimeAnchor` and `end: Option` (`tempo.rs:109`) | + + Each chain needs **prospective and restoration-aware** treatment — every one + is a `WriteChain`, so a restoration can reinstate a measure reference the + undo was about to strand, exactly as with `cross_cutting_modify_chain`. + + **`TempoSegment` does carry anchors, at two sites** — `start` and the + optional `end` (`tempo.rs:109`). Both MUST be checked; a guard reading only + `start` misses a measure named as a segment's end. + + **Owner-liveness and `targets` handling is per surface, and the tempo + surface splits by KEY — not wholesale.** Six classes are always owned + (spanner, repeat, measure, meter change, system break, page break): the + guard requires the *owning* object `Live` and not in `targets`, as Packet + A's guards do. + + `tempo_segment_chain` is keyed `(Option, MusicalPosition)` + (`reduce.rs:995`), and the two key shapes behave differently: + - **`Some(region)` — owned.** There is a live `Region` to test, so this + entry carries the full `Live` + `!targets.contains` treatment exactly like + the other six. + - **`None` — score-level, genuinely ownerless.** No object exists to test + for liveness. The guard blocks on the reference itself, exempting only a + segment whose *write* belongs to the undone transaction. + + Draft 5 excluded the **entire** tempo surface from owner treatment on the + strength of the `None` case. That was an over-correction: it would leave a + region-scoped tempo segment's `Live` conjunct unguarded. + + A `Measure.start` naming another measure is the subtlest: undoing measure + *B* while live measure *C* is anchored to it strands authored state inside + the very map pin 5 introduces. + +6. Test rows: removal from the owning instance, ledger tombstoning, + `TargetTombstoned` on byte-identical re-carry after undo, strand refusal + from **each of the seven surfaces**, both restoration directions, + same-transaction teardown, and a tombstoned referencer not blocking — the + last two **per surface**, since each guard has its own conjuncts to get + wrong, and the score-level tempo case has no owner conjunct at all. + +**Pin 11 — the `all()` count, and a mutation that compiles.** +`[GraphInvariant; 19]` → `; 20`, the array gains its entry, the "All 19 +invariants" doc becomes 20, and `check_invariants` (`invariants.rs:229`) +dispatches it. A hand-maintained count — the class this project keeps +rediscovering (four stale sites at Push 4a, six at G2a, the golden lock at +P13-S15). + +**Reverting the count literal alone is a type error** over a 20-element array, +and a mutation that does not compile signs nothing. The signing mutation is +**deleting invariant 20's arm from `check_invariants`' dispatch**, leaving the +enum and `all()` intact — so the row must assert **behaviourally** (a score +violating only invariant 20 is flagged by `check_invariants`), since +`all().len() == 20` passes with the dispatch gone. + +**Pin 12 — the golden lock is not extended.** It stays +`[(OperationKind, u8); 30]`. P13-S15 remains open by design; extending it is +its own rung, so that a golden-lock extension lands with its own mutation +evidence and nothing else in the diff. + +**Pin 13 — companion 0.12.0 → 0.13.0**, `epiphany-textproj/src/lib.rs:59`, +with the negative-vector anchors at `vectors.rs:533`–`:534` and `:941` moved +to reject `(0 12 0)`. + +**Pin 14 — the four-document ritual, plus the new guard.** An +operation-vocabulary append is a documented event in **four** spec documents: +`operation_catalog.tex` (a new `CreateMeasure` section with undo semantics +and the stale-form/deferral notes), `binary_format.tex` (kind table, tag +table, payload layout, **version 0.15.0 → 0.16.0, and a Revision History +row**), `core_spec.tex` (invariant 20 in the invariant table and its +numbering prose), and `text_projection.tex` (the companion grammar). + +**And `binary_format_history.rs` must gain `("G3b", "--- Genesis tranche G3b")` +in `PRINCIPAL_MARKERS`.** The guard I landed at `6170015` will fail otherwise +— deliberately. Do not weaken the guard to accommodate the rung; add the row +and the marker. + +**Pin 15 — the monotonicity evidence repair.** +`spec/PLAN_GMINOR_SCHEMA_MINOR.md:201` claims real-time monotonicity with an +evidence chain ending at G2a `7df5ca1`. Extend it with **vocabulary-introducing +events only**: + +| Epoch | Event | Commit | Date | +|---|---|---|---| +| 10 | G2b | `13c3d2f` | 2026-07-29 | +| 11 | G3a | `6c5e69f` | 2026-07-29 | +| 12 | G3b | this rung | — | + +**G-minor and P13-S17 (`6170015`) do NOT belong in this chain** — G-minor +built the epoch machinery and P13-S17 restored a document's history; neither +introduced an additive variant, and the chain records introducing commits, not +rungs. All four dates verified with `git show -s --format=%cs`. + +**The 2026-07-29 tie is resolved by ancestry, not timestamp.** `13c3d2f` is an +ancestor of `6c5e69f` (verified with `git merge-base --is-ancestor`), so G2b +precedes G3a. Record the tie-break method, exactly as the existing prose does +for the two events sharing 2026-07-07. + +--- + +## §2. Touch table + +**Draft 1 omitted mandatory integration files; drafts 2–3 miscounted them.** +The authority is G3a's own footprint — `git show --name-only 6c5e69f`, 35 +files — not memory. Rows 6–14 are **nine** files, not ten; the tenth omitted +G3a-footprint file is **`core/DECISIONS.md`, carried in row 35**. Row 14a +(`QUICKSTART.md`) and row 12a (`decode.rs`) are further surfaces found in later +review rounds. Every one of them fails **silently**, none at compile time. + +**Row 12a is a pre-existing live bug, not just this rung's plumbing.** +`decode.rs`'s `precondition_reason` (`:205`) ends at **13** and rejects +everything higher, so `AcousticRealizationPinned` (14) and +`TranspositionOutOfRange` (15) **encode but cannot decode today** — a +materialized effect carrying either fails canonical round-trip. Nothing caught +it because the testkit's generator draws `rng.below(14)` (`generators.rs:417`) +under a doc comment claiming "every core and registered variant." This rung +repairs the decoder through **18** and the generator through **18**, and files +the pre-existing half as **P13-S20**. + +| # | File | Change | +|---|---|---| +| 1 | `ops/src/payload.rs` | `CreateMeasureOp`; `OperationKind::CreateMeasure`; `discriminant()` 39; `introduced_minor()` `Some(12)`; tag `39 => "create-measure" @ Some(12)`; `CanonicalEncode` (parent then value); **no `schema_major` arm** | +| 2 | `ops/src/effect.rs` | `MeasureMeterMismatch = 16`, `MeasureOutOfOrder = 17`, `MeasureOrderUnverifiable = 18`; three `introduced_minor()` arms → `Some(12)` | +| 3 | `core/src/codec.rs` | `Measure` in `canonical_value!` | +| 4 | `core/src/invariants.rs` | invariant 20; `number()` arm; `all()` `19`→`20` + entry + doc; `check_invariants` dispatch; the check body (pins 6, 6b, 9b); doc-comment abstention + pickup deferrals | +| 5 | `ops/src/reduce.rs` | `create_measure` (pins 8, 9); parent-carrying **`measure_values` ×7 sites**; **`repeat_values` ×7 sites** (no delete site — pin 10.4); **`instance_grid` ×7 sites** and the **shared effective-grid oracle** reconstructing inheritance from `metric_grid_chain` + `meter_change_chain` in **canonical write order**, folding in prospective restorations, run in **both** reduction modes (pin 6c, M30a–M30c, M31a); `Measure` graph-removal arm; the `Measure` strand guard across **all seven surfaces** — `cross_cutting_modify_chain` (spanner), `repeat_values`, `measure_values`, `meter_change_chain`, `break_chain`, `page_break_chain`, `tempo_segment_chain` (both anchor sites) — restoration-aware on the five `WriteChain` surfaces; **`SetMetricGrid`/`SetTimeSignature` preservation, both clauses, both undo policies, all checks before any mint; `TimeSignature` strand extension (pin 9c)** | +| **6** | `ops/src/envdecode.rs` | envelope decode arm for kind 39 | +| **7** | `ops/src/migrate.rs` | migrate-on-read handling | +| **8** | `ops/src/v0.rs` | v0 wire path | +| **9** | `ops/src/vectors.rs` | operation wire vector for kind 39 | +| **10** | `ops/src/valuegen.rs` | a `measure(...)` fixture — see §3's length-distinguishability requirement | +| **11** | `ops/src/fuzz.rs` | fuzz arm | +| **12** | `ops/src/textproj_kind.rs` | kind ↔ projection mapping — **this is where the operation-kind production lives**, not `textproj/project.rs` | +| **12a** | `ops/src/decode.rs` | `precondition_reason` (`:205`) must decode **16, 17, 18** — and **14, 15**, which it already omits. See below | +| **13** | `spec/vectors/decode_vectors.txt` | frozen cross-impl decode vectors — **append only** | +| **14** | `spec/vectors/textproj_document_vectors.txt` | frozen projection vectors — **append only** | +| **14a** | `spec/QUICKSTART.md:56` | "the 19 graph invariants" → 20. A stale surface absent from every prior draft; it is prose, so it fails silently forever | +| 15 | `ops/src/lib.rs` | re-exports | +| 16 | `core/src/graph.rs` | `Measure` / `measures` docs: append-only ordering, the abstention, `None`-inherits-boundaries | +| 17 | `textproj/src/lib.rs:59` | `COMPANION_VERSION` → `(0, 13, 0)` | +| 18 | `textproj/src/parse.rs` | **companion-version fixture only.** `project.rs` has **zero** operation-kind productions (verified: 0 matches for the G3a kinds, vs 4 in `ops/src/textproj_kind.rs`), so drafts 1–3 named the wrong file | +| 19 | `textproj/src/vectors.rs` | positive vector; negative anchors → `(0 12 0)` | +| 20 | `editor-core/src/barriers.rs` | one `subjects_of` arm — **the only authorized editor-crate change** | +| 21 | `layout-ir/src/barrier.rs:1179` | `tag: 39` → `40` | +| 22 | `testkit/tests/text_projection_grammar.rs:316`–`:317` | `40`, `"39 payload-free kinds…"` | +| 23 | `testkit/src/generators.rs` | draw arm; comment `30..=39`; `saw_create_measure` + assertion; **`precondition_failure_reason`'s `rng.below(14)` (`:417`) → `below(19)` with arms 14–18** — its doc comment claims "every core and registered variant" and is false today | +| 24 | `testkit/src/layout_stub.rs:1375` | comment `30..=39` | +| 25 | `testkit/src/roundtrip.rs` | **unconditional**: verify at ratification whether the kind list is enumerated there, and record the finding either way. Draft 2's "if the kind list is enumerated there" made a touch row depend on the implementer's reading | +| 26 | `testkit/tests/binary_format_history.rs` | `("G3b", "--- Genesis tranche G3b")` in `PRINCIPAL_MARKERS` | +| 27 | `spec/operation_catalog.tex` | `CreateMeasure` section incl. undo semantics; **the three new precondition reasons 16–18**; **document version bump + changelog entry** | +| 28 | `spec/binary_format.tex` | kind table, tag table, payload layout, **the three new `PreconditionFailureReason` discriminants 16–18**, **0.15.0 → 0.16.0**, Revision History row | +| 29 | `spec/core_spec.tex` | invariant 20 + the numbering prose, **and the normative `OperationKind` / `OperationKindTag` listings, which must gain `CreateMeasure`** — drafts 1–3 named only the invariant | +| 30 | `spec/text_projection.tex` | companion grammar, **plus the document title/version bump and a changelog entry** — not merely the grammar | +| 31 | four `.pdf` | regenerated | +| 32 | `spec/PLAN_GMINOR_SCHEMA_MINOR.md` | epoch 12 row; **pin 15's evidence repair** | +| 33 | `spec/PLAN_GENESIS_OPS.md` | ladder CLOSED | +| 34 | `spec/PASS13_CANDIDATES.md` | **P13-S18** (pin 7 abstention residue, **open**), **P13-S19** (pin 9 pickup deferral, **open**), **P13-S20** (the reasons-14/15 decoder hole, **RESOLVED in this rung**) | +| 35 | `core/DECISIONS.md`, `ops/DECISIONS.md` | the rung's record | +| 36 | this contract | DRAFT → RATIFIED | + +**No `epiphany-bundle` change** (pin 2: no accept-set move). No editor-crate +change beyond row 20. + +--- + +## §3. Mutation plan — seventy-five mutations, ratified + +Draft 2 named outcomes without edits for most rows. Every mutation below is a +**specific edit to a specific site**, and each must be **observed** red. +Reasoning that a mutation "would fail" signs nothing. + +### Numbering rule + +**One M = one edit.** Draft 3 claimed 52 while M48 and M50 each specified two +distinct edits, so the stated total was not a count of anything. Where one edit +is observed across several rows, that is stated and it remains **one** M. + +### Vocabulary — both spaces, all five epoch sites (M1–M10) + +Draft 2's single "t1" collapsed all of this into one tag-epoch mutation. There +are **two** discriminant spaces and **two** epoch functions for the kind +(`OperationKind::introduced_minor()` at `payload.rs:431`, the tag vocabulary's +at `:723`), plus three reasons with a discriminant and an epoch each. + +| M | Edit | +|---|---| +| M1 | `OperationKind::discriminant()` `CreateMeasure` 39 → 40 | +| M2 | tag vocabulary `CreateMeasure` 39 → 40 | +| M3 | `OperationKind::introduced_minor()` → `Some(11)` | +| M4 | tag vocabulary `@ Some(12)` → `Some(11)` | +| M5, M6, M7 | each new reason's discriminant → 19 (unused, so it compiles) | +| M8, M9, M10 | each new reason's `introduced_minor()` → `Some(11)` | + +### Wire (M11–M14) + +| M | Edit | Row | +|---|---|---| +| M11 | add `CreateMeasure(_) => 3` to `schema_major()` | a block containing **only** a `CreateMeasure` envelope stamps major **0**; `OperationEnvelopeBlock` stays 3. The block composition is load-bearing: a mixed block would stamp from another kind and pass with M11 applied | +| M12 | reorder `struct_codec!(Measure)`'s field list | frozen literal bytes | +| M13 | swap parent/value order in `CreateMeasureOp::encode_canonical` | frozen literal bytes. Signs pin 4 | +| M14 | **weaken the strict re-encode rejection** — make `decode_canonical` return the decoded value without comparing the re-encoding | strict-canonicality row | + +**M14 is not "remove `Measure` from `canonical_value!`".** That does not +compile: the new envelope decode arm calls `value::`, whose bound is +`T: CanonicalValue` (`envdecode.rs:224`), so dropping the macro entry breaks the +build and signs nothing. + +**And the row must use a structurally decodable but NONCANONICAL encoding.** A +field-swapped byte string is likely rejected by ordinary structural decoding, +which proves nothing about the macro's re-encode check. + +**The fixture is concrete and verified — the row is signable, with no +conditional.** Encode `Measure.start` as a `Musical` offset whose +`RationalTime` is spelled **unreduced, `2/4`**. That decodes cleanly: +`RationalTime`'s `decode_canonical` ends in +`Ok(RationalTime::from_big(BigRational::new(numer, denom)))` +(`time.rs:352`ff), and `BigRational::new` **reduces on construction**, so the +value becomes `1/2`. Re-encoding then emits `1/2`'s magnitudes, which differ +from the `2/4` bytes fed in — so the strict per-value re-encode comparison is +the *only* thing standing between the input and acceptance, which is exactly +what M14 must prove. + +Draft 5's "if none exists, mark the row unsignable" branch is **removed**: it +contradicted §3's no-deviation rule, and the encoding demonstrably exists. + +### Mint (M15–M22) + +| M | Edit | Row | +|---|---|---| +| M15 | skip the `measure_values` base seed | **base**-recarry only — it cannot reach the from-empty row | +| M16 | make the value comparison always report `identical` | differing re-carry observes `AlreadyApplied` | +| M17 | drop `StaffInstanceId` from the comparison | parent-mismatch re-carry. Signs pin 5 | +| M18 | remove the parent-liveness check | dead parent, graph-aware **and** base-free legs (ungated) | +| M19 | remove the `time_signature` resolution check | unresolvable signature | +| M20 | remove the `start`-referent resolution check | three observations — `Event`, `Measure`, `Region`. Signs pin 8.3 | +| M21 | append unconditionally | ordering precondition | +| M22 | report `MeasureOrderUnverifiable` for a comparable-but-reversed start | must observe the wrong reason. Signs pin 8b | + +### Comparability (M23–M26) + +| M | Edit | Row | +|---|---|---| +| M23 | allow `Musical` vs `WallClock` offsets | cross-clock must be `MeasureOrderUnverifiable` | +| M24 | widen c3 to admit `pos: End` or nonzero offsets | a nonzero-offset measure anchor must be **not comparable** | +| M25 | **order across differing `pos`/`edge`** (restore draft 3's `Start < End`) | `Region{Start, Musical(100)}` vs `Region{End, Zero}` must be **unverifiable**. Signs pin 6's identical-selector rule | +| M26 | **subtract offsets across differing `pos`/`edge`** in the delta | the delta must be **not computable**, not a wrong number. Signs pin 6b | + +### Grid oracle and the instance-grid ledger (M27–M31) + +| M | Edit | Row | +|---|---|---| +| M27 | on multiple mutually-incomparable maxima, pick by document order | must refuse (`MeasureOrderUnverifiable`), never fabricate a total order | +| M28 | drop **step 0**, letting an empty candidate set win | a sequence whose changes are **all incomparable** must be indeterminate, **not** vacuous. Signs pin 6c step 0 | +| M29 | treat an empty candidate set as a violation | a **genuinely empty** `meter_sequence` — no active signature is **vacuous**, not a violation | +| M30 | skip the `instance_grid` base seed | base-free must still see a local override | +| M30a | reconstruct the inherited grid from `metric_grid_chain` **only**, ignoring `meter_change_chain` | layered per-key changes must reach the oracle | +| M30b | in graph-aware mode, read the effective grid from `score` instead of the shared oracle | both modes must run the **same** oracle | +| M30c | overlay **current** per-key `meter_change_chain` values onto the **current** whole grid, ignoring write order | the two chains must interleave in **canonical order**. Tests must cover **both** interleavings: a `SetTimeSignature` **before** a later `SetMetricGrid` (the whole-grid write supersedes the earlier per-key change) and **after** it (the per-key change overlays). M30a/M30b/M31a are all satisfiable by an order-ignoring oracle, so this is a distinct obligation | +| M31 | read the region default while a local override exists | the override must win | +| M31a | omit prospective undo restorations from the reconstruction | the grid a restoration **would** install must govern the check | + +**Disposition (A) is ratified**, so M30, M30a, M30b, M31 and M31a all apply. + +**The total is 75.** The arithmetic has moved twice, each time because a +review round added a real obligation rather than because the count drifted: + +- **71 → 74.** (A) keeps M30/M31 as two rather than collapsing them, and the + confirmed `TempoSegment` double-site keeps M58 — but disposition (A)'s oracle + carries three obligations draft 4 had no mutations for: reconstruct + inheritance from **both** chains (M30a), fold in **prospective restorations** + (M31a), and run in **both** reduction modes (M30b). +- **74 → 75.** M30a/M30b/M31a are each satisfiable by an oracle that ignores + write order, so **canonical interleaving was required but unsigned**; + **M30c** signs it. + +Counted mechanically: **71 rows, two of which carry three ids each → 75 +distinct single-edit mutations.** + +### Create-side clauses (M32–M33) + +| M | Edit | Row | +|---|---|---| +| M32 | remove `create_measure`'s agreement check | disagreeing signature | +| M33 | remove `create_measure`'s **delta** check | a strictly-later start at the **wrong distance** must be refused — the immediate-violation gap | + +### Invariant 20 (M34–M40) + +| M | Edit | Row | +|---|---|---| +| M34 | remove the agreement clause | flags disagreement | +| M35 | remove the boundary clause | flags wrong distance | +| M36 | skip the boundary clause when `time_signature` is `None` | `None` inherits and is still bound | +| M37 | flag the incomparable case | abstention | +| M38 | flag a pickup first measure | deferral | +| M39 | add a resolution check to invariant 20 | an unresolvable reference is an invariant-**10** violation and **not** a 20 | +| M40 | delete invariant 20's arm from `check_invariants`' dispatch | asserted **behaviourally**; reverting `all()`'s count is a type error and signs nothing | + +### Preservation (M41–M47) + +| M | Edit | Row | +|---|---|---| +| M41 | remove `SetMetricGrid`'s agreement precondition | | +| M42 | remove `SetMetricGrid`'s boundary precondition | draft 2 checked agreement only | +| M43 | remove `SetTimeSignature`'s agreement precondition | | +| M44 | remove `SetTimeSignature`'s boundary precondition | | +| M45 | **move the checks back after `mint_time_signature`** (`reduce.rs:4518`) | a refused `SetTimeSignature` must leave **no residue** in `objects`, in any carried-value map, or in the graph. Signs pin 9c.2 | +| M46 | evaluate restorations **individually** under `StrictInverse` | a jointly-safe grid + meter-change set must be **applied**, and a jointly-unsafe individually-safe set must **conflict** — both observed | +| M47 | replace `BestEffort`'s canonical-order greedy with a naive per-restoration filter | the safe subset must be the documented deterministic one | + +### Undo — seven surface classes (M48–M62) + +| M | Edit | Row | +|---|---|---| +| M48 | remove the `TimeSignature`↔`Measure` strand extension | closes a hole live **today** | +| M49 | remove the `Measure` graph-removal arm | removal from the owning instance | +| M50 | rewrite the `Measure` strand guard against `structures` | **must go red**, proving the event-only index cannot see measure anchors (`reduce.rs:1755`–`:1765`) | +| M51 | remove the spanner surface | | +| M52 | remove the repeat surface | signs pin 10.4 — `CrossCuttingValue` has no `RepeatStructure` variant | +| M53 | skip the `repeat_values` base seed | | +| M54 | remove the **`measure_values`** surface | another `Measure.start` anchored to the undone measure | +| M55 | remove the **`meter_change_chain`** surface | | +| M56 | remove the **`break_chain`** surface | | +| M57 | remove the **`page_break_chain`** surface | | +| M58 | remove the **`tempo_segment_chain`** surface | **confirmed applicable**: `TempoSegment` carries `start: TimeAnchor` **and** `end: Option` (`tempo.rs:109`). Must be observed at **both** sites — a `start`-only guard misses a measure named as a segment's end | +| M59 | drop restoration-awareness on the shared prospective-value path, **reinstated-reference** direction (one edit) | must block. **Observed on all five restoration-capable surfaces**: spanner (`cross_cutting_modify_chain`), meter change, system break, page break, and tempo segment — the last at **both** anchor sites. Six observations, one mutant | +| M60 | drop the **added-anchor** direction on the same shared path (one edit) | must **not** block. Same five surfaces, same six observations | +| M61 | drop `!targets.contains(…)` from every surface guard (one edit) | observed on **all seven** surface rows — same-transaction teardown | +| M62 | drop the `Live` conjunct from every owned-surface guard (one edit) | observed on **seven** rows — the six always-owned classes **plus region-scoped (`Some(region)`) tempo segments**, which have a live `Region` owner. Only **score-level (`None`) tempo** is N/A. M58 cannot substitute here: it removes tempo handling wholesale and so cannot detect a *missing `Live` conjunct* in tempo handling that is present | + +**Restoration is N/A for exactly two surfaces.** `repeat_values` and +`measure_values` are **immutable value maps**, not `WriteChain`s — nothing +rewrites a repeat's or a measure's anchors in place, so there is no prospective +post-undo value that could differ from the stored one. Drafts 4–5 left M59/M60 +as single generic observations, which signed the *mechanism* but not its +*coverage*: a prospective-value path wired up for spanners only would have +passed both. One mutant each is still correct — the path is shared — but **every +applicable row must observe it**, and the two N/A surfaces must be named as N/A +rather than silently absent. + +### Post-undo re-create ordering (M63) + +| M | Edit | Row | +|---|---|---| +| M63 | make `create_measure`'s `Tombstoned` arm fall through to the value-map identity check | a **byte-identical** re-carry after undo must observe `TargetTombstoned`, not `AlreadyApplied`. Draft 3 had regression coverage here and **no killing mutation** | + +### Precondition-reason decoding (M64–M66) — and a live bug + +`decode.rs`'s `precondition_reason` (`:205`) ends at **13**. Reasons **14 and +15 already encode but cannot decode**, so a materialized effect carrying either +fails canonical round-trip **today**. Nothing caught it because +`generators.rs:417` draws `rng.below(14)` under a doc comment claiming "every +core and registered variant." + +| M | Edit | Row | +|---|---|---| +| M64 | revert the decoder to end at 13 | reasons 16–18 round-trip | +| M65 | remove decoder arms **14/15** | the pre-existing hole, now regression-locked (**P13-S20**) | +| M66 | revert `generators.rs` `below(19)` → `below(14)` | the generator must actually reach every variant its doc claims | + +### Sentinels and bookkeeping (M67–M71) + +| M | Edit | Row | +|---|---|---| +| M67 | `barrier.rs` `tag: 40` → `39` | invalid-tag sentinel | +| M68 | revert the grammar count | grammar sentinel | +| M69 | revert the `saw_create_measure` assertion | generator sentinel | +| M70 | revert `COMPANION_VERSION` | companion accepts 0.13.0, rejects `(0 12 0)` | +| M71 | delete the G3b Revision History row | the `binary_format_history.rs` guard | + +**Ratified total: 75 mutations, one edit each.** Every former conditional is +resolved — disposition (A) is ratified, `TempoSegment` carries two anchor sites, +and M14's fixture is concrete — so **no deviation clause remains anywhere in +this contract**. Report the observed count; any deviation from 75 is a finding. + +Prose-only sentinel sites (`layout_stub.rs:1375`, `QUICKSTART.md:56`) have no +mutation; they are covered by the §4 grep gate, and the report must say so +rather than implying mutation coverage. + +### Not mutations — gates + +Draft 2 listed these among the mutations, which overstated its own coverage: + +- **Frozen-corpus byte stability**: `spec/vectors/*.txt` gain entries and **no + existing vector's bytes move** — a corpus **diff gate**, verified by + inspecting the diff, not by mutating anything. +- **PDF title pages** read the bumped versions. + +### The `valuegen` fixture trap, from G3a + +G3a's `analysis_layer` fixture happened to encode a name at exactly 16 bytes — +the same width as the id — making a field-swap mutation **byte-invisible**. +`Measure` has five fields including two `Option`s and a `TimeAnchor`. Choose +fixture values whose encodings are **mutually distinguishable in length**, and +state in the report that this was checked. M12/M13's frozen literals are what +make it matter. + +### Anti-traps + +A mutation that does not compile signs nothing. A mutation in an operation that +runs *before* the asserted state cannot reach it. A symmetric encode/decode +change is invisible to round-trip assertions. A grep guard must be sliced so it +cannot match its own citation, and *name-presence* is not enough when the name +already appears in neighbouring prose. **A guard written against an index that +structurally cannot hold the referent is born green.** A row asserted but not +observed red is unsigned. + +--- + +## §4. Gate + +- `cargo test --workspace` — count compared to the **1454** baseline at + `6170015`, delta explained. +- `cargo clippy --workspace --all-targets` — zero warnings. +- `cargo fmt --check` — clean. +- All **75** mutations observed red and restored by hand-editing back. +- The two corpus diff gates and the four PDF title pages. +- A grep gate over the prose-only sentinels (`layout_stub.rs:1375`, + `QUICKSTART.md:56`) confirming no stale count survives. +- Whitespace per §5. + +## §5. Whitespace and staging + +1. Stage the touch-table files explicitly. **Never `git add -A`.** +2. **`git diff --cached --check`** — catches staged and formerly-untracked + files. `git diff --check` alone is blind to both, which is how this + session's own contract sat untracked past a "clean" gate. +3. Commit. +4. `git diff --check ..HEAD -- crates/ spec/` — path-scoped, so the + pre-existing `spikes/` failure cannot mask a real one. + +**A concurrent session commits to this repository.** Re-check `HEAD` before +committing, commit with an explicit pathspec, and never run `git reset`, +`git restore --staged`, `git checkout`, or `git stash` against the shared +index. + +## §6. Boundary — unchanged and absolute + +MUST NOT be read, written, or staged: `spec/PLAN_EDITOR_APP.md`, +`spec/CONTRACT_EDITOR_*.md`, `spec/ANALYSIS_GENESIS_PERSISTENCE.md`, +`spec/ANALYSIS_TEXT_RUN_PRIMITIVES.md`, `spec/DRAFT_T4_FIXTURE_RECIPE.md`, +`crates/epiphany-editor-gui/goldens/*.png`, `crates/epiphany-render-svg/**`, +`crates/epiphany-glyphs/**`, +`crates/epiphany-testkit/benches/editor_pipeline.rs`, the entire `spikes/` +tree, the root `Cargo.toml` change, `.claude/worktrees/`. + +**Touch-table row 20** (`barriers.rs`, one `subjects_of` arm) is the **only** +authorized editor-crate change, and it does not generalize beyond this packet. + +## §7. Report requirements + +Baseline and final counts; **all 75 mutations** with their **observed** failing +test names; the four PDF title pages; anything the contract did not anticipate. + +Confirm explicitly: +- `schema_major()` gained **no** arm, and the wire row used a + **`CreateMeasure`-only** block; +- the golden lock still reads `; 30]`; +- the evidence chain lists **only** the three vocabulary-introducing events; +- **all nine** integration files in rows 6–14 were touched, plus row 12a + (`decode.rs`), row 14a (`QUICKSTART.md`), and `core/DECISIONS.md` in row 35 — + which is the tenth omitted G3a-footprint file; +- that disposition **(A)** was implemented — the instance ledger **plus** the + shared oracle reconstructing inheritance from `metric_grid_chain` and + `meter_change_chain` in canonical order, folding in prospective restorations, + and running in **both** reduction modes (M30a, M30b, M31a); +- that M58 was observed at **both** `TempoSegment` anchor sites; +- that M14 used the ratified unreduced-`RationalTime` (`2/4`) fixture and + observed the strict re-encode rejection; +- that **no mint precedes any invariant check** in either setter, and that a + refusal leaves no residue in `objects`, the value maps, or the graph; +- that restoration safety was evaluated in **aggregate**, with `BestEffort`'s + subset chosen by the documented canonical-order greedy; +- that **all seven** surface classes have guards, with the per-surface owner + rule stated — M62 observed on **seven** rows (six always-owned plus + region-scoped tempo), with only score-level (`None`) tempo N/A; +- that M30c observed **both** grid-chain interleavings; +- that M59 and M60 were each observed on **all five** restoration-capable + surfaces — six observations apiece, tempo at both anchor sites — and that + `repeat_values` and `measure_values` were recorded as **N/A** for restoration + rather than silently omitted; +- the physical site counts actually implemented: `measure_values` **7**, + `instance_grid` **7**, `repeat_values` **7** — **no delete site on any of + them**, and tombstoned values deliberately retained (pin 10.4); +- that M62's repeat observation used a state where a **tombstoned repeat's + value is retained** and still names the measure; +- that `decode.rs` decodes reasons **14 through 18** and `generators.rs` draws + all of them — **P13-S20** filed as RESOLVED; +- whether `roundtrip.rs` enumerates kinds (row 25) — **report the finding + either way**; +- **no existing vector's bytes moved** in either frozen corpus; +- the `valuegen` fixture's field encodings are length-distinguishable; +- M50 was observed red — the strand guard genuinely cannot be written against + the event-only `structures` index; +- **P13-S18** and **P13-S19** were filed with status **open**. + +If a pin is wrong or unsatisfiable, **stop and say so.** Pins 2, 9b, and 12 +are prohibitions. Pin 6 forbids inventing a comparable relation beyond its five +shapes, and pin 6b forbids treating an ordering as a delta — the correct +response to an incomputable case is to refuse at the operation and abstain at +the invariant. Pin 9c widens this rung into two pre-existing operations, both +undo policies, and one pre-existing undo hole; **if that scope is wrong, say so +before writing code** rather than delivering an invariant nothing maintains. diff --git a/spec/text_projection.tex b/spec/text_projection.tex index 1cd58a0..e922374 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.12.0 --- The four remaining genesis root-level entity mints reach the grammar}\\[4pt] + {\normalsize\color{epiphanyink}Version 0.13.0 --- The genesis ladder closes: \texttt{create-measure} reaches the grammar}\\[4pt] {\small\color{epiphanyslate}Normative for the text form it defines} \vfill \end{titlepage} @@ -483,7 +483,7 @@ A projection is, in order: \begin{requirement} \label{req:textproj:header-version} A parser implementing this companion \MUST{} accept exactly one header - version: \texttt{(0 12 0)}, the version of the companion it implements. It + version: \texttt{(0 13 0)}, the version of the companion it implements. It \MUST{} reject any other version at line one. Multi-version acceptance and text migrate-on-read are deferred in the same @@ -534,7 +534,7 @@ projection introduces no ordering of its own. A parser \MUST{} reject every \texttt{(blob ...)} line whose blob is unreferenced by canonical state (Requirement~\ref{req:textproj:canonical-blobs}). At companion - version~0.12.0, neither a canonical operation nor canonical reduced state can + version~0.13.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} @@ -1066,6 +1066,7 @@ kind ::= "(insert-event " bytes " " value ")" | "(create-part-definition " value ")" | "(create-analysis-layer " value ")" | "(create-view " value ")" + | "(create-measure " value ")" tuplet-comp ::= "not-in-tuplet" | "(replace-with-rest " value ")" | "(rewrite-tuplets (" bytes* "))" @@ -1142,7 +1143,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 12 0)) +(text-projection (0 13 0)) (document #x05050505050505050505050505050505 (schema 0 1)) (profile full (0 1 0) (constraints 67108864 (retention 1 () true))) (canonical-base #x1f8b0000000000000000000000000000 #x 1 full (schema 0 1) #x0000) diff --git a/spec/vectors/decode_vectors.txt b/spec/vectors/decode_vectors.txt index 00c9add..6b0ccf0 100644 --- a/spec/vectors/decode_vectors.txt +++ b/spec/vectors/decode_vectors.txt @@ -76,8 +76,9 @@ ops.operation_kind_tag accept - tag_35 23 ops.operation_kind_tag accept - tag_36 24 ops.operation_kind_tag accept - tag_37 25 ops.operation_kind_tag accept - tag_38 26 +ops.operation_kind_tag accept - tag_39 27 ops.operation_kind_tag accept - registered 1000000000000000000123456789abcdef -ops.operation_kind_tag reject unknown-discriminant tag_39_one_past_the_vocabulary 27 +ops.operation_kind_tag reject unknown-discriminant tag_40_one_past_the_vocabulary 28 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 @@ -100,6 +101,8 @@ ops.operation_envelope accept - create_analysis_layer 00000000000000010000000000 ops.operation_envelope reject trailing-bytes create_analysis_layer_trailing 00000000000000010000000000000007000000000000000000000000000000000100000000000000010000000000000000000001000000000000000700000000000000000000251f0000001000000000000000000000010000000000000001070000006c617965722d3100 ops.operation_envelope accept - create_view 000000000000000100000000000000080000000000000000000000000000000001000000000000000100000000000000000000010000000000000008000000000000000000002636000000100000000000000000000001000000000000000106000000766965772d31010000001000000000000000000000010000000000000001 ops.operation_envelope reject trailing-bytes create_view_trailing 000000000000000100000000000000080000000000000000000000000000000001000000000000000100000000000000000000010000000000000008000000000000000000002636000000100000000000000000000001000000000000000106000000766965772d3101000000100000000000000000000001000000000000000100 +ops.operation_envelope accept - create_measure 0000000000000001000000000000000900000000000000000000000000000000010000000000000001000000000000000000000100000000000000090000000000000000000027000000000000000100000000000000013c0000001000000000000000000000010000000000000001030800000000ca9a3b00000000011000000000000000000000010000000000000001010100000000 +ops.operation_envelope reject trailing-bytes create_measure_trailing 0000000000000001000000000000000900000000000000000000000000000000010000000000000001000000000000000000000100000000000000090000000000000000000027000000000000000100000000000000013c0000001000000000000000000000010000000000000001030800000000ca9a3b0000000001100000000000000000000001000000000000000101010000000000 # bundle.manifest bundle.manifest accept - empty_manifest 6f9e7d11689ab113c4a1f05faf60fe60050505050505050505050505050505050000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000001000000000000000000000400000000010000000001000000000000 diff --git a/spec/vectors/textproj_document_vectors.txt b/spec/vectors/textproj_document_vectors.txt index 6eddde0..dd36420 100644 --- a/spec/vectors/textproj_document_vectors.txt +++ b/spec/vectors/textproj_document_vectors.txt @@ -22,21 +22,22 @@ # document bytes are normative. `` is lowercase with no separators. # textproj.document -textproj.document accept - minimal 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a -textproj.document accept - set_tuning_context 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830353035303530353035303530353035303530353035303530353035303530352028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303037202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203730302030202378303030303030303030303030303030313030303030303030303030303030303729202863617573616c2028292028292920282920287072696d697469766520287365742d74756e696e672d636f6e74657874202874756e696e672d636f6e746578742d73657474696e67732022636d6e2d31322220227465742d31322220287265666572656e63652d70697463682028636d6e2061203020342920237830303030303030303030663037623430292028736d75666c2d76657273696f6e2d726571756972656d656e742028736d75666c2d76657273696f6e2031203430292028736d75666c2d76657273696f6e20312034302929202829292929290a -textproj.document accept - lineage_custom_profile 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a -textproj.document accept - extension_base_two_envelopes 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a -textproj.document accept - rich_document 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a -textproj.document accept - create_staff_group 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830363036303630363036303630363036303630363036303630363036303630362028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303038202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203830302030202378303030303030303030303030303030313030303030303030303030303030303829202863617573616c2028292028292920282920287072696d697469766520286372656174652d73746166662d67726f7570202873746166662d67726f757020237830303030303030303030303030303031303030303030303030303030303030312028736f6d65202273746166662d67726f75702d312229206772616e642d737461666620282378303030303030303030303030303030313030303030303030303030303030303129292929290a -textproj.document accept - create_part_definition 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830373037303730373037303730373037303730373037303730373037303730372028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303039202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203930302030202378303030303030303030303030303030313030303030303030303030303030303929202863617573616c2028292028292920282920287072696d697469766520286372656174652d706172742d646566696e6974696f6e2028706172742d646566696e6974696f6e20237830303030303030303030303030303031303030303030303030303030303030312022706172742d312220282378303030303030303030303030303030313030303030303030303030303030303129292929290a -textproj.document accept - create_analysis_layer 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830383038303830383038303830383038303830383038303830383038303830382028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303061202378303030303030303030303030303030303030303030303030303030303030616220287374616d7020313030302030202378303030303030303030303030303030313030303030303030303030303030306129202863617573616c2028292028292920282920287072696d697469766520286372656174652d616e616c797369732d6c617965722028616e616c797369732d6c61796572202378303030303030303030303030303030313030303030303030303030303030303120226c617965722d3122292929290a -textproj.document accept - create_view 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830393039303930393039303930393039303930393039303930393039303930392028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303062202378303030303030303030303030303030303030303030303030303030303030616220287374616d7020313130302030202378303030303030303030303030303030313030303030303030303030303030306229202863617573616c2028292028292920282920287072696d697469766520286372656174652d766965772028766965772d646566696e6974696f6e20237830303030303030303030303030303031303030303030303030303030303030312022766965772d312220282378303030303030303030303030303030313030303030303030303030303030303129292929290a -textproj.document reject wrong-header-version superseded_companion_version 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a -textproj.document reject blob-line unreferenced_blob 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28626c6f6220226170706c69636174696f6e2f6f637465742d73747265616d222028292023783031290a -textproj.document reject out-of-order-sections canonical_base_before_extension 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a -textproj.document reject repeated-singular-section lineage_repeated 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a -textproj.document reject operation-envelope-order envelopes_reversed 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a -textproj.document reject profile-declaration-order profiles_reversed 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a -textproj.document reject extension-declaration-order extensions_reversed 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a -textproj.document reject extension-chunk-order extension_chunks_reversed 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a -textproj.document reject missing-trailing-lf final_lf_missing 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e20312028292074727565292929 +textproj.document accept - minimal 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a +textproj.document accept - set_tuning_context 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830353035303530353035303530353035303530353035303530353035303530352028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303037202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203730302030202378303030303030303030303030303030313030303030303030303030303030303729202863617573616c2028292028292920282920287072696d697469766520287365742d74756e696e672d636f6e74657874202874756e696e672d636f6e746578742d73657474696e67732022636d6e2d31322220227465742d31322220287265666572656e63652d70697463682028636d6e2061203020342920237830303030303030303030663037623430292028736d75666c2d76657273696f6e2d726571756972656d656e742028736d75666c2d76657273696f6e2031203430292028736d75666c2d76657273696f6e20312034302929202829292929290a +textproj.document accept - lineage_custom_profile 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a +textproj.document accept - extension_base_two_envelopes 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a +textproj.document accept - rich_document 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a +textproj.document accept - create_staff_group 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830363036303630363036303630363036303630363036303630363036303630362028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303038202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203830302030202378303030303030303030303030303030313030303030303030303030303030303829202863617573616c2028292028292920282920287072696d697469766520286372656174652d73746166662d67726f7570202873746166662d67726f757020237830303030303030303030303030303031303030303030303030303030303030312028736f6d65202273746166662d67726f75702d312229206772616e642d737461666620282378303030303030303030303030303030313030303030303030303030303030303129292929290a +textproj.document accept - create_part_definition 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830373037303730373037303730373037303730373037303730373037303730372028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303039202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203930302030202378303030303030303030303030303030313030303030303030303030303030303929202863617573616c2028292028292920282920287072696d697469766520286372656174652d706172742d646566696e6974696f6e2028706172742d646566696e6974696f6e20237830303030303030303030303030303031303030303030303030303030303030312022706172742d312220282378303030303030303030303030303030313030303030303030303030303030303129292929290a +textproj.document accept - create_analysis_layer 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830383038303830383038303830383038303830383038303830383038303830382028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303061202378303030303030303030303030303030303030303030303030303030303030616220287374616d7020313030302030202378303030303030303030303030303030313030303030303030303030303030306129202863617573616c2028292028292920282920287072696d697469766520286372656174652d616e616c797369732d6c617965722028616e616c797369732d6c61796572202378303030303030303030303030303030313030303030303030303030303030303120226c617965722d3122292929290a +textproj.document accept - create_view 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830393039303930393039303930393039303930393039303930393039303930392028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303062202378303030303030303030303030303030303030303030303030303030303030616220287374616d7020313130302030202378303030303030303030303030303030313030303030303030303030303030306229202863617573616c2028292028292920282920287072696d697469766520286372656174652d766965772028766965772d646566696e6974696f6e20237830303030303030303030303030303031303030303030303030303030303030312022766965772d312220282378303030303030303030303030303030313030303030303030303030303030303129292929290a +textproj.document accept - create_measure 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830613061306130613061306130613061306130613061306130613061306130612028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303063202378303030303030303030303030303030303030303030303030303030303030616220287374616d7020313230302030202378303030303030303030303030303030313030303030303030303030303030306329202863617573616c2028292028292920282920287072696d697469766520286372656174652d6d656173757265202378303030303030303030303030303030313030303030303030303030303030303120286d6561737572652023783030303030303030303030303030303130303030303030303030303030303031202877616c6c2d636c6f636b2031303030303030303030292028736f6d652023783030303030303030303030303030303130303030303030303030303030303031292028736f6d65203129206175746f292929290a +textproj.document reject wrong-header-version superseded_companion_version 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a +textproj.document reject blob-line unreferenced_blob 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28626c6f6220226170706c69636174696f6e2f6f637465742d73747265616d222028292023783031290a +textproj.document reject out-of-order-sections canonical_base_before_extension 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a +textproj.document reject repeated-singular-section lineage_repeated 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a +textproj.document reject operation-envelope-order envelopes_reversed 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a +textproj.document reject profile-declaration-order profiles_reversed 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a +textproj.document reject extension-declaration-order extensions_reversed 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a +textproj.document reject extension-chunk-order extension_chunks_reversed 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a +textproj.document reject missing-trailing-lf final_lf_missing 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e20312028292074727565292929