diff --git a/crates/epiphany-bundle/DECISIONS.md b/crates/epiphany-bundle/DECISIONS.md index f7a3981..0c65831 100644 --- a/crates/epiphany-bundle/DECISIONS.md +++ b/crates/epiphany-bundle/DECISIONS.md @@ -644,3 +644,45 @@ declaration order (`core_spec` §"Extension Declarations"), which had chunks bef kinds and barriers. Still no implementation. The companion is now complete enough to implement against. + +## Genesis tranche G2b: op-block accept-set raised to [0, 3] (2026-07-28) + +`spec/CONTRACT_GENESIS_G2B_TUNING.md` charges this crate alone with the raise: +`max_supported_major(OperationEnvelopeBlock)` → 3. `SetTuningContext` +(`epiphany-ops`) is the sole genesis-tranche payload born at schema major 3 — +its carried `epiphany_core::TuningContextSettings` has mandatory (not +`Option`-hidden) appends past major 2, so a block carrying one is now born at +v3. This is the one-way door the governing plan (`PLAN_GENESIS_OPS.md` §4) +warned against burying inside routine work: G2 was split into G2a/G2b +precisely so the two major-0 setters (G2a) would not carry this raise, and it +lands alone here instead. + +**The doc comment above `max_supported_major` asserted a rationale this rung +falsifies, and it had to move with the number, not just the number itself.** +The prior text ("Schema major 3 ... does not raise this role: no operation +payload embeds the tuning context, so no op block is ever born at v3") is now +false. Rewritten, and mutation-verified (`accept_set_doc_no_longer_claims_ +no_payload_embeds_the_tuning_context`, `bundle.rs`) that the exact stale +sentence is absent from the source — not merely superseded by newer prose +elsewhere in the file, which a reader skimming only the doc comment could +still miss. This is the `binary_format.tex:2373` lesson applied to Rust doc +comments, not just the LaTeX companion. + +**One existing test needed a value bump as a direct consequence, not a +touch-table item.** `committing_an_unsupported_major_op_root_makes_the_live_ +bundle_read_only` staged a block at major 3 to exercise the "beyond the +accept-set" read-only path; major 3 is now *inside* the accept-set, so the +test silently stopped testing what its name claims (it would still pass, +vacuously, for the wrong reason) — caught by running the full bundle suite +after the raise, not by the touch table, which did not name this test. +Moved to major 4, with a comment explaining why 3 no longer works. This is +exactly the kind of collateral fix the touch table's "floor, not ceiling" +framing anticipates: a file not listed can still need an edit when a change +elsewhere makes its assumption stale. + +**No `epiphany-bundle` participation in the never-authored/authored-to-default +question (pin 5).** This crate has no visibility into operation semantics — +`edit_barriers` and the op-block bytes are opaque to it — so pin 5's +seeded-undo discipline is entirely `epiphany-ops`'s concern. This crate's +only stake is that the raise is now paid for real, once, by the value that +actually needs it. diff --git a/crates/epiphany-bundle/src/bundle.rs b/crates/epiphany-bundle/src/bundle.rs index 83ccaf9..ae14249 100644 --- a/crates/epiphany-bundle/src/bundle.rs +++ b/crates/epiphany-bundle/src/bundle.rs @@ -50,23 +50,26 @@ pub const SUPPORTED_SCHEMA_MAJOR: u16 = 0; /// bound of its per-role accept-set `[0, max]` (Binary Format companion /// §"Schema Major 1", "The accept-set gate"). /// -/// `OperationEnvelopeBlock` admits major 2 (schema major 2 fills the +/// `OperationEnvelopeBlock` admits major 3 (schema major 2 fills the /// cross-cutting/staff/metadata bodies its payloads embed; major 1 embedded a /// v1 `CreateRegion`; the reader treats the block bytes opaquely, so it -/// parses a higher-major block without decoding the payload). Schema major 3 -/// (Push 4b tranche 3b-i) does **not** raise this role: no operation payload -/// embeds the tuning context, so no op block is ever born at v3. `Snapshot` -/// admits major 3 for the acceleration full-`Score` form (decoded through -/// the core versioned seam); the canonical BASE carried under the same kind -/// must stay major 0, enforced per role. Every other role stays at -/// [`SUPPORTED_SCHEMA_MAJOR`] until its own versioned path lands — the -/// layout cache, the operation index, and the manifest (carried opaquely, +/// parses a higher-major block without decoding the payload). **Schema major +/// 3 is raised by genesis tranche G2b** (`spec/CONTRACT_GENESIS_G2B_TUNING.md`): +/// `SetTuningContext` is the sole operation payload that embeds the tuning +/// context (`epiphany_core::TuningContextSettings`, born at major 3 +/// unconditionally), so an op block carrying one is now born at v3 — this +/// superseded the earlier Push 4b tranche 3b-i note claiming no payload ever +/// would. `Snapshot` admits major 3 for the acceleration full-`Score` form +/// (decoded through the core versioned seam); the canonical BASE carried +/// under the same kind must stay major 0, enforced per role. Every other role +/// stays at [`SUPPORTED_SCHEMA_MAJOR`] until its own versioned path lands — +/// the layout cache, the operation index, and the manifest (carried opaquely, /// never grows a versioned layout). A chunk above its role's max is not /// admitted; for a **canonical** role that means the bundle opens read-only /// (a lower-major-only reader meeting a newer op block), not a hard reject. pub fn max_supported_major(kind: ChunkKind) -> u16 { match kind { - ChunkKind::OperationEnvelopeBlock => 2, + ChunkKind::OperationEnvelopeBlock => 3, // The payload-polymorphic Snapshot role: the acceleration // full-`Score` form is decoded through the core versioned seam // (`Score::decode_canonical_versioned`, majors {0,1,2,3}). The @@ -1380,14 +1383,17 @@ mod tests { // CreateRegion), and schema major 2 to major 2 (a block bearing a v2 // cross-cutting/staff/metadata value); every other role stays exact-0 // until its own versioned path lands, and the manifest stays major 0 - // forever. Schema major 3 (Push 4b tranche 3b-i, §"Schema Major 3") - // raises only the snapshot role — no operation payload embeds the - // tuning context, so the op-block role's admission is untouched. + // forever. Genesis tranche G2b (`spec/CONTRACT_GENESIS_G2B_TUNING.md`) + // raises the op-block role to major 3: `SetTuningContext` is the sole + // operation payload that embeds the tuning context, born at major 3 + // unconditionally, so a block carrying one is now born at v3 — this + // supersedes the earlier Push 4b tranche 3b-i note claiming no + // payload ever would. assert_eq!(SchemaVersion::V1.major, 1); assert_eq!(SchemaVersion::V2.major, 2); assert_eq!(SchemaVersion::V3.major, 3); - // The op-block role admits [0, 2] — unchanged by schema major 3. - assert_eq!(max_supported_major(ChunkKind::OperationEnvelopeBlock), 2); + // (t4) The op-block role admits [0, 3] as of genesis tranche G2b. + assert_eq!(max_supported_major(ChunkKind::OperationEnvelopeBlock), 3); // The snapshot role admits the major-3 acceleration form (decoded // through the core versioned seam); the canonical BASE stays major 0 // per role (`mis_stamped_canonical_base`). The remaining roles stay @@ -1401,16 +1407,45 @@ mod tests { assert_eq!(Manifest::SCHEMA.major, 0); } + /// (t10) `CONTRACT_GENESIS_G2B_TUNING.md` pin 3: the doc comment above + /// `max_supported_major` used to assert a rationale G2b falsifies (no + /// operation payload reaches the tuning context, so no op block would + /// ever reach schema major 3). That claim must be gone from the source, + /// not merely superseded in prose elsewhere; a stale rationale beside a + /// corrected constant is exactly how `binary_format.tex:2373` decayed. + /// + /// **Mutation:** restore the stale sentence into the doc comment; must + /// fail. + #[test] + fn accept_set_doc_no_longer_claims_no_payload_embeds_the_tuning_context() { + let source = include_str!("bundle.rs"); + let stale_claim: String = ["no operation payload ", "embeds the tuning context"].concat(); + let stale_consequence: String = ["no op block is ever ", "born at v3"].concat(); + assert!( + !source.contains(&stale_claim), + "the doc comment above max_supported_major must not assert the falsified claim anymore" + ); + assert!( + !source.contains(&stale_consequence), + "the doc comment above max_supported_major must not assert the falsified consequence anymore" + ); + } + #[test] fn committing_an_unsupported_major_op_root_makes_the_live_bundle_read_only() { // A commit publishes an op block beyond this reader's accept-set (a // forward-compat write): structural validation lets it through, but the // LIVE bundle must go read-only at once — not only on the next reopen — // so no further commit runs against canonical history it cannot parse. + // + // Major 4, not 3: genesis tranche G2b raised the op-block accept-set + // to [0, 3] (`SetTuningContext` is born at major 3), so major 3 is now + // admitted and this test's "future major" must move past it to stay + // an actual test of the read-only-on-overflow path. let mut bundle = fresh_bundle(); let block = StagedChunk::operation_block_versioned( crate::block::encode_block(&[vec![1u8, 2, 3]]), - SchemaVersion::new(3, 0), + SchemaVersion::new(4, 0), ); bundle .commit(&[block], |ctx| { @@ -1426,7 +1461,7 @@ mod tests { ); assert!(bundle.anomalies().iter().any(|a| matches!( a, - IntegrityAnomaly::UnsupportedCanonicalChunkMajor { schema_major: 3 } + IntegrityAnomaly::UnsupportedCanonicalChunkMajor { schema_major: 4 } ))); // A further commit against the now-read-only bundle is refused. let more = StagedChunk::operation_block_versioned( diff --git a/crates/epiphany-core/DECISIONS.md b/crates/epiphany-core/DECISIONS.md index cad85fd..5645589 100644 --- a/crates/epiphany-core/DECISIONS.md +++ b/crates/epiphany-core/DECISIONS.md @@ -1441,3 +1441,53 @@ in the *containing* `Canvas` walk (`dec_canvas_v0` default-fills the field; itself. So neither op gains a `schema_major()` arm — both fall into the existing `_ => 0` catch-all in `epiphany-ops`. This is unlike G1's `CreateInstrument`, whose carried `Instrument` has mandatory major-2 appends. + +## Genesis tranche G2b — `TuningContextSettings`, the subset type +## (2026-07-28) + +`spec/CONTRACT_GENESIS_G2B_TUNING.md` §1 (RATIFIED, resolving +`spec/PLAN_GENESIS_OPS.md` §3's open pin) adds `SetTuningContext` to +`epiphany-ops`, the G2b rung of the genesis ladder. Unlike G1 and G2a, its +carried payload is **not** the full `ScoreTuningContext` — it is a new type, +`epiphany_core::TuningContextSettings`, holding exactly the five fields +`ScoreTuningContext`'s `Codec` actually walks onto the wire +(`default_pitch_space`, `default_tuning_system`, `reference`, `smufl`, +`overrides`), in that same order. + +**Why a new type, not the full value or a normalizing construction check.** +`ScoreTuningContext`'s `Codec` deliberately drops `accidental_extensions` on +encode and default-fills it to `Vec::new()` on decode (the field is staged out +of schema major 3). `OperationSet::accept` stores the authored envelope as a +**value**, so a `SetTuningContext` carrying the full `ScoreTuningContext` +would reduce with `accidental_extensions` intact on the authoring replica and +empty on any replica that received the document through serialization — the +same document in two graph states, depending only on whether you just +authored it. `canonical_value!`'s generated `decode_canonical` cannot catch +this: it compares decode → `finish()` → re-encode bytes, never the +originating value, so a field that never reaches the bytes is invisible to +it. + +Normalization (clearing the field at construction) and reject-on-non-empty +were both rejected: normalization makes correctness depend on remembering to +clear a field at every construction site, enforced by nothing the compiler or +codec can see — a shape this track has been burned by twice already (four +stale literal sites at Push 4a, six found during G2a); reject-on-non-empty +turns an in-memory-only field into an authoring error for callers who never +opted into persistence. The subset type makes the divergence +**unrepresentable**: a field that does not exist cannot be set wrongly. + +**No new byte layout.** `TuningContextSettings`'s `Codec` is byte-identical to +`ScoreTuningContext`'s existing five-field walk by construction — same five +fields, same order, same per-field codecs — asserted directly by +`tuning_context_settings_canonical_bytes_match_score_tuning_context` +(`codec.rs`). This is a type-level narrowing, not a new wire form, so +`canonical_value!` still applies and the G1/G2a payload template is +unchanged. One more `canonical_value!` line makes it reachable per-value. + +`SetTuningContext` **does** gain a real `schema_major()` arm returning 3, +unconditionally — the opposite of G2a. `ScoreTuningContext`'s wire form is +born at schema major 3 and its appends (`smufl`, `overrides`) are mandatory, +not `Option`-hidden, so there is no lower-major layout for this payload to +fall back to. This is the sole surface among the nine genesis-tranche +settings/creates that drags `OperationEnvelopeBlock`'s accept-set from 2 to 3 +(`epiphany-bundle`'s `DECISIONS.md`). diff --git a/crates/epiphany-core/src/codec.rs b/crates/epiphany-core/src/codec.rs index e51aaff..3d555f9 100644 --- a/crates/epiphany-core/src/codec.rs +++ b/crates/epiphany-core/src/codec.rs @@ -61,8 +61,8 @@ use crate::graph::{ SpannerKind, Staff, StaffBasedContent, StaffBracketKind, StaffExtent, StaffGroup, StaffGroupKind, StaffInstance, StaffLineConfiguration, StemDirection, SubBeam, TempoMapReference, TextLineDefinition, Tie, TieClass, TimeExtent, TimeSignature, - TimeSignatureDisplay, Timestamp, Tuplet, TupletRatio, UnpitchedMember, ViewDefinition, Voice, - VoiceOrigin, Volta, + TimeSignatureDisplay, Timestamp, TuningContextSettings, Tuplet, TupletRatio, UnpitchedMember, + ViewDefinition, Voice, VoiceOrigin, Volta, }; use crate::ids::{ AnalysisLayerId, AnalyticalAnnotationId, BarlineAlignmentGroupId, BeamId, ChordSymbolId, @@ -1984,6 +1984,40 @@ impl Codec for ScoreTuningContext { }) } } + +// `TuningContextSettings` (genesis tranche G2b, +// `spec/CONTRACT_GENESIS_G2B_TUNING.md` §1): the authored subset of +// `ScoreTuningContext` that `SetTuningContext` carries — exactly its five +// wire-bearing fields, in the codec's existing order. This walk is +// **byte-identical** to `ScoreTuningContext`'s `enc`/`dec` above by +// construction (same five fields, same order, same per-field codecs) — a +// type-level narrowing, not a new wire form. The identity is asserted by a +// round-trip test just below +// (`tuning_context_settings_canonical_bytes_match_score_tuning_context`). +impl Codec for TuningContextSettings { + fn enc(&self, out: &mut Vec) { + self.default_pitch_space.enc(out); + self.default_tuning_system.enc(out); + self.reference.enc(out); + self.smufl.enc(out); + self.overrides.enc(out); + } + fn dec(r: &mut Reader<'_>) -> Result { + let default_pitch_space = Codec::dec(r)?; + let default_tuning_system = Codec::dec(r)?; + let reference = Codec::dec(r)?; + let smufl = Codec::dec(r)?; + let overrides = Codec::dec(r)?; + Ok(TuningContextSettings { + default_pitch_space, + default_tuning_system, + reference, + smufl, + overrides, + }) + } +} + // Schema major 2: the cross-cutting bodies filled (appended fields); the // frozen prior layouts are read by the `dec_*_v1` sub-decoders. struct_codec!(Slur { @@ -3545,6 +3579,12 @@ canonical_value! { // own — see the contract's "Why these two" table. CanvasLayoutDefaults, SpellingPrecedence, + // Genesis tranche G2b (`CONTRACT_GENESIS_G2B_TUNING.md` §1) — + // SetTuningContext embeds this authored subset, not the full + // `ScoreTuningContext`. Its `Codec` above is byte-identical to + // `ScoreTuningContext`'s five-field walk; this makes that layout + // reachable per-value, exactly as `ScoreTuningContext` above is. + TuningContextSettings, } #[cfg(test)] @@ -3873,6 +3913,65 @@ mod tests { assert!(back.accidental_extensions.is_empty()); } + /// 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 + /// narrowing, not a new wire form. Proven directly against non-default + /// values in all five fields, not just the default. + /// + /// **Mutation:** reorder two fields in `TuningContextSettings::enc` (e.g. + /// swap `smufl` and `overrides`) without making the matching change in + /// `ScoreTuningContext::enc`; must fail. + #[test] + fn tuning_context_settings_canonical_bytes_match_score_tuning_context() { + use crate::accidental::{SmuflVersion, SmuflVersionRequirement}; + use crate::graph::{ScoreTuningContext, TuningContextSettings}; + use crate::ids::{ReplicaId, VoiceId}; + use crate::pitch::TuningSystemId; + use crate::tuning::{TuningOverride, TuningScope}; + + let full = ScoreTuningContext { + smufl: SmuflVersionRequirement { + minimum: SmuflVersion::from_decimal(1, "12").unwrap(), + authored_against: SmuflVersion::from_decimal(1, "18").unwrap(), + }, + overrides: vec![TuningOverride { + scope: TuningScope::Voice(VoiceId::new(ReplicaId(1), 7)), + pitch_space: None, + tuning_system: Some(TuningSystemId::new("tet-19")), + reference: None, + }], + ..ScoreTuningContext::default() + }; + let settings = TuningContextSettings { + default_pitch_space: full.default_pitch_space.clone(), + default_tuning_system: full.default_tuning_system.clone(), + reference: full.reference.clone(), + smufl: full.smufl, + overrides: full.overrides.clone(), + }; + + let mut full_bytes = Vec::new(); + full.enc(&mut full_bytes); + let mut settings_bytes = Vec::new(); + settings.enc(&mut settings_bytes); + assert_eq!( + settings_bytes, full_bytes, + "TuningContextSettings must encode byte-identically to ScoreTuningContext's five-field walk" + ); + + let decoded = + TuningContextSettings::dec(&mut Reader::new(&settings_bytes)).expect("decodes"); + assert_eq!(decoded.default_pitch_space, settings.default_pitch_space); + assert_eq!( + decoded.default_tuning_system, + settings.default_tuning_system + ); + assert_eq!(decoded.reference, settings.reference); + assert_eq!(decoded.smufl, settings.smufl); + assert_eq!(decoded.overrides, settings.overrides); + } + #[test] fn generator_scores_round_trip() { for seed in 0..200u64 { diff --git a/crates/epiphany-core/src/graph.rs b/crates/epiphany-core/src/graph.rs index 9edd439..0982835 100644 --- a/crates/epiphany-core/src/graph.rs +++ b/crates/epiphany-core/src/graph.rs @@ -1705,6 +1705,49 @@ impl Default for ScoreTuningContext { } } +/// The **authored subset** of [`ScoreTuningContext`] that `SetTuningContext` +/// (genesis tranche G2b, `spec/CONTRACT_GENESIS_G2B_TUNING.md` §1) carries: +/// exactly the five fields `ScoreTuningContext`'s `Codec` actually walks onto +/// the wire (`codec.rs`'s hand-written `impl Codec for ScoreTuningContext`), +/// in that same order. `accidental_extensions` is **not** a field of this +/// type — it is deliberately absent, not cleared or normalized. +/// +/// **Why a new type rather than reusing `ScoreTuningContext` directly.** +/// `ScoreTuningContext`'s `Codec` drops `accidental_extensions` on encode and +/// default-fills it to `Vec::new()` on decode: the field is staged out of +/// schema major 3 and stays in-memory-only. If an operation carried the full +/// `ScoreTuningContext`, `OperationSet::accept` would store the authored +/// envelope as a **value** (with `accidental_extensions` intact), while a +/// document reloaded from bytes would decode the same envelope with that +/// field reconstructed as empty — two divergent graph states from one +/// document, observable only by whether you just authored it or reloaded it. +/// A field that never reaches the wire cannot be caught by +/// `canonical_value!`'s decode → `finish()` → re-encode byte comparison, +/// because that comparison never touches the originating value. +/// +/// This type makes the divergence **unrepresentable**: it has no +/// `accidental_extensions` field to diverge on. `SetTuningContext`'s +/// reduction writes exactly these five fields onto `score.tuning_context` and +/// leaves `accidental_extensions` untouched, preserving whatever the graph +/// already held. This is a **type-level narrowing, not a new wire form** — +/// `TuningContextSettings`'s canonical encoding is byte-for-byte identical to +/// `ScoreTuningContext`'s existing five-field walk (asserted in +/// `codec.rs`), so the packet designs no new layout. +/// +/// When a later schema major lands `accidental_extensions` on the wire, this +/// type gains the field like any other major payload change — the same cost +/// normalization would have paid, but without making the never-authored / +/// authored-to-default distinction depend on a clearing discipline enforced +/// by nothing the compiler can see. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct TuningContextSettings { + pub default_pitch_space: PitchSpaceId, + pub default_tuning_system: TuningSystemId, + pub reference: ReferencePitch, + pub smufl: crate::accidental::SmuflVersionRequirement, + pub overrides: Vec, +} + /// The root object of a score (Chapter 5 §"Top-Level Score Structure"). /// /// This carries the full Chapter 5 top-level shape. The invariant-bearing diff --git a/crates/epiphany-core/src/lib.rs b/crates/epiphany-core/src/lib.rs index da1d135..bacf46d 100644 --- a/crates/epiphany-core/src/lib.rs +++ b/crates/epiphany-core/src/lib.rs @@ -160,8 +160,8 @@ pub use graph::{ SoundConfiguration, SpaceUnit, SpanStyle, Spanner, SpannerKind, Staff, StaffBasedContent, StaffBracketKind, StaffExtent, StaffGroup, StaffGroupKind, StaffInstance, StaffLineConfiguration, StemDirection, SubBeam, TempoMapReference, TextLineDefinition, Tie, - TieClass, TimeExtent, TimeSignature, TimeSignatureDisplay, Timestamp, Tuplet, TupletRatio, - UnpitchedMember, ViewDefinition, Voice, VoiceOrigin, Volta, + TieClass, TimeExtent, TimeSignature, TimeSignatureDisplay, Timestamp, TuningContextSettings, + Tuplet, TupletRatio, UnpitchedMember, ViewDefinition, Voice, VoiceOrigin, Volta, }; pub use tempo::{ diff --git a/crates/epiphany-core/src/textvalue_graph.rs b/crates/epiphany-core/src/textvalue_graph.rs index 11d0df0..09cbdf5 100644 --- a/crates/epiphany-core/src/textvalue_graph.rs +++ b/crates/epiphany-core/src/textvalue_graph.rs @@ -28,7 +28,7 @@ use crate::graph::{ AnnotationAnchor, DecompositionSource, EventOrderingDAG, GestureAnchoring, KeySignature, MetadataValue, RegionContent, RegionTimeModel, RepeatKind, ScoreTuningContext, SoundConfiguration, SpaceUnit, SpannerKind, StaffGroupKind, TieClass, TimeSignature, - TimeSignatureDisplay, Timestamp, TupletRatio, VoiceOrigin, + TimeSignatureDisplay, Timestamp, TuningContextSettings, TupletRatio, VoiceOrigin, }; use crate::textvalue::{kebab, Sexp, TextError, TextValue}; use crate::textvalue_impls::class_of; @@ -447,6 +447,38 @@ impl TextValue for ScoreTuningContext { } } +/// The **authored subset** of `ScoreTuningContext` that `SetTuningContext` +/// carries (genesis tranche G2b, `spec/CONTRACT_GENESIS_G2B_TUNING.md` §1) — +/// the same five fields, in the same order, as `ScoreTuningContext`'s +/// projection above; `accidental_extensions` has no field here to project. +impl TextValue for TuningContextSettings { + fn project(&self) -> Sexp { + Sexp::List(vec![ + Sexp::Symbol(kebab("TuningContextSettings")), + self.default_pitch_space.project(), + self.default_tuning_system.project(), + self.reference.project(), + self.smufl.project(), + self.overrides.project(), + ]) + } + fn parse(s: &Sexp) -> Result { + let fields = s.expect_struct(&kebab("TuningContextSettings"), 5)?; + let default_pitch_space = TextValue::parse(&fields[0])?; + let default_tuning_system = TextValue::parse(&fields[1])?; + let reference = TextValue::parse(&fields[2])?; + let smufl = TextValue::parse(&fields[3])?; + let overrides = TextValue::parse(&fields[4])?; + Ok(TuningContextSettings { + default_pitch_space, + default_tuning_system, + reference, + smufl, + overrides, + }) + } +} + // =========================================================================== // Tagged unions. // =========================================================================== diff --git a/crates/epiphany-editor-core/src/barriers.rs b/crates/epiphany-editor-core/src/barriers.rs index e0e1525..2ee2c8a 100644 --- a/crates/epiphany-editor-core/src/barriers.rs +++ b/crates/epiphany-editor-core/src/barriers.rs @@ -20,8 +20,8 @@ //! container is what the barrier's **scope** is for, and scope is matched //! against the target's real containment, precisely. //! * **Score-level operations** (`SetMetadata`, the transaction descriptor, -//! `SetCanvasLayoutDefaults`, `SetSpellingPrecedence`) name no graph object: -//! only a score-wide barrier (empty +//! `SetCanvasLayoutDefaults`, `SetSpellingPrecedence`, `SetTuningContext`) +//! name no graph object: only a score-wide barrier (empty //! `affected_object_kinds`, `WholeScore`/`TuningContext`/`Registered` scope) //! can match them. //! * **Extension-defined operations** (`OperationKind::Registered`) carry a @@ -469,7 +469,11 @@ pub(crate) fn subjects_of(kind: &OperationKind, score: &Score) -> BarrierSubject OperationKind::SetMetadata(_) | OperationKind::DeclareTransaction(_) | OperationKind::SetCanvasLayoutDefaults(_) - | OperationKind::SetSpellingPrecedence(_) => BarrierSubjects::ScoreWide, + | OperationKind::SetSpellingPrecedence(_) + // Genesis tranche G2b (`CONTRACT_GENESIS_G2B_TUNING.md`, one-time + // authorization): another score-singleton field overwrite naming no + // resolvable region or object, exactly like `SetMetadata`. + | OperationKind::SetTuningContext(_) => BarrierSubjects::ScoreWide, OperationKind::CreateRepeatStructure(op) => one( TypedObjectId::RepeatStructure(op.repeat_structure_id()), repeat_context(score, &op.repeat), diff --git a/crates/epiphany-layout-ir/src/barrier.rs b/crates/epiphany-layout-ir/src/barrier.rs index ca8a341..f18e85b 100644 --- a/crates/epiphany-layout-ir/src/barrier.rs +++ b/crates/epiphany-layout-ir/src/barrier.rs @@ -1153,28 +1153,28 @@ mod tests { tag: 7 }) ); - // Operation-kind tag 34 is one past the vocabulary (the Phase-3 ops + // Operation-kind tag 35 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; - // encodings are append-only). + // `SetCanvasLayoutDefaults` 32 and `SetSpellingPrecedence` 33, + // genesis G2b's `SetTuningContext` 34; encodings are append-only). // - // This assertion named 30 until Push 5 / P4, 31 until genesis G1, and - // 32 until genesis G2a — 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. + // This assertion named 30 until Push 5 / P4, 31 until genesis G1, 32 + // until genesis G2a, and 34 until genesis G2b — each time, by then, + // the number had become a real kind, so the test was pinning a bug: a + // barrier that prohibited the new operation encoded fine and would + // not read back. It must be bumped by every tranche that appends a + // tag, and it is deliberately a literal rather than + // `PAYLOAD_FREE.len()` so the bump is a conscious act. let mut bytes = vec![0u8]; bytes.extend(set_blob(&[])); - bytes.extend(set_blob(&[vec![34u8]])); + bytes.extend(set_blob(&[vec![35u8]])); bytes.push(0); assert_eq!( EditBarrier::decode_canonical_bytes(&bytes), Err(BarrierDecodeError::InvalidTag { kind: "OperationKindTag", - tag: 34 + tag: 35 }) ); } diff --git a/crates/epiphany-ops/DECISIONS.md b/crates/epiphany-ops/DECISIONS.md index 2783360..3f0d48b 100644 --- a/crates/epiphany-ops/DECISIONS.md +++ b/crates/epiphany-ops/DECISIONS.md @@ -1888,3 +1888,72 @@ surface for a leak to appear on even before checking the hash. `gen_payload` arms appended, reshuffling the seeded RNG stream exactly as at Phase D, Push 4a, and G1; the digest moved and was re-pinned with that reasoning recorded in the test's own comment. + +## Genesis tranche G2b — `SetTuningContext`, kind/tag 34, the accept-set raise +## (2026-07-28) + +`spec/CONTRACT_GENESIS_G2B_TUNING.md` lands the third rung: the sole genesis +payload born at schema major 3, and the one surface among the nine +settings/creates that drags `OperationEnvelopeBlock`'s accept-set from 2 to 3. +Its carried type is **not** the full `ScoreTuningContext` but the new +`epiphany_core::TuningContextSettings` — the authored subset of exactly the +five wire-bearing fields; see that crate's `DECISIONS.md` for why (the +never-authored / authored-to-default divergence a full-value payload would +have introduced). + +`set_tuning_context` copies `set_metadata` structurally — advisory LWW, no +conflict, no idempotence short-circuit — but writes onto `score.tuning_context` +**field by field** rather than by whole-struct assignment, because the payload +carries a different (narrower) type than the field it targets: +`accidental_extensions` is left untouched in both the ordinary apply path and +undo's restoration-apply path. This is the one place in this rung's reduction +code that could not be a literal copy-paste of `set_metadata`/ +`set_canvas_layout_defaults`/`set_spelling_precedence` — those three write +`score. = op..clone()` in one line because the carried type +*is* the field's type; here it is a strict subset, so the assignment is five +lines, one per subset field, deliberately omitting `accidental_extensions`. + +**Pin 5, verified rather than assumed.** An earlier draft of the governing +contract required undo to distinguish "never authored" from +"authored-to-default" — `spec/PLAN_GENESIS_OPS.md` §5 trap 5, withdrawn +2026-07-28 before this packet started. `tuning_context_chain` is seeded from +`Score::empty`'s default in `seed_from_graph`, exactly as `metadata_chain` is, +so the first undo of a `SetTuningContext` write yields +`Restore(Some(Predecessor::Base(seeded)))` regardless of whether the seed (or +the write) happened to equal the type default. Test t7 asserts the two cases +(default seed, non-default seed) restore identically — proving the +distinction unobservable, not merely failing to test for it. + +`schema_major()` gains a real, unconditional arm returning 3 — the opposite of +G2a's "no arm" pin, because `ScoreTuningContext`'s `smufl`/`overrides` appends +are mandatory (not `Option`-hidden), so no lower-major layout for this payload +exists (a `CreateInstrument`-shaped arm, not a `CreateRegion`-shaped one). + +**The accept-set raise is entirely `epiphany-bundle`'s change** (see that +crate's `DECISIONS.md`); this crate's contribution is making `SetTuningContext` +the value that requires it. + +**Boundary crossings, budgeted as the contract's one-time authorization.** +`editor-core/src/barriers.rs::subjects_of` gains kind 34 in the same +score-wide `SetMetadata(_) | DeclareTransaction(_) | ...` arm G2a's two kinds +joined — a score-level field overwrite with no resolvable region or object. +`layout-ir/src/barrier.rs`'s "one past the vocabulary" literal moves 34→35. +`testkit/src/generators.rs`'s `rng.below` bound and +`testkit/tests/text_projection_grammar.rs`'s kind count both move by one. +`testkit/src/layout_stub.rs`'s derived `PAYLOAD_FREE`-based draw needed no +edit — confirmed, not assumed, per the contract's explicit ask (touch row 21) +— because it derives from the vocabulary macro rather than being hand-extended. + +**Decode vector pinned to literal bytes** (`vectors.rs`, +`set_tuning_context_envelope_decode_vector_is_pinned_to_literal_bytes`), not +merely round-tripped — the plan's trap 4 (a self-consistent discriminant +reorder once passed 1283 tests and 8/8 conformance). Mutation-verified: a +one-byte corruption of the pinned discriminant produces +`InvalidTag { kind: "OperationKind", tag: 99 }`, observed directly rather than +inferred. + +**No pruning or compaction is implemented, enabled, or prepared here** — pin +9's explicit non-goal. The op log is now the sole canonical carrier of the +tuning context (the canonical base embeds no graph value for any `Score` +field), which is exactly why a future prune must stay blocked on disposition +C; this packet adds no `fn prune` and no scaffolding toward one. diff --git a/crates/epiphany-ops/src/envdecode.rs b/crates/epiphany-ops/src/envdecode.rs index 81ffa53..c1796b5 100644 --- a/crates/epiphany-ops/src/envdecode.rs +++ b/crates/epiphany-ops/src/envdecode.rs @@ -39,8 +39,8 @@ use epiphany_core::{ CanvasLayoutDefaults, EventId, Instrument, InstrumentId, MetricGrid, MusicalPosition, OperationId, PitchId, PitchSpelling, RegionId, RegionTimeModel, RepeatStructureId, ReplicaId, ScoreMetadata, SpellingPrecedence, Staff, StaffInstance, StaffInstanceId, - StaffLineConfiguration, TimeAnchor, TimeSignature, TranspositionInterval, TupletId, - TypedObjectId, Voice, VoiceId, WallClockTime, + StaffLineConfiguration, TimeAnchor, TimeSignature, TranspositionInterval, + TuningContextSettings, TupletId, TypedObjectId, Voice, VoiceId, WallClockTime, }; use epiphany_determinism::{CanonicalDecode, CanonicalEncode}; @@ -596,6 +596,9 @@ fn operation_kind(r: &mut Reader<'_>) -> Result { 33 => OperationKind::SetSpellingPrecedence(SetSpellingPrecedenceOp { precedence: value::(r, "SpellingPrecedence")?, }), + 34 => OperationKind::SetTuningContext(SetTuningContextOp { + settings: value::(r, "TuningContextSettings")?, + }), tag => { return Err(EnvelopeDecodeError::InvalidTag { kind: "OperationKind", @@ -895,6 +898,11 @@ pub(crate) mod tests { precedence: valuegen::spelling_precedence(1), }) } + OperationKindTag::SetTuningContext => { + OperationKind::SetTuningContext(SetTuningContextOp { + settings: valuegen::tuning_context_settings(1), + }) + } } } diff --git a/crates/epiphany-ops/src/fuzz.rs b/crates/epiphany-ops/src/fuzz.rs index ce3b744..13d73a6 100644 --- a/crates/epiphany-ops/src/fuzz.rs +++ b/crates/epiphany-ops/src/fuzz.rs @@ -90,7 +90,7 @@ fn pitch(n: u64) -> PitchId { /// Generates a random payload over the shared id space. fn gen_payload(rng: &mut SplitMix64) -> OperationPayload { - let kind = match rng.below(31) { + let kind = match rng.below(32) { 0 => { let voice = VoiceId::new(ReplicaId(7), rng.below(3)); let position = MusicalPosition(RationalTime::from_int(rng.below(4) as i32)); @@ -300,6 +300,10 @@ fn gen_payload(rng: &mut SplitMix64) -> OperationPayload { 30 => OperationKind::SetSpellingPrecedence(crate::payload::SetSpellingPrecedenceOp { precedence: valuegen::spelling_precedence(rng.below(3) as u8), }), + // Genesis tranche G2b: the sole genesis payload born at schema major 3. + 31 => OperationKind::SetTuningContext(crate::payload::SetTuningContextOp { + settings: valuegen::tuning_context_settings(rng.below(3) as u8), + }), _ => 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 9b84069..d9debdf 100644 --- a/crates/epiphany-ops/src/lib.rs +++ b/crates/epiphany-ops/src/lib.rs @@ -132,8 +132,9 @@ pub use payload::{ ModifyIdentifiedPitchOp, OperationKind, OperationKindTag, OperationPayload, PositionRemapping, ResolveConflictPayload, ResolveEquivocationPayload, RespellPitchOp, SetCanvasLayoutDefaultsOp, SetMetadataOp, SetMetricGridOp, SetSpellingPrecedenceOp, SetStaffLayoutOp, SetTempoSegmentOp, - SetTimeSignatureOp, SetUserPageBreakOp, SetUserSystemBreakOp, TransactionCategory, - TransactionDescriptor, TransposeIntervalOp, TransposeOp, TupletCompensation, + 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 1ce513f..e8cca1f 100644 --- a/crates/epiphany-ops/src/migrate.rs +++ b/crates/epiphany-ops/src/migrate.rs @@ -188,6 +188,8 @@ fn project_kind(kind: &OperationKind) -> V0OperationKind { OperationKind::SetSpellingPrecedence(op) => { V0OperationKind::SetSpellingPrecedence(op.clone()) } + // Genesis tranche G2b: born past v0; projected verbatim. + OperationKind::SetTuningContext(op) => V0OperationKind::SetTuningContext(op.clone()), } } @@ -350,6 +352,8 @@ fn migrate_kind(kind: &V0OperationKind, context: &Score) -> Result { OperationKind::SetSpellingPrecedence(op.clone()) } + // Genesis tranche G2b: identity round-trip (no lossy v0 form). + V0OperationKind::SetTuningContext(op) => OperationKind::SetTuningContext(op.clone()), }) } diff --git a/crates/epiphany-ops/src/payload.rs b/crates/epiphany-ops/src/payload.rs index ef46b28..e5d7a76 100644 --- a/crates/epiphany-ops/src/payload.rs +++ b/crates/epiphany-ops/src/payload.rs @@ -37,7 +37,8 @@ use epiphany_core::{ OperationId, Pitch, PitchId, PitchSpelling, Region, RegionId, RegionTimeModel, RepeatStructure, RepeatStructureId, Rest, ScoreMetadata, Slur, Spanner, SpellingPrecedence, Staff, StaffId, StaffInstance, StaffInstanceId, StaffLineConfiguration, TempoSegment, Tie, TimeAnchor, - TimeSignature, TransactionId, TranspositionInterval, TupletId, TypedObjectId, Voice, VoiceId, + TimeSignature, TransactionId, TranspositionInterval, TuningContextSettings, TupletId, + TypedObjectId, Voice, VoiceId, }; use epiphany_determinism::{ sorted_canonical, CanonicalDecode, CanonicalEncode, CanonicalSet, DecodeError, @@ -263,6 +264,15 @@ pub enum OperationKind { /// Overwrite the score's spelling precedence (later-in-canonical-order /// wins). SetSpellingPrecedence(SetSpellingPrecedenceOp), + // --- Genesis tranche G2b (`spec/CONTRACT_GENESIS_G2B_TUNING.md`): the + // sole genesis payload born at schema major 3. Discriminant extends + // additively past 33. + // --- + /// Overwrite the score's tuning context settings (later-in-canonical- + /// order wins). Carries [`TuningContextSettings`], the authored subset of + /// `ScoreTuningContext` — `accidental_extensions` is not on the wire and + /// is left untouched by reduction (contract §1). + SetTuningContext(SetTuningContextOp), } impl OperationKind { @@ -291,6 +301,13 @@ impl OperationKind { | OperationKind::CreateStaff(_) | OperationKind::SetMetadata(_) | OperationKind::CreateInstrument(_) => 2, + // Genesis tranche G2b (contract pin 2): unconditionally 3. + // `ScoreTuningContext`'s `smufl`/`overrides` appends are + // mandatory, not `Option`-hidden, so there is no lower-major + // layout for this payload — a `CreateInstrument`-shaped arm, not + // a `CreateRegion`-shaped one. The sole surface among the nine + // genesis settings/creates that raises the op-block accept-set. + OperationKind::SetTuningContext(_) => 3, // Value-dependent: the embedded StaffLineConfiguration rides an // Option; None encodes byte-identically to the prior major. OperationKind::CreateRegion(op) => { @@ -368,6 +385,9 @@ impl OperationKind { // by the same accident as above, not by rule. OperationKind::SetCanvasLayoutDefaults(_) => 32, OperationKind::SetSpellingPrecedence(_) => 33, + // Genesis tranche G2b; appended past 33. Coincides with tag 34 by + // the same accident as G1/G2a, not by rule. + OperationKind::SetTuningContext(_) => 34, } } @@ -424,6 +444,9 @@ impl OperationKind { OperationKind::SetCanvasLayoutDefaults(_) | OperationKind::SetSpellingPrecedence(_) => { Some(9) } + // Minor 10 (Genesis tranche G2b), ratified + // `spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4. + OperationKind::SetTuningContext(_) => Some(10), } } @@ -471,6 +494,7 @@ impl OperationKind { OperationKind::CreateInstrument(_) => OperationKindTag::CreateInstrument, OperationKind::SetCanvasLayoutDefaults(_) => OperationKindTag::SetCanvasLayoutDefaults, OperationKind::SetSpellingPrecedence(_) => OperationKindTag::SetSpellingPrecedence, + OperationKind::SetTuningContext(_) => OperationKindTag::SetTuningContext, } } } @@ -516,6 +540,7 @@ impl CanonicalEncode for OperationKind { OperationKind::CreateInstrument(op) => op.encode_canonical(out), OperationKind::SetCanvasLayoutDefaults(op) => op.encode_canonical(out), OperationKind::SetSpellingPrecedence(op) => op.encode_canonical(out), + OperationKind::SetTuningContext(op) => op.encode_canonical(out), } } } @@ -570,6 +595,8 @@ pub enum OperationKindTag { SetCanvasLayoutDefaults, /// Genesis tranche G2a. SetSpellingPrecedence, + /// Genesis tranche G2b. + SetTuningContext, } /// The discriminant of [`OperationKindTag::Registered`], the one tag that @@ -684,6 +711,7 @@ operation_kind_tag_vocabulary! { CreateInstrument = 31 => "create-instrument" @ Some(8), SetCanvasLayoutDefaults = 32 => "set-canvas-layout-defaults" @ Some(9), SetSpellingPrecedence = 33 => "set-spelling-precedence" @ Some(9), + SetTuningContext = 34 => "set-tuning-context" @ Some(10), } impl CanonicalEncode for OperationKindTag { @@ -1650,6 +1678,30 @@ impl CanonicalEncode for SetSpellingPrecedenceOp { } } +// --- Genesis tranche G2b (`spec/CONTRACT_GENESIS_G2B_TUNING.md`): the sole +// genesis payload born at schema major 3. LWW field-overwrite, the same +// `SetMetadata` discipline. --- + +/// Overwrite the score's tuning context settings (operation_catalog +/// §SetTuningContext). Carries [`TuningContextSettings`] — the **authored +/// subset** of `ScoreTuningContext` (contract §1): exactly the five +/// wire-bearing fields, in the codec's existing order. +/// `accidental_extensions` is not a field of the carried type and is never +/// touched by reduction, which writes only these five fields onto +/// `score.tuning_context`. The score-singleton field-overwrite is *advisory* +/// last-writer-wins — the latest write in canonical order silently wins and +/// no conflict is recorded, exactly `SetMetadata`. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct SetTuningContextOp { + pub settings: TuningContextSettings, +} + +impl CanonicalEncode for SetTuningContextOp { + fn encode_canonical(&self, out: &mut Vec) { + push_lp_bytes(out, &self.settings.canonical_bytes()); + } +} + /// Set, replace, or (`None`) remove the single meter change at the anchor's /// resolved musical position in a region's default metric grid /// (operation_catalog §"Meter and Tempo Overwrites"). Carries the full diff --git a/crates/epiphany-ops/src/reduce.rs b/crates/epiphany-ops/src/reduce.rs index 6edf4bd..9213111 100644 --- a/crates/epiphany-ops/src/reduce.rs +++ b/crates/epiphany-ops/src/reduce.rs @@ -40,7 +40,7 @@ use epiphany_core::{ SpellingDirective, SpellingPrecedence, SpellingScope, SpellingSource, Staff, StaffId, StaffInstance, StaffInstanceId, StaffLineConfiguration, TempoMap, TempoSegment, TempoShape, TimeAnchor, TimeSignature, TimeSignatureId, TransactionId, TransposeRefusal, - TranspositionInterval, TypedObjectId, Voice, VoiceId, VoiceOrigin, + TranspositionInterval, TuningContextSettings, TypedObjectId, Voice, VoiceId, VoiceOrigin, }; use epiphany_determinism::CanonicalEncode; @@ -63,7 +63,8 @@ use crate::payload::{ InsertIdentifiedPitchOp, ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, OperationKind, OperationPayload, RespellPitchOp, SetCanvasLayoutDefaultsOp, SetMetadataOp, SetMetricGridOp, SetSpellingPrecedenceOp, SetStaffLayoutOp, SetTempoSegmentOp, - SetTimeSignatureOp, SetUserPageBreakOp, TransposeIntervalOp, TransposeOp, TupletCompensation, + SetTimeSignatureOp, SetTuningContextOp, SetUserPageBreakOp, TransposeIntervalOp, TransposeOp, + TupletCompensation, }; use crate::stamp::StampTuple; use crate::support::{ObjectKind, SerializedCanonicalInputs}; @@ -851,6 +852,11 @@ enum ValueRestoration { SpellingPrecedence { value: Option, }, + /// Genesis tranche G2b. The subset type — `accidental_extensions` is not + /// part of it and is never touched by restoration. + TuningContext { + value: Option, + }, MetricGrid { region: RegionId, value: Option, @@ -972,6 +978,11 @@ struct Reducer<'a> { // the pre-operational value. canvas_layout_defaults_chain: WriteChain, spelling_precedence_chain: WriteChain, + // Genesis tranche G2b (`CONTRACT_GENESIS_G2B_TUNING.md` pin 5): same + // discipline, carrying the **subset** type — `accidental_extensions` + // never participates in undo, because no operation ever writes it and + // undo must leave it exactly as it found it. + tuning_context_chain: WriteChain, break_chain: BTreeMap<(RegionId, MusicalPosition), WriteChain<(TimeAnchor, bool)>>, page_break_chain: BTreeMap<(RegionId, MusicalPosition), WriteChain<(TimeAnchor, bool)>>, // Meter/tempo overwrite chains (Phase-3 tranche): `Some` = a set/replace at @@ -1085,6 +1096,7 @@ struct WorkingSnapshot { metadata_chain: WriteChain, canvas_layout_defaults_chain: WriteChain, spelling_precedence_chain: WriteChain, + tuning_context_chain: WriteChain, break_chain: BTreeMap<(RegionId, MusicalPosition), WriteChain<(TimeAnchor, bool)>>, page_break_chain: BTreeMap<(RegionId, MusicalPosition), WriteChain<(TimeAnchor, bool)>>, meter_change_chain: BTreeMap<(RegionId, MusicalPosition), WriteChain>>, @@ -1367,6 +1379,7 @@ impl<'a> Reducer<'a> { metadata_chain: WriteChain::new(), canvas_layout_defaults_chain: WriteChain::new(), spelling_precedence_chain: WriteChain::new(), + tuning_context_chain: WriteChain::new(), break_chain: BTreeMap::new(), page_break_chain: BTreeMap::new(), meter_change_chain: BTreeMap::new(), @@ -1469,6 +1482,17 @@ impl<'a> Reducer<'a> { .seed(score.canvas.layout_defaults); self.spelling_precedence_chain .seed(score.spelling_precedence.clone()); + // Genesis tranche G2b (contract pin 5): same discipline, seeded with + // only the five wire-bearing fields — the subset the chain's value + // type carries. `accidental_extensions` is not part of the seed and + // is never touched by undo. + self.tuning_context_chain.seed(TuningContextSettings { + default_pitch_space: score.tuning_context.default_pitch_space.clone(), + default_tuning_system: score.tuning_context.default_tuning_system.clone(), + reference: score.tuning_context.reference.clone(), + smufl: score.tuning_context.smufl, + overrides: score.tuning_context.overrides.clone(), + }); for segment in &score.tempo_map.segments { self.tempo_segment_chain .entry((None, resolved_anchor_position(&segment.start))) @@ -2833,6 +2857,7 @@ impl<'a> Reducer<'a> { self.set_canvas_layout_defaults(env, op) } OperationKind::SetSpellingPrecedence(op) => self.set_spelling_precedence(env, op), + OperationKind::SetTuningContext(op) => self.set_tuning_context(env, op), }, OperationPayload::ResolveConflict(op) => self.resolve_conflict(env, op), OperationPayload::UndoTransaction(op) => self.undo_transaction(env, op), @@ -2947,6 +2972,30 @@ impl<'a> Reducer<'a> { OperationEffect::Applied } + /// Genesis tranche G2b (`CONTRACT_GENESIS_G2B_TUNING.md` pin 5): copies + /// `set_metadata` structurally — advisory LWW, no conflict, no + /// idempotence short-circuit. Writes exactly the five subset fields onto + /// `score.tuning_context` and **leaves `accidental_extensions` + /// untouched** — the carried payload has no such field, and preserving + /// whatever the graph already held is the ruling (contract §1). + fn set_tuning_context( + &mut self, + env: &OperationEnvelope, + op: &SetTuningContextOp, + ) -> OperationEffect { + self.tuning_context_chain + .record(env.id, env.transaction, op.settings.clone()); + if let Some(score) = self.graph.as_mut() { + score.tuning_context.default_pitch_space = op.settings.default_pitch_space.clone(); + score.tuning_context.default_tuning_system = op.settings.default_tuning_system.clone(); + score.tuning_context.reference = op.settings.reference.clone(); + score.tuning_context.smufl = op.settings.smufl; + score.tuning_context.overrides = op.settings.overrides.clone(); + // accidental_extensions is intentionally not touched here. + } + OperationEffect::Applied + } + fn set_metric_grid( &mut self, env: &OperationEnvelope, @@ -5334,6 +5383,16 @@ impl<'a> Reducer<'a> { }) } } + // Genesis tranche G2b (contract pin 5): mirror the same shape. + match self.tuning_context_chain.undo_verdict(tx) { + ChainUndoVerdict::NotWritten => {} + ChainUndoVerdict::Superseded { by } => superseded.push(by), + ChainUndoVerdict::Restore(predecessor) => { + restorations.push(ValueRestoration::TuningContext { + value: predecessor.map(Predecessor::into_value), + }) + } + } for (region, chain) in &self.metric_grid_chain { if !slot_live(TypedObjectId::Region(*region)) { continue; @@ -5574,6 +5633,24 @@ impl<'a> Reducer<'a> { .record(env.id, env.transaction, value); } } + // Genesis tranche G2b (contract pin 5): mirror `Metadata`'s + // restoration-apply shape; writes only the five subset + // fields, leaving `accidental_extensions` untouched. + ValueRestoration::TuningContext { value } => { + if let Some(value) = value { + if let Some(score) = self.graph.as_mut() { + score.tuning_context.default_pitch_space = + value.default_pitch_space.clone(); + score.tuning_context.default_tuning_system = + value.default_tuning_system.clone(); + score.tuning_context.reference = value.reference.clone(); + score.tuning_context.smufl = value.smufl; + score.tuning_context.overrides = value.overrides.clone(); + } + self.tuning_context_chain + .record(env.id, env.transaction, value); + } + } ValueRestoration::MetricGrid { region, value } => { self.metric_grid_chain .entry(region) @@ -7551,6 +7628,7 @@ impl<'a> Reducer<'a> { metadata_chain: self.metadata_chain.clone(), canvas_layout_defaults_chain: self.canvas_layout_defaults_chain.clone(), spelling_precedence_chain: self.spelling_precedence_chain.clone(), + tuning_context_chain: self.tuning_context_chain.clone(), break_chain: self.break_chain.clone(), page_break_chain: self.page_break_chain.clone(), meter_change_chain: self.meter_change_chain.clone(), @@ -7590,6 +7668,7 @@ impl<'a> Reducer<'a> { self.metadata_chain = s.metadata_chain; self.canvas_layout_defaults_chain = s.canvas_layout_defaults_chain; self.spelling_precedence_chain = s.spelling_precedence_chain; + self.tuning_context_chain = s.tuning_context_chain; self.break_chain = s.break_chain; self.page_break_chain = s.page_break_chain; self.meter_change_chain = s.meter_change_chain; @@ -10914,6 +10993,92 @@ mod tests { 0, "SetSpellingPrecedence stays in the major-0 catch-all" ); + + // (t2) Genesis tranche G2b: the opposite of G2a's pin — SetTuningContext + // is *unconditionally* 3, the same "no lower-major layout exists" shape + // as CreateInstrument above (not a value-dependent Option-hidden + // embedding). Asserted on the minimal (default) settings specifically, + // so an arm that becomes value-dependent is caught immediately. + assert_eq!( + OperationKind::SetTuningContext(crate::payload::SetTuningContextOp { + settings: crate::valuegen::tuning_context_settings(0), + }) + .schema_major(), + 3, + "SetTuningContext is unconditionally v3, even for the minimal settings" + ); + } + + /// (t1) Genesis tranche G2b: kind and tag are both 34, and the + /// discriminant byte leads the canonical encoding of the op payload. + /// + /// **Mutation:** move either `OperationKind::SetTuningContext`'s + /// discriminant or `OperationKindTag::SetTuningContext`'s tag to 35; + /// must fail. + #[test] + fn t1_set_tuning_context_kind_and_tag_are_both_34() { + use crate::payload::{OperationKindTag, SetTuningContextOp}; + let op = OperationKind::SetTuningContext(SetTuningContextOp { + settings: crate::valuegen::tuning_context_settings(1), + }); + assert_eq!(op.tag(), OperationKindTag::SetTuningContext); + assert_eq!(op.tag().discriminant(), 34); + let mut bytes = Vec::new(); + op.encode_canonical(&mut bytes); + assert_eq!( + bytes[0], 34, + "the discriminant byte must lead the canonical encoding" + ); + } + + /// (t3) Genesis tranche G2b: a block containing exactly one + /// `SetTuningContext` stamps schema major 3. + /// + /// **Mutation:** make `schema_major` ignore the `SetTuningContext` arm + /// (fold it into the catch-all `_ => 0`); must fail. + #[test] + fn t3_a_block_with_one_set_tuning_context_stamps_major_3() { + let op = OperationKind::SetTuningContext(crate::payload::SetTuningContextOp { + settings: crate::valuegen::tuning_context_settings(1), + }); + let envelopes = [op.clone()]; + let major = envelopes.iter().map(|k| k.schema_major()).max().unwrap(); + assert_eq!( + major, 3, + "a block carrying only SetTuningContext stamps major 3" + ); + } + + /// (t8) Genesis tranche G2b: kind 34 carries epoch 10 + /// (`spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4), and a block containing it + /// stamps schema minor 10. + /// + /// **Mutation:** assign epoch 9 instead of 10 in `introduced_minor`'s + /// `SetTuningContext` arm; must fail. + #[test] + fn t8_set_tuning_context_carries_epoch_10() { + let op = OperationKind::SetTuningContext(crate::payload::SetTuningContextOp { + settings: crate::valuegen::tuning_context_settings(1), + }); + assert_eq!(op.introduced_minor(), Some(10)); + assert_eq!(op.tag().introduced_minor(), Some(10)); + + let env = OperationEnvelope { + id: OperationId::new(ReplicaId(1), 1), + author: crate::support::AuthorId(0), + stamp: crate::stamp::OperationStamp::new( + crate::stamp::HybridLogicalClock::new(epiphany_core::WallClockTime(1), 1), + OperationId::new(ReplicaId(1), 1), + ), + causal_context: crate::causal::CausalContext::new(), + transaction: None, + payload: OperationPayload::Primitive(op), + }; + assert_eq!( + crate::payload::operation_block_introduced_minor(&[env]), + Some(10), + "a block containing kind 34 stamps minor 10" + ); } #[test] @@ -11007,6 +11172,16 @@ mod tests { // leak to appear on even if the base did stamp one (it does not — // that is `OperationEnvelopeBlock`'s concern in `epiphany-bundle`, // untouched by this packet). + // + // Re-pinned again at genesis tranche G2b + // (`spec/CONTRACT_GENESIS_G2B_TUNING.md`): `gen_payload` gained + // `SetTuningContext` (arm 31), and `rng.below(31)` became + // `below(32)` — the same reshuffle, same reasoning. `SetTuningContext` + // is schema major **3**, unlike its siblings, but that is + // `OperationEnvelopeBlock`'s concern (`epiphany-bundle`), not + // `MaterializedState`'s: the canonical base still embeds no `Score` + // field value for any setting, so there remains no schema-major + // surface on this type for a leak to appear on. let mut rng = epiphany_determinism::fuzz::SplitMix64::new(0xBA5E); let envelopes = crate::fuzz::gen_envelope_set(&mut rng, 200); let mut set = OperationSet::new(); @@ -11016,7 +11191,7 @@ mod tests { let hex: String = digest.iter().map(|b| format!("{b:02x}")).collect(); assert_eq!( hex, - "7fd6455a6ae304101774ba981d01702ad37c567575707b1e814dad6e153ab07d" + "116e88b4013a18864dcd1b09489ab297aa332e14800cf4ca00be9dfa100b4a08" ); } @@ -12952,6 +13127,55 @@ mod tests { ); } + /// (t9) Regression guard: the from-empty spine still reaches a note, now + /// with a `SetTuningContext` authored alongside it — genesis tranche G2b + /// does not disturb the spine G1 established. Mutation is t2's (the + /// `schema_major` arm), which this test does not re-verify on its own; + /// its job is to prove the spine and the new op coexist cleanly. + #[test] + fn from_empty_spine_reaches_a_note_with_a_tuning_context_authored() { + let spine = genesis_spine_envelopes(); + let settings = crate::valuegen::tuning_context_settings(1); + let set_tuning = prim_env( + 1, + 12, + 70, + CausalContext::new(), + OperationKind::SetTuningContext(crate::payload::SetTuningContextOp { + settings: settings.clone(), + }), + ); + let mut envelopes = spine.envelopes; + envelopes.push(set_tuning); + let mut set = OperationSet::new(); + set.accept_all(envelopes); + let identity = IdentityContext::new(ReplicaId(1)); + let out = reduce_operation_set_onto(&set, &Score::empty(identity)); + + for counter in [0, 2, 4, 6, 8, 10, 12] { + assert_eq!( + effect_at(&out.state, counter), + Some(&OperationEffect::Applied), + "spine operation at counter {counter} must apply" + ); + } + assert!( + out.score.events.contains(spine.event_id), + "the spine still reaches a note" + ); + assert_eq!( + out.score.tuning_context.default_pitch_space, + settings.default_pitch_space + ); + assert_eq!( + out.score.tuning_context.default_tuning_system, + settings.default_tuning_system + ); + assert_eq!(out.score.tuning_context.reference, settings.reference); + assert_eq!(out.score.tuning_context.smufl, settings.smufl); + assert_eq!(out.score.tuning_context.overrides, settings.overrides); + } + /// (i2) Re-carry idempotence: the same `CreateInstrument` twice -> /// `AlreadyApplied`, and a differing second carry under the same live id /// -> `RecreateContentMismatch` — exactly `create_staff`'s discipline @@ -13511,6 +13735,245 @@ mod tests { } } + // ========================================================================= + // Genesis tranche G2b (`spec/CONTRACT_GENESIS_G2B_TUNING.md`): the sole + // genesis payload born at schema major 3, `SetTuningContext`. + // ========================================================================= + + /// (t5) **The pin's whole reason for existing** (contract §1): an op + /// authored **live** and the same op reloaded **from bytes** must + /// reduce to identical graph states, with a pre-existing non-empty + /// `accidental_extensions` preserved across both. This is what the + /// subset type (pin 1) makes structurally impossible to get wrong — a + /// full-value `SetTuningContext(ScoreTuningContext)` would diverge here, + /// because `OperationSet::accept` stores the authored envelope as a + /// **value** (live path keeps whatever `accidental_extensions` the + /// constructor set) while a decoded envelope always reconstructs it as + /// empty (the wire drops it). + /// + /// **Mutation:** make the payload carry the full `ScoreTuningContext` + /// instead of the subset `TuningContextSettings`; must fail. Performed + /// and reversed live for this rung (see the report) rather than left in + /// the tree, since it requires the type-level change pin 1 forbids. + #[test] + fn t5_live_authored_and_reloaded_from_bytes_agree_with_extensions_preserved() { + use epiphany_core::{AccidentalRegistryId, ScoreAccidentalExtensions}; + + // A base graph with a pre-existing, non-empty accidental_extensions — + // the field the op never carries and reduction must leave untouched. + let mut base = Score::empty(IdentityContext::new(ReplicaId(1))); + let pre_existing = vec![ScoreAccidentalExtensions { + base: AccidentalRegistryId::new("heji"), + additions: Vec::new(), + overrides: Vec::new(), + }]; + base.tuning_context.accidental_extensions = pre_existing.clone(); + + let settings = crate::valuegen::tuning_context_settings(3); + let live_env = prim_env( + 1, + 0, + 10, + CausalContext::new(), + OperationKind::SetTuningContext(crate::payload::SetTuningContextOp { + settings: settings.clone(), + }), + ); + + // Path A: authored live — the envelope object itself, never touching + // bytes. + let mut live_set = OperationSet::new(); + live_set.accept_all(vec![live_env.clone()]); + let live_out = reduce_operation_set_onto(&live_set, &base); + + // Path B: the identical envelope, round-tripped through canonical + // bytes and decoded back before reduction. + let bytes = live_env.to_canonical_bytes(); + let reloaded_env = crate::envdecode::decode_envelope(&bytes).expect("the envelope decodes"); + let mut reloaded_set = OperationSet::new(); + reloaded_set.accept_all(vec![reloaded_env]); + let reloaded_out = reduce_operation_set_onto(&reloaded_set, &base); + + // The two graph states are identical. + assert_eq!( + live_out.score.tuning_context, reloaded_out.score.tuning_context, + "an op authored live and the same op reloaded from bytes must reduce identically" + ); + // Both apply the authored five-field settings... + assert_eq!( + live_out.score.tuning_context.default_pitch_space, + settings.default_pitch_space + ); + assert_eq!(live_out.score.tuning_context.reference, settings.reference); + // ...and both preserve the base's pre-existing accidental_extensions, + // untouched — the field the payload cannot carry. + assert_eq!( + live_out.score.tuning_context.accidental_extensions, pre_existing, + "live path: accidental_extensions preserved" + ); + assert_eq!( + reloaded_out.score.tuning_context.accidental_extensions, pre_existing, + "reloaded path: accidental_extensions preserved" + ); + } + + /// (t6) Undo restores the previous (chain-predecessor) five-field value — + /// not the seeded base, and not the type default. Two `SetTuningContext` + /// writes in two separate transactions; undoing the *second* must restore + /// the *first*'s authored value. + /// + /// **Mutation:** in the `TuningContext` restoration-apply arm, restore + /// the default `TuningContextSettings` instead of the chain predecessor + /// (or drop the field writes entirely); must fail. + #[test] + fn t6_undo_restores_the_chain_predecessor_tuning_settings() { + let identity = IdentityContext::new(ReplicaId(1)); + let first = crate::valuegen::tuning_context_settings(1); + let second = crate::valuegen::tuning_context_settings(2); + assert_ne!(first, second); + + let tx2 = TransactionId::from_raw(2); + let envelopes = vec![ + prim_env( + 1, + 0, + 10, + CausalContext::new(), + OperationKind::SetTuningContext(crate::payload::SetTuningContextOp { + settings: first.clone(), + }), + ), + declare_transaction(1, 1, 20, seen_r1(0), tx2), + tx_member( + 1, + 2, + 21, + seen_r1(1), + tx2, + OperationKind::SetTuningContext(crate::payload::SetTuningContextOp { + settings: second, + }), + ), + undo_env(1, 3, 22, seen_r1(2), tx2, UndoPolicy::StrictInverse), + ]; + let mut set = OperationSet::new(); + set.accept_all(envelopes); + let out = reduce_operation_set_onto(&set, &Score::empty(identity)); + + assert_eq!( + out.score.tuning_context.default_pitch_space, + first.default_pitch_space + ); + assert_eq!( + out.score.tuning_context.default_tuning_system, + first.default_tuning_system + ); + assert_eq!(out.score.tuning_context.reference, first.reference); + assert_eq!(out.score.tuning_context.smufl, first.smufl); + assert_eq!( + out.score.tuning_context.overrides, first.overrides, + "undo of the second write restores the first write's value" + ); + } + + /// (t7) Undo of the **first** authoring restores the **seeded base** + /// settings, identically whether the seed was the type default or a + /// non-default value — the never-authored / authored-to-default + /// distinction stays unobservable (contract pin 5, and plan trap 5's + /// withdrawal). `accidental_extensions` is untouched throughout: it is + /// not part of the carried subset type, so undo cannot move it, and this + /// test proves the base's own extensions survive both the write and the + /// undo unchanged. + /// + /// **Mutation:** skip `tuning_context_chain.seed(...)` in + /// `seed_from_graph` -> the first undo sees `NotWritten` (no predecessor + /// recorded) instead of `Restore(Base(seeded))`, and the field is left at + /// the authored value instead of reverting; must fail. + #[test] + fn t7_undo_of_first_authoring_restores_the_seeded_base_default_or_not() { + // Both cases run through the exact same assertion shape below — no + // branch distinguishes "the seed happened to be the type default" + // from "the seed was a genuine non-default value". That uniformity is + // itself the proof the distinction is unobservable, per pin 5's + // explicit instruction not to write code that tells them apart. + // A pre-existing, non-empty `accidental_extensions` fixture, built + // directly (its constructor is `pub(crate)` in `epiphany-core`, so + // this crate cannot reuse the core fixture helper). + let default_extensions = || { + vec![epiphany_core::ScoreAccidentalExtensions { + base: epiphany_core::AccidentalRegistryId::new("heji"), + additions: Vec::new(), + overrides: Vec::new(), + }] + }; + + for (case, base) in [ + ("case A: from-empty, default seed", { + let mut b = Score::empty(IdentityContext::new(ReplicaId(1))); + b.tuning_context.accidental_extensions = default_extensions(); + b + }), + ("case B: loaded base, non-default seed", { + let mut b = Score::empty(IdentityContext::new(ReplicaId(1))); + let non_default = crate::valuegen::tuning_context_settings(9); + b.tuning_context.default_pitch_space = non_default.default_pitch_space; + b.tuning_context.default_tuning_system = non_default.default_tuning_system; + b.tuning_context.reference = non_default.reference; + b.tuning_context.smufl = non_default.smufl; + b.tuning_context.overrides = non_default.overrides; + b.tuning_context.accidental_extensions = default_extensions(); + b + }), + ] { + let seeded = base.tuning_context.clone(); + let tx = TransactionId::from_raw(3); + let authored = crate::valuegen::tuning_context_settings(20); + let envelopes = vec![ + declare_transaction(1, 0, 10, CausalContext::new(), tx), + tx_member( + 1, + 1, + 11, + seen_r1(0), + tx, + OperationKind::SetTuningContext(crate::payload::SetTuningContextOp { + settings: authored, + }), + ), + undo_env(1, 2, 12, seen_r1(1), tx, UndoPolicy::StrictInverse), + ]; + let mut set = OperationSet::new(); + set.accept_all(envelopes); + let out = reduce_operation_set_onto(&set, &base); + + assert_eq!( + out.score.tuning_context.default_pitch_space, seeded.default_pitch_space, + "{case}: undo restores the seeded default_pitch_space" + ); + assert_eq!( + out.score.tuning_context.default_tuning_system, seeded.default_tuning_system, + "{case}: undo restores the seeded default_tuning_system" + ); + assert_eq!( + out.score.tuning_context.reference, seeded.reference, + "{case}: undo restores the seeded reference" + ); + assert_eq!( + out.score.tuning_context.smufl, seeded.smufl, + "{case}: undo restores the seeded smufl" + ); + assert_eq!( + out.score.tuning_context.overrides, seeded.overrides, + "{case}: undo restores the seeded overrides" + ); + assert_eq!( + out.score.tuning_context.accidental_extensions, seeded.accidental_extensions, + "{case}: accidental_extensions is untouched throughout — it is not part \ + of the carried subset type, so undo cannot move it" + ); + } + } + /// (s7) Transaction rollback discards the write — and the assertion must /// not be on the field. `WorkingSnapshot` (`:7480`) is the transaction /// **rollback** mechanism: snapshot before, restore on failure. But diff --git a/crates/epiphany-ops/src/textproj_kind.rs b/crates/epiphany-ops/src/textproj_kind.rs index f1646ca..911b51a 100644 --- a/crates/epiphany-ops/src/textproj_kind.rs +++ b/crates/epiphany-ops/src/textproj_kind.rs @@ -15,8 +15,8 @@ use crate::payload::{ InsertIdentifiedPitchOp, ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, OperationKind, OperationKindTag, RespellPitchOp, SetCanvasLayoutDefaultsOp, SetMetadataOp, SetMetricGridOp, SetSpellingPrecedenceOp, SetStaffLayoutOp, SetTempoSegmentOp, - SetTimeSignatureOp, SetUserPageBreakOp, SetUserSystemBreakOp, TransactionDescriptor, - TransposeIntervalOp, TransposeOp, + SetTimeSignatureOp, SetTuningContextOp, SetUserPageBreakOp, SetUserSystemBreakOp, + TransactionDescriptor, TransposeIntervalOp, TransposeOp, }; use crate::support::OperationKindRegistryId; @@ -229,6 +229,9 @@ impl TextValue for OperationKind { OperationKind::SetSpellingPrecedence(op) => { production(self.tag(), vec![op.precedence.project()]) } + OperationKind::SetTuningContext(op) => { + production(self.tag(), vec![op.settings.project()]) + } } } @@ -566,6 +569,14 @@ impl TextValue for OperationKind { precedence: TextValue::parse(precedence)?, }) } + OperationKindTag::SetTuningContext => { + let [settings] = fields(s, tag, 1)? else { + unreachable!("the arity-1 check returned one field") + }; + OperationKind::SetTuningContext(SetTuningContextOp { + settings: TextValue::parse(settings)?, + }) + } }) } } @@ -619,7 +630,7 @@ mod tests { #[test] fn every_operation_kind_round_trips_with_canonical_text() { let tags: Vec<_> = all_tags().collect(); - assert_eq!(tags.len(), 34, "the grammar has 34 kind productions"); + assert_eq!(tags.len(), 35, "the grammar has 35 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 d5ff532..c5163b4 100644 --- a/crates/epiphany-ops/src/v0.rs +++ b/crates/epiphany-ops/src/v0.rs @@ -112,6 +112,10 @@ pub enum V0OperationKind { // every other v1-native kind above. SetCanvasLayoutDefaults(crate::payload::SetCanvasLayoutDefaultsOp), SetSpellingPrecedence(crate::payload::SetSpellingPrecedenceOp), + // Genesis tranche G2b — born at wire-disc 34; no lossy v0 form (v0 + // predates the catalog entirely), so it round-trips by identity like + // every other v1-native kind above. + SetTuningContext(crate::payload::SetTuningContextOp), } /// 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 a8b63cf..0b70f07 100644 --- a/crates/epiphany-ops/src/valuegen.rs +++ b/crates/epiphany-ops/src/valuegen.rs @@ -409,6 +409,30 @@ pub fn spelling_precedence(nth: u8) -> epiphany_core::SpellingPrecedence { .expect("both listed orders are total over the five source kinds") } +/// Tuning context settings with an `nth`-distinct reference frequency +/// (genesis tranche G2b) — the carried [`epiphany_core::TuningContextSettings`] +/// of `SetTuningContext`. Distinct `nth` give distinct values so a harness can +/// drive concurrent `SetTuningContext`s, an advisory LWW field that resolves +/// by canonical order with no conflict. +pub fn tuning_context_settings(nth: u8) -> epiphany_core::TuningContextSettings { + use epiphany_core::{CmnNominal, PitchSpacePosition, ReferencePitch}; + epiphany_core::TuningContextSettings { + default_pitch_space: epiphany_core::PitchSpaceId::new("cmn-12"), + default_tuning_system: epiphany_core::TuningSystemId::new("tet-12"), + reference: ReferencePitch::new( + PitchSpacePosition::Cmn { + nominal: CmnNominal::A, + alteration: 0, + octave: 4, + }, + 440.0 + f64::from(nth), + ) + .expect("a small positive offset from A440 stays finite and positive"), + smufl: epiphany_core::SmuflVersionRequirement::default(), + overrides: Vec::new(), + } +} + /// A well-formed `numerator`/4 [`TimeSignature`](epiphany_core::TimeSignature) /// (Phase-3 tranche): `numerator` quarter-note beat groups summing exactly to /// the measure duration, so [`epiphany_core::TimeSignature::new`]'s beat-group diff --git a/crates/epiphany-ops/src/vectors.rs b/crates/epiphany-ops/src/vectors.rs index f9c206c..826d1d7 100644 --- a/crates/epiphany-ops/src/vectors.rs +++ b/crates/epiphany-ops/src/vectors.rs @@ -373,6 +373,44 @@ pub fn decode_vectors() -> Vec { precedence_trailing, )); + // --- OperationEnvelope carrying SetTuningContext (genesis tranche G2b) — + // the sole genesis payload born at schema major 3. Same rationale as the + // siblings above: nothing else in this corpus exercises this payload's + // decode path, and a round-trip check alone cannot see a self-consistent + // encoder/decoder reorder (the 3b-i lesson). + let tuning_envelope = OperationEnvelope { + id: OperationId::new(ReplicaId(1), 4), + author: crate::support::AuthorId(0), + stamp: crate::stamp::OperationStamp::new( + crate::stamp::HybridLogicalClock::new(epiphany_core::WallClockTime(1), 1), + OperationId::new(ReplicaId(1), 4), + ), + causal_context: crate::causal::CausalContext::new(), + transaction: None, + payload: crate::payload::OperationPayload::Primitive( + crate::payload::OperationKind::SetTuningContext(crate::payload::SetTuningContextOp { + settings: crate::valuegen::tuning_context_settings(1), + }), + ), + }; + let tuning_envelope_bytes = tuning_envelope.to_canonical_bytes(); + v.push(row( + OE, + "accept", + "-", + "set_tuning_context", + tuning_envelope_bytes.clone(), + )); + let mut tuning_trailing = tuning_envelope_bytes; + tuning_trailing.push(0); + v.push(row( + OE, + "reject", + "trailing-bytes", + "set_tuning_context_trailing", + tuning_trailing, + )); + v } @@ -555,4 +593,33 @@ mod tests { "the committed literal bytes must decode and re-encode injectively" ); } + + /// (s8 analogue) Genesis tranche G2b (`spec/CONTRACT_GENESIS_G2B_TUNING.md` + /// touch row 11): the `SetTuningContext` envelope decode vector, pinned to + /// a literal byte array copied from the committed corpus — not derived by + /// calling `.to_canonical_bytes()` here, for the same reason as the + /// sibling tests above (the 3b-i lesson: round-trip locking alone cannot + /// see a self-consistent encoder/decoder reorder). The mutation this + /// guards against is a swap of discriminant 34 with any neighboring + /// discriminant in both the encoder and the decoder — self-consistent, so + /// every round-trip test stays green, while this correctly-named literal + /// vector dies. + #[test] + fn set_tuning_context_envelope_decode_vector_is_pinned_to_literal_bytes() { + #[rustfmt::skip] + let bytes: Vec = vec![ + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 48, 0, 0, 0, 6, 0, 0, 0, 99, 109, 110, + 45, 49, 50, 6, 0, 0, 0, 116, 101, 116, 45, 49, 50, 0, 5, 0, 4, 8, 0, 0, 0, 0, 0, 0, + 0, 0, 144, 123, 64, 1, 0, 40, 0, 1, 0, 40, 0, 0, 0, 0, 0, + ]; + let result = check("ops.operation_envelope", &bytes) + .expect("ops.operation_envelope is owned by this crate"); + assert_eq!( + result, + Ok(true), + "the committed literal bytes must decode and re-encode injectively" + ); + } } diff --git a/crates/epiphany-testkit/src/generators.rs b/crates/epiphany-testkit/src/generators.rs index 0603b1c..588f939 100644 --- a/crates/epiphany-testkit/src/generators.rs +++ b/crates/epiphany-testkit/src/generators.rs @@ -47,9 +47,9 @@ use epiphany_ops::{ ReplicaAnomalyReason, ReplicaAnomalyRegistryId, ResolutionAction, ResolutionRegistryId, ResolveConflictPayload, RespellPitchOp, SerializedCanonicalInputs, SetCanvasLayoutDefaultsOp, SetMetadataOp, SetMetricGridOp, SetSpellingPrecedenceOp, SetStaffLayoutOp, SetTempoSegmentOp, - SetTimeSignatureOp, SetUserPageBreakOp, SetUserSystemBreakOp, TransactionCategory, - TransactionDescriptor, TransposeIntervalOp, TransposeOp, TupletCompensation, - TupletCompensationKind, UndoPolicy, UndoTransactionPayload, + SetTimeSignatureOp, SetTuningContextOp, SetUserPageBreakOp, SetUserSystemBreakOp, + TransactionCategory, TransactionDescriptor, TransposeIntervalOp, TransposeOp, + TupletCompensation, TupletCompensationKind, UndoPolicy, UndoTransactionPayload, }; use crate::rng::Rng; @@ -646,7 +646,7 @@ pub fn operation_payload(rng: &mut Rng, events: u64, pitches: u64) -> OperationP } _ => {} } - let kind = match rng.below(34) { + let kind = match rng.below(35) { 0 => { let pitches = if rng.boolean() { vec![obj_pitch(rng.below(pitches))] @@ -871,6 +871,10 @@ pub fn operation_payload(rng: &mut Rng, events: u64, pitches: u64) -> OperationP 32 => OperationKind::SetSpellingPrecedence(SetSpellingPrecedenceOp { precedence: valuegen::spelling_precedence(rng.below(3) as u8), }), + // Genesis tranche G2b: the sole genesis payload born at schema major 3. + 33 => OperationKind::SetTuningContext(SetTuningContextOp { + settings: valuegen::tuning_context_settings(rng.below(3) as u8), + }), _ => OperationKind::Registered( OperationKindRegistryId(rng.next_u64() as u128), rng.byte_vec(0, 16), diff --git a/crates/epiphany-testkit/src/roundtrip.rs b/crates/epiphany-testkit/src/roundtrip.rs index 56c724c..35a05c9 100644 --- a/crates/epiphany-testkit/src/roundtrip.rs +++ b/crates/epiphany-testkit/src/roundtrip.rs @@ -686,12 +686,18 @@ mod tests { #[test] fn op_block_beyond_the_accept_set_opens_read_only() { use epiphany_bundle::IntegrityAnomaly; - // A newer writer's op block, stamped schema major 3 — beyond the reader's - // op-block accept-set [0,2]. The bundle opens read-only preservation (the + // A newer writer's op block, stamped schema major 4 — beyond the reader's + // op-block accept-set [0,3]. The bundle opens read-only preservation (the // canonical base and manifest still read) rather than hard-rejecting. + // + // Major 4, not 3: genesis tranche G2b raised the op-block accept-set + // to [0, 3] (`SetTuningContext` is born at major 3), so major 3 is + // now admitted and this test's "beyond the accept-set" major must + // move past it to stay an actual test of the read-only-on-overflow + // path. let block = StagedChunk::operation_block_versioned( encode_block(&[vec![1u8, 2, 3, 4]]), - SchemaVersion::new(3, 0), + SchemaVersion::new(4, 0), ); let reopened = reopen_with_op_block(0xD2_0002, block); assert!( @@ -700,7 +706,7 @@ mod tests { ); assert!(reopened.anomalies().iter().any(|a| matches!( a, - IntegrityAnomaly::UnsupportedCanonicalChunkMajor { schema_major: 3 } + IntegrityAnomaly::UnsupportedCanonicalChunkMajor { schema_major: 4 } ))); } diff --git a/crates/epiphany-testkit/tests/text_projection_grammar.rs b/crates/epiphany-testkit/tests/text_projection_grammar.rs index 20ab755..2f02519 100644 --- a/crates/epiphany-testkit/tests/text_projection_grammar.rs +++ b/crates/epiphany-testkit/tests/text_projection_grammar.rs @@ -304,14 +304,15 @@ fn the_kind_productions_are_the_operation_vocabulary() { // The count is still hand-maintained, which is the same shape the comment // above warns about — derived list, literal total. Every tranche that // appends a tag must bump it (genesis G1 took it from 31 to 32 by adding - // `CreateInstrument`; genesis G2a takes it from 32 to 34 by adding - // `SetCanvasLayoutDefaults` and `SetSpellingPrecedence`). 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. + // `CreateInstrument`; genesis G2a took it from 32 to 34 by adding + // `SetCanvasLayoutDefaults` and `SetSpellingPrecedence`; genesis G2b takes + // it from 34 to 35 by adding `SetTuningContext`). It stays a literal on + // 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(), - 34, - "33 payload-free kinds plus `Registered`" + 35, + "34 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 4f8c011..a1b4063 100644 --- a/crates/epiphany-textproj/src/lib.rs +++ b/crates/epiphany-textproj/src/lib.rs @@ -43,7 +43,13 @@ use epiphany_ops::OperationEnvelope; /// `epiphany-layout-ir` dependency and structurally cannot decode edit-barrier /// bytes). Holding the version while changing the grammar would leave two /// incompatible grammars both claiming `(0 9 0)`. -pub const COMPANION_VERSION: (u32, u32, u32) = (0, 10, 0); +/// +/// Bumped again 0.10.0 → 0.11.0 by the genesis tranche G2b +/// (`spec/CONTRACT_GENESIS_G2B_TUNING.md`), which appended +/// `set-tuning-context` to the `kind` production — the same reasoning as G1 +/// and G2a: extending the grammar without moving this constant would leave +/// two incompatible grammars both claiming `(0 10 0)`. +pub const COMPANION_VERSION: (u32, u32, u32) = (0, 11, 0); /// A parsed canonical Text Projection document. /// diff --git a/crates/epiphany-textproj/src/parse.rs b/crates/epiphany-textproj/src/parse.rs index d415439..ac1b522 100644 --- a/crates/epiphany-textproj/src/parse.rs +++ b/crates/epiphany-textproj/src/parse.rs @@ -649,11 +649,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). 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 10 0))"; + // 0.9.0, genesis G2a; 0.9.0 → 0.10.0, G-minor; 0.10.0 → 0.11.0, genesis + // G2b). Kept a literal because `projection` takes `&[&str]` and a + // formatted String would ripple through every call site; + // `the_test_header_tracks_the_implemented_version` below fails loudly if + // the two ever drift. + const HEADER: &str = "(text-projection (0 11 0))"; 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 61dfdf1..30fa27c 100644 --- a/crates/epiphany-textproj/src/vectors.rs +++ b/crates/epiphany-textproj/src/vectors.rs @@ -329,17 +329,17 @@ pub fn document_vectors() -> Vec { .map(|(name, text)| (SURFACE, "accept", "-", *name, text.as_bytes().to_vec())) .collect(); - // The rejected version must be one this crate does NOT implement. The - // G-minor rung moved `COMPANION_VERSION` to 0.10.0, which had been this + // The rejected version must be one this crate does NOT implement. Genesis + // tranche G2b moved `COMPANION_VERSION` to 0.11.0, which had been this // vector's "future" version — leaving it would have made the negative // vector assert that the *correct* header is rejected. It now names - // 0.9.0, the immediately superseded companion, which is the better test + // 0.10.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 11 0))", "(text-projection (0 10 0))", - "(text-projection (0 9 0))", ); vectors.push(( SURFACE, diff --git a/spec/PASS13_CANDIDATES.md b/spec/PASS13_CANDIDATES.md index 344c8fa..f773561 100644 --- a/spec/PASS13_CANDIDATES.md +++ b/spec/PASS13_CANDIDATES.md @@ -90,6 +90,6 @@ visible — a value with a wire form and no canonical carrier to reach it.) | P13-S10 | **`PitchSpaceModification::Cents(f64)` puts a raw `f64` in canonical state, which the byte layer cannot encode.** `core_spec.tex:3112` declares `Cents(f64)`; `PitchSpaceModification` is reached from canonical score state through `AccidentalDefinition` → `ScoreAccidentalExtensions` → `ScoreTuningContext`. `req:determinism:canonical-floating-point` requires canonical stored floats to be finite IEEE 754 binary64, and the byte layer enforces it with no escape hatch: `serialize.rs:110` decodes floats *only* through `CanonicalF64::from_le_bytes → NonFiniteFloat`, so there is no `Codec for f64`. A raw `f64` is therefore not merely risky in canonical state — it is unencodable without inventing a new unvalidated codec. The same chapter already resolved this exact tension forty lines later: `EngravingBoundingBox` (`:3150`) carries `SpaceUnit` edges with a rationale (`:3191`) invoking the same requirement | this file (verified 2026-07-23 against `core_spec.tex:3112`, `:3150-3197`, and `crates/epiphany-determinism/src/serialize.rs:110`) | **resolved** (ratified: `Cents(CanonicalF64)`. A one-line spec correction, the only shape consistent with the existing byte layer — the identical maneuver Ruling D applied to the bounding box. Freezes in tranche 3b) | | P13-S11 | **`AnchorPoint`, referenced by `AccidentalEngraving.anchor`, is defined nowhere.** `core_spec.tex:3166` names `pub anchor: AnchorPoint`; no struct or enum of that name exists in the spec, and `epiphany-core` (whose `Cargo.toml` has no `epiphany-layout-ir` dependency) cannot borrow any layout-ir type even if one shared the name — the same core-native requirement that forced `EngravingBoundingBox`. An undefined leaf frozen onto the wire is the `KeyContext`-shaped gap. Compounding it: the bounding box is documented "relative to the glyph's anchor point" (`:3160`), so freezing an anchor with no defined coordinate frame freezes a point with an undefined origin | this file (verified 2026-07-23; `AnchorPoint` appears once in `core_spec.tex`, defined nowhere; `epiphany-core/Cargo.toml` lists no layout-ir) | **resolved** (ratified: core-native `AnchorPoint { x: SpaceUnit, y: SpaceUnit }`, over the same `SpaceUnit` as `advance_width` and `EngravingBoundingBox`. **Plus one normative sentence pinning the frame**: x/y in canonical space units, y-up, relative to the glyph's coordinate origin — matching the repo's existing Bravura outline convention — so the anchor and the box it anchors share an unambiguous origin. Freezes in tranche 3b) | | P13-S12 | **`SmuflVersion` is undefined, and its obvious representation orders SMuFL's real history backwards.** `SmuflVersionRequirement` (`core_spec.tex:3269`) carries `minimum`/`authored_against` of type `SmuflVersion`, which Chapter 4 references but does not define (it exists only as a **Chapter 7 / layout-ir** type, `glyph.rs:29`, with literal-minor encoding — see the resolution). Ordering is load-bearing (`SmuflVersionRequirement.minimum`, `:3271`, gates the fallback of `req:tuning:smufl-version-fallback`), and the type is dual-purpose — it also anchors Chapter 9's `GlyphCatalog::smufl_version()` / `GlyphCatalogIdentity` (`:10420`, `:10460`), so this freeze touches layout-conformance identity, not just tuning. The trap: SMuFL versions are decimal fractions — 1.12 (2015), 1.18, 1.20 (2016), 1.3 (2019), 1.4 (2021) — that succeed by fraction (0.12 < 0.18 < 0.20 < 0.30 < 0.40). A `{ major: u16, minor: u16 }` storing literal digits with derived `Ord` orders the minors 3 < 4 < 12 < 18 < 20, placing 1.3 and 1.4 **before** 1.12; old fonts declaring 1.18-era versions exist forever | this file (verified 2026-07-23 against `core_spec.tex:3269-3274` and SMuFL's published release history) | **resolved** (ratified shape `SmuflVersion { major: u16, minor_centi: u16 }`, **literal-minor storage rejected**: the minor is stored **fraction-normalized to hundredths** — 1.12→(1,12), 1.18→(1,18), 1.20→(1,20), 1.3→(1,30), 1.4→(1,40), rule normative, release table a note. Derived `Ord` is then correct across the whole real history and collapses the 1.2/1.20 ambiguity (both → (1,20)). **Correction on filing: the leaf is NOT undefined — it exists in `epiphany-layout-ir/src/glyph.rs:29` as `SmuflVersion { major, minor }` with LITERAL minor (`{1,4}`) and derived `Ord`, so the backwards-ordering bug is LIVE there today (1.3 < 1.12), and it is a direct field of `GlyphCatalogIdentity` — conformance identity.** `epiphany-core` cannot depend on layout-ir, and `SmuflVersionRequirement` is core, so the type MUST be defined in core and layout-ir must reuse it — a **unification**, not a fresh definition, which moves `GlyphCatalogIdentity` (`{1,4}`→`minor_centi 40`). Tranche 3a defines `core::SmuflVersion` for the tuning use and leaves layout-ir's alone (a bounded, core-invisible homonym since core can't import layout-ir's); tranche 3b performs the unification and the deliberate `GlyphCatalogIdentity` move. **Correction (2026-07-23, tranche 3b-ii): "with golden/vector regen" above was verified false before that tranche's dispatch.** No golden, baseline, or vector is pinned to the catalog identity — every assertion on `ResolvedLayoutIR::canonical_bytes()` is relative (stability, determinism, and a `metrics_hash[0] ^= 1` sensitivity check that never touches `smufl_version`), and the committed SVG/PNG goldens do not embed it. The move changes the catalog's emitted bytes (`encode_catalog`'s minor field: `04 00` → `28 00`) in *value*, with nothing to regenerate. The hundredths scale is blocking for 3b, free to adjust in 3a) | -| P13-S13 | **The score tuning context has no canonical persistence path: it can be saved, but never authored, replicated, or merged.** Schema major 3 (Push 4b tranche 3b-i) put `smufl` and `overrides` on the wire, but the only *persisted* carrier that embeds a `ScoreTuningContext` is the **acceleration snapshot**, which Chapter 8 makes explicitly non-canonical and regenerable and which the bundle may discard and rebuild at will. No **canonical** carrier embeds it at all: no operation authors it (`epiphany-ops` has no tuning-context payload anywhere — the vocabulary's only `tuning` references are the per-pitch `TuningReference::Inherit`), and `MaterializedState` carries effects, conflicts, anomalies, objects, spellings, breaks, page-breaks, and pending, but no tuning context. So a user who selects a tuning system or sets a per-voice override has authored something the format cannot durably represent, and `req:tuning:tuning-resolution-order`'s scopes 2–4 (`overrides`) are in practice unreachable from any document a replica could exchange | this file (found 2026-07-24 while scoping text-projection parity; verified by searching the whole `epiphany-ops` operation vocabulary and `MaterializedState`'s field list) | **resolved by the genesis tranche** — `spec/RULING_GENESIS_PERSISTENCE.md` (ratified 2026-07-24, 011c68a) reverses Pass-12 K8 and absorbs genesis into the operation set, naming `SetTuningContext` in its §2 settings table. **This was not a tuning-specific defect and did not get a tuning-specific fix:** the operation arrives as one of nine surfaces in a single coordinated tranche, and the wire layout is untouched. **The disposition, and the evidence that produced it:** `spec/ANALYSIS_GENESIS_PERSISTENCE.md` maps the same gap across the whole `Score`: **eight fields have no operation that can produce them** — `canvas.layout_defaults`, `instruments`, `staff_groups`, `parts`, `tuning_context` (this entry, its field 10), `spelling_precedence`, `analysis_layers`, `views`, plus `identity` — and one more (`decomposition_attachments`) can only be pruned back, never authored. Independently re-verified against the working tree: each field's sole `reduce.rs` mention is a read-only base-seed read. Of the four dispositions weighed there (a canonical genesis block; closing the op-coverage gap; promoting the canonical base to carry graph values; scope-limiting), **the second was taken** — every mutable `Score` field becomes operation-authored — so S13 closes when that tranche lands and is tracked *there*, not here. **One cost this entry contributes as evidence:** the per-field operation route is not the free schema-minor it appears. Blocks stamp *minimally*, at the lowest major whose layouts decode them, and `bundle.rs`'s `max_supported_major` currently caps `OperationEnvelopeBlock` at **2** on the explicit ground that no operation payload embeds the tuning context. A `SetTuningContext`-style operation would make its blocks stamp v3, dragging a role accept-set raise along with the kind append — for one field of eight. That is a concrete instance of the analysis's closing constraint, that any option enumerating fields must be re-audited against its table whenever `Score` gains a field. **This cost is now paid deliberately rather than avoided — but NOT amortised, as first written.** The ruling's "one accept-set raise, spent once" implied a single batch landing every new kind at `OperationEnvelopeBlock` major 3, spreading the raise across nine surfaces. Re-derived 2026-07-28 against the working tree, that is wrong: minimal stamping is a pure function of each payload's value, so the other eight surfaces stamp at major 0, 2, or 2 and never reach the raised bound at all. `SetTuningContext` is the sole payload born at v3, so the raise is charged to **exactly this surface** after all — which is why `spec/PLAN_GENESIS_OPS.md` §4 splits G2 and isolates it in **G2b**. **And the closure argument is not the canonical base:** the base is role-bound to major 0 (`mis_stamped_canonical_base`) and is a `MaterializedState`, which embeds no graph values for *any* field — including `metadata`, op-authored since M2d and durable purely through its operations. S13 closes on that precedent: the op log is canonical, and G2b makes an operation author the tuning context. Consequence to carry forward: once G2b lands, pruning would discard *authored* genesis state rather than merely re-derivable state, so the standing prohibition on pruning (blocked on disposition C) gains real teeth. Note `bundle.rs` documents the cap of 2 *with the tuning-context rationale in prose*, so that comment must move with the cap. Note the wire layouts themselves are already frozen and correct under every disposition: this was never about how the tuning context encodes, only about which carrier embeds it. Not a regression — nothing ever worked; major 3 made the gap visible by giving the value a wire form and no way to reach it) | +| P13-S13 | **The score tuning context has no canonical persistence path: it can be saved, but never authored, replicated, or merged.** Schema major 3 (Push 4b tranche 3b-i) put `smufl` and `overrides` on the wire, but the only *persisted* carrier that embeds a `ScoreTuningContext` is the **acceleration snapshot**, which Chapter 8 makes explicitly non-canonical and regenerable and which the bundle may discard and rebuild at will. No **canonical** carrier embeds it at all: no operation authors it (`epiphany-ops` has no tuning-context payload anywhere — the vocabulary's only `tuning` references are the per-pitch `TuningReference::Inherit`), and `MaterializedState` carries effects, conflicts, anomalies, objects, spellings, breaks, page-breaks, and pending, but no tuning context. So a user who selects a tuning system or sets a per-voice override has authored something the format cannot durably represent, and `req:tuning:tuning-resolution-order`'s scopes 2–4 (`overrides`) are in practice unreachable from any document a replica could exchange | this file (found 2026-07-24 while scoping text-projection parity; verified by searching the whole `epiphany-ops` operation vocabulary and `MaterializedState`'s field list) | **CLOSED by genesis tranche G2b** (`spec/CONTRACT_GENESIS_G2B_TUNING.md`). `SetTuningContext` — kind/tag **34**, schema major **3**, minor epoch **10** — makes an operation author the tuning context, and the op log is canonical. **The closure argument is the `metadata` precedent, not the canonical base:** the base embeds no graph values for *any* field, including `metadata`, which has been op-authored since M2d and is durable purely through its operations. The rung paid the accept-set raise this entry predicted (`OperationEnvelopeBlock` 2→3), charged to this one surface because minimal stamping is a pure function of each payload's value and `ScoreTuningContext` is the only genesis payload born at v3. **The payload carries `epiphany_core::TuningContextSettings`, a five-field subset, not the full graph type** — `accidental_extensions` is staged off the wire, so a full-value payload would have diverged between a live session and the same document reloaded, and `canonical_value!` could not have caught it (it compares bytes, never the originating value). Reduction leaves that field untouched. *Original filing and disposition follow.* — `spec/RULING_GENESIS_PERSISTENCE.md` (ratified 2026-07-24, 011c68a) reverses Pass-12 K8 and absorbs genesis into the operation set, naming `SetTuningContext` in its §2 settings table. **This was not a tuning-specific defect and did not get a tuning-specific fix:** the operation arrives as one of nine surfaces in a single coordinated tranche, and the wire layout is untouched. **The disposition, and the evidence that produced it:** `spec/ANALYSIS_GENESIS_PERSISTENCE.md` maps the same gap across the whole `Score`: **eight fields have no operation that can produce them** — `canvas.layout_defaults`, `instruments`, `staff_groups`, `parts`, `tuning_context` (this entry, its field 10), `spelling_precedence`, `analysis_layers`, `views`, plus `identity` — and one more (`decomposition_attachments`) can only be pruned back, never authored. Independently re-verified against the working tree: each field's sole `reduce.rs` mention is a read-only base-seed read. Of the four dispositions weighed there (a canonical genesis block; closing the op-coverage gap; promoting the canonical base to carry graph values; scope-limiting), **the second was taken** — every mutable `Score` field becomes operation-authored — so S13 closes when that tranche lands and is tracked *there*, not here. **One cost this entry contributes as evidence:** the per-field operation route is not the free schema-minor it appears. Blocks stamp *minimally*, at the lowest major whose layouts decode them, and `bundle.rs`'s `max_supported_major` currently caps `OperationEnvelopeBlock` at **2** on the explicit ground that no operation payload embeds the tuning context. A `SetTuningContext`-style operation would make its blocks stamp v3, dragging a role accept-set raise along with the kind append — for one field of eight. That is a concrete instance of the analysis's closing constraint, that any option enumerating fields must be re-audited against its table whenever `Score` gains a field. **This cost is now paid deliberately rather than avoided — but NOT amortised, as first written.** The ruling's "one accept-set raise, spent once" implied a single batch landing every new kind at `OperationEnvelopeBlock` major 3, spreading the raise across nine surfaces. Re-derived 2026-07-28 against the working tree, that is wrong: minimal stamping is a pure function of each payload's value, so the other eight surfaces stamp at major 0, 2, or 2 and never reach the raised bound at all. `SetTuningContext` is the sole payload born at v3, so the raise is charged to **exactly this surface** after all — which is why `spec/PLAN_GENESIS_OPS.md` §4 splits G2 and isolates it in **G2b**. **And the closure argument is not the canonical base:** the base is role-bound to major 0 (`mis_stamped_canonical_base`) and is a `MaterializedState`, which embeds no graph values for *any* field — including `metadata`, op-authored since M2d and durable purely through its operations. S13 closes on that precedent: the op log is canonical, and G2b makes an operation author the tuning context. Consequence to carry forward: once G2b lands, pruning would discard *authored* genesis state rather than merely re-derivable state, so the standing prohibition on pruning (blocked on disposition C) gains real teeth. Note `bundle.rs` documents the cap of 2 *with the tuning-context rationale in prose*, so that comment must move with the cap. Note the wire layouts themselves are already frozen and correct under every disposition: this was never about how the tuning context encodes, only about which carrier embeds it. Not a regression — nothing ever worked; major 3 made the gap visible by giving the value a wire form and no way to reach it) | | P13-S14 | **The chunk schema *minor* is a normative MUST that no writer has ever honoured, so every appended operation discriminant is unattributable to version skew.** `binary_format.tex:2330` requires a writer to raise the chunk schema minor when it emits any discriminant appended after the minor it otherwise declares, with a stated rationale: so that a decode failure on an unknown appended discriminant is attributable to a version skew rather than to corruption. `SchemaVersion::for_major` (`bundle/src/ids.rs:204`) maps a major to a fixed constant — `V0` is `{0, 1}`, not `{0, 0}` (`ids.rs:173`), and `V1`/`V2`/`V3` carry minor 0 — and, decisively, **takes only a major**, so no per-kind additive minor can reach it — and **both** writer-side staging paths derive nothing but the major (`testkit/src/bundle_harness.rs:25`, `textproj/src/serialize.rs:183`, each mapping `max(OperationEnvelope::schema_major)` through `for_major`). So every kind appended after the initial vocabulary has no additive-version record: **24–27** (Phase-3 first tranche), **28–29** (the schema-major-2 repeat revision), **30** (Push 4a `TransposeInterval`), and **31** (genesis G1 `CreateInstrument`). The requirement's own failure mode is exactly what the gap produces: a reader meeting kind 31 from a newer writer cannot distinguish "my vocabulary is stale" from "these bytes are damaged" | this file (found 2026-07-28 during G2a contract review; verified against the working tree — `for_major`'s signature admits no minor, and neither staging path computes one) | **RESOLVED at `ff9bd0f`** (the G-minor rung, 2026-07-28). Epoch ladder ratified as minors 2–9, one per additive event, complete against `spec/AUDIT_GMINOR_VOCABULARIES.md` and monotonic in the introducing commits' order. `introduced_minor()` landed on the five vocabularies with post-baseline variants, exhaustive and wildcard-free; envelope minor = max over emitted, block = max over envelopes, major and minor derived independently; baselines not normalised. **Two things the filing did not anticipate**, both from the audit that gated the rung: the scope was never just `OperationKind` (`OperationPayload` 3, `ReanchorReason` 6 and `PreconditionFailureReason` 10–15 also append), and the **manifest** reaches `OperationKindTag` through `edit_barriers` with no operation envelope in it — so the "no companion bump" note below is **superseded**: `TextDocument` carries the manifest `SchemaVersion` it cannot derive, and the companion moved 0.9.0 → 0.10.0. Op-block stamping did remain projection-invisible, exactly as scoped. Gate `[7f]` guards the manifest aggregate with an exact-equality oracle. *Original filing follows.* Ruled 2026-07-28: policy ratified 2026-07-28 as a **global additive epoch with content-minimal stamping** (`spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4); the fix is its own rung, sequenced **after G2a and before G2b** — G2b appends kind 34, so running it first would either grow this sweep mid-flight or ship 34 with the very defect the rung retires. G2a appends kinds 32/33 and takes the violation from eight kinds to ten, knowingly — the debt is already eight deep, the +2 is immaterial against it, and one retroactive sweep over 24–33 is strictly less work than 24–31 now and 32–33 later. **What the rung owes:** a minor-assignment policy (which minor each past append belongs to — this is a retroactive judgement, not a derivation), a per-kind `schema_minor()` or equivalent, block minor = max over payloads, a `for_major` replacement that accepts one, and both staging paths. **Scoped 2026-07-28:** the minor IS in the chunk content-address preimage (`chunk_content_hash`, `chunk.rs:177`), so raising it moves every affected `ChunkId` and the manifest body naming it — real address churn, though **not** a `req:format:manifest-id` problem, since that promise is conditional on the same manifest body. It does **not** reach the text projection: op-block schemas are discarded during projection (`textproj/src/project.rs:424`), so no companion bump. And the problem is **not** per-`OperationKind`: an envelope also emits the outer `OperationPayload` discriminant (`ResolveEquivocation` = 3 carries no kind at all), so the gating work is an audit of every append-only vocabulary reachable from an affected payload. Note this is orthogonal to the *major* accept-set: `OperationEnvelopeBlock` stays at 2 through G2a and rises to 3 only at G2b | | P13-S15 | **The `OperationKind` wire golden lock stops at discriminant 29, so kinds 30–33 have no byte-level lock at all.** `operation_kind_wire_discriminants_are_golden` (`crates/epiphany-ops/src/payload.rs:1959`) declares `let table: [(OperationKind, u8); 30]`, covering 0..=29 and asserting for each that `kind.discriminant()` has not moved *and* that the discriminant byte truly leads the canonical encoding. **`TransposeInterval` (30, Push 4a), `CreateInstrument` (31, G1), `SetCanvasLayoutDefaults` (32) and `SetSpellingPrecedence` (33, G2a) are absent from that table**, so their wire discriminants are unlocked: a hand edit to `OperationKind::discriminant()`'s match — itself the site Push 4a got wrong — would move any of the four silently, and the encoding-leads-with-the-byte assertion never runs for them. The macro-guarded `OperationKindTag` half is unaffected (`operation_kind_tag_vocabulary!` is compile-enforced); this is the hand-written half. **The shape is the failure this project keeps rediscovering:** a hand-maintained parallel table that quietly stopped being extended, exactly like the four literal sites found at Push 4a and the six found during G2a — and the golden lock is the one guard specifically meant to catch that class, so its own staleness is the worst place for it | this file (found 2026-07-28 by the G-minor implementation while building s1's coverage; independently verified against the working tree at `ff9bd0f` — the array length literal is `30` and no arm for 30–33 appears in the table) | **open.** Deliberately no code change yet. **Not a live incorrectness:** every one of the four discriminants is currently correct, and `binary_format.tex:1443-1457`/`:1526-1527` carry them normatively — the gap is the *absence of a guard*, not a wrong byte. Fixing it is mechanical (extend the table to `; 34` and add the four rows), and the mutation that proves the fix is to change one of the four discriminants and watch the extended lock fail where it previously stayed green. **Worth doing as its own rung rather than as a rider**, because a golden-lock extension should land with its mutation evidence and nothing else in the diff. Note the sibling `phase3_tag_discriminants_are_golden` should be checked for the same drift at the same time | diff --git a/spec/PLAN_GENESIS_OPS.md b/spec/PLAN_GENESIS_OPS.md index 1f46760..68bff5c 100644 --- a/spec/PLAN_GENESIS_OPS.md +++ b/spec/PLAN_GENESIS_OPS.md @@ -6,10 +6,28 @@ reverses Pass-12 K8 and makes every mutable field of `Score` operation-authored. This plan is the execution scope: what the tranche touches, in what order, and which questions must be answered before a dispatch contract can be written. -**Status:** **G1 landed** (3b09595, CI green) via -`spec/CONTRACT_GENESIS_G1_INSTRUMENT.md`. **G2a contracted** -(`spec/CONTRACT_GENESIS_G2A_SETTINGS.md`); **G-minor**, G2b, and G3 scoped, not -contracted. §6 lists what still needs ratification. +**Status:** the ladder **G1 → G2a → G-minor → G2b** is **complete**; only **G3** +remains. + +* **G1 landed** (3b09595) — `CreateInstrument`, kind/tag 31. +* **G2a landed** (7df5ca1 + 55eff00) — `SetCanvasLayoutDefaults` and + `SetSpellingPrecedence`, kinds/tags 32/33, both major 0. +* **G-minor landed** (ff9bd0f) — the chunk schema minor became a derived + record; epoch ladder ratified at minors 2–9 (`spec/PLAN_GMINOR_SCHEMA_MINOR.md` + §4), governed by `spec/AUDIT_GMINOR_VOCABULARIES.md`. Closed **P13-S14**. +* **G2b landed** — `SetTuningContext`, kind/tag **34**, schema major **3**, + minor epoch **10**. Carried the sole accept-set raise + (`OperationEnvelopeBlock` 2→3) and rewrote the `bundle.rs` rationale it + falsified. Payload is the five-field subset + `epiphany_core::TuningContextSettings`, **not** the full graph type — §5 + trap 7's holdout, resolved in the contract as *subset over normalization*. + Closed **P13-S13**. +* **G3** — the five remaining mint families, plus the delete/modify coverage + design. Scoped, not contracted. + +§6 lists what still needs ratification. **Standing constraint, now with real +teeth:** pruning MUST NOT be implemented until disposition C lands — after G2b +it would discard *authored* genesis state, not merely re-derivable state. **G1 shipped documentation debt** — five normative falsehoods across `binary_format.tex`, `core_spec.tex`, and `operation_catalog.tex`, because its diff --git a/spec/binary_format.pdf b/spec/binary_format.pdf index 52d675f..1e8fc4c 100644 Binary files a/spec/binary_format.pdf and b/spec/binary_format.pdf differ diff --git a/spec/binary_format.tex b/spec/binary_format.tex index 45eb98b..deefcce 100644 --- a/spec/binary_format.tex +++ b/spec/binary_format.tex @@ -1455,6 +1455,13 @@ and the reader disagree about the field's type. \tablenums{33} & \texttt{SetSpellingPrecedence} & $\mathrm{lp}$(\texttt{SpellingPrecedence}) & \sectionsc{SetSpellingPrecedence} \\ + \tablenums{34} & \texttt{SetTuningContext} & + $\mathrm{lp}$(\texttt{TuningContextSettings}) --- the \textbf{authored + subset} of \texttt{ScoreTuningContext}: exactly its five wire-bearing + fields, in the same order, byte-identical to the schema-major-3 + \texttt{ScoreTuningContext} layout (Section~\ref{sec:evolution:major3}); + born at v3 unconditionally & + \sectionsc{SetTuningContext} \\ \bottomrule \end{longtable} \endgroup @@ -1525,6 +1532,7 @@ discriminants and wrong lengths are decode errors. Append-only past \tablenums{28} & \texttt{CreateRepeatStructure} & \tablenums{29} & \texttt{DeleteRepeatStructure} \\ \tablenums{30} & \texttt{TransposeInterval} & \tablenums{31} & \texttt{CreateInstrument} \\ \tablenums{32} & \texttt{SetCanvasLayoutDefaults} & \tablenums{33} & \texttt{SetSpellingPrecedence} \\ + \tablenums{34} & \texttt{SetTuningContext} & & \\ \bottomrule \end{longtable} \endgroup @@ -2332,9 +2340,13 @@ Every chunk declares a \texttt{SchemaVersion} (major, minor) in its role}, not one shared bound (Section~\ref{sec:evolution:major1}'s ``raised per chunk role'' rule, sharpened by major~3, the first bump under which the roles' maxima genuinely diverge): the \texttt{Snapshot} role's max is - \tablenums{3}; the \texttt{OperationEnvelopeBlock} role's max stays - \tablenums{2} (schema major~3 embeds no operation payload, so this role - does not rise); every other role's max is \tablenums{0}. The reference + \tablenums{3}; the \texttt{OperationEnvelopeBlock} role's max is + \tablenums{3} as of genesis tranche G2b + (\texttt{spec/CONTRACT\_GENESIS\_G2B\_TUNING.md} --- + \texttt{SetTuningContext}, \tablenums{34}, is born at v3 + unconditionally; the role stayed at \tablenums{2} from Push~4b tranche + 3b-i, when schema major~3 embedded no operation payload, until this + rung); every other role's max is \tablenums{0}. The reference implementation's overall envelope is $\{0, 1, 2, 3\}$ ($\textsc{min} = 0$, $\textsc{max} = 3$), realized as these differing per-role maxima rather than a single shared bound. @@ -2370,22 +2382,26 @@ The \emph{only} minor-additive mechanism in schema major~0 is \textbf{appending discriminants to open vocabularies}: \begin{itemize} - \item \texttt{OperationKind}: append at ${\geq}\,\tablenums{34}$ + \item \texttt{OperationKind}: append at ${\geq}\,\tablenums{35}$ (Requirement~\ref{req:binfmt:kind-discriminants}; the Phase-3 tranche took \tablenums{24}--\tablenums{27} under this mechanism, the repeat pair \tablenums{28}/\tablenums{29} in the schema-major-2 revision, Push~4a took \tablenums{30} (\texttt{TransposeInterval}), genesis tranche G1 took \tablenums{31} - (\texttt{CreateInstrument}), and genesis tranche G2a took + (\texttt{CreateInstrument}), genesis tranche G2a took \tablenums{32}/\tablenums{33} (\texttt{SetCanvasLayoutDefaults}, - \texttt{SetSpellingPrecedence}) --- the discriminant space is one - append-only table across majors, but a kind's \emph{block stamp} follows - minimal stamping over its payload: \tablenums{28}'s payload embeds a v2 - layout, so its blocks stamp major~2, Section~\ref{sec:evolution:major2}); - \item \texttt{OperationKindTag}: append at ${\geq}\,\tablenums{34}$ - (Requirement~\ref{req:binfmt:kind-tag}; the same ten variants as - \texttt{OperationKind}, at the same ten discriminants, appended by the - same five tranches); + \texttt{SetSpellingPrecedence}), and genesis tranche G2b took + \tablenums{34} (\texttt{SetTuningContext}) --- the discriminant space is + one append-only table across majors, but a kind's \emph{block stamp} + follows minimal stamping over its payload: \tablenums{28}'s payload + embeds a v2 layout, so its blocks stamp major~2, + Section~\ref{sec:evolution:major2}; \tablenums{34}'s payload embeds a v3 + layout unconditionally, so its blocks stamp major~3, + Section~\ref{sec:evolution:major3} --- the sole appended kind to do so); + \item \texttt{OperationKindTag}: append at ${\geq}\,\tablenums{35}$ + (Requirement~\ref{req:binfmt:kind-tag}; the same eleven variants as + \texttt{OperationKind}, at the same eleven discriminants, appended by + the same six tranches); \item \texttt{OperationPayload}: append at ${\geq}\,\tablenums{4}$ (Section~\ref{sec:ops:payload}; \texttt{ResolveEquivocation} = 3, Push~3, is the one and only append this vocabulary has had); @@ -2977,24 +2993,39 @@ layout ahead of a real consumer is exactly what the frozen-layout rule major~1. \subsection{Where the changed fields reach} -Only one payload type changes, and it changes at exactly one site: + +\textbf{As first defined (Push~4b tranche 3b-i), no operation payload +embedded the tuning context} --- \texttt{ScoreTuningContext} reached the +wire only through the acceleration full-\texttt{Score} snapshot, and the +\texttt{OperationEnvelopeBlock} role's accept-set stayed at major~2. This +changed at genesis tranche G2b +(\texttt{spec/CONTRACT\_GENESIS\_G2B\_TUNING.md}), which corrects the claim +below: \begin{itemize} - \item \textbf{Snapshot-only:} \texttt{ScoreTuningContext} reaches the wire - \emph{only} through the acceleration full-\texttt{Score} snapshot. No - operation payload embeds the tuning context (core Chapter~6's operation - vocabulary carries no \texttt{ScoreTuningContext} / \texttt{TuningOverride} - / \texttt{SmuflVersionRequirement} field anywhere), so the - \textbf{canonical operation layer is untouched}: no op payload is ``born - at v3'', and no frozen v2 op-payload decoder is needed. The - \texttt{OperationEnvelopeBlock} role's accept-set therefore stays at - major~2 (Section~\ref{sec:evolution:major2}) --- schema major~3 is the - first data-model bump under which a chunk role's max does \emph{not} - move in lockstep with the others. + \item \textbf{Reaches two sites, as of genesis tranche G2b.} + \texttt{ScoreTuningContext} still reaches the wire through the + acceleration full-\texttt{Score} snapshot, \emph{and} its authored + subset --- \texttt{TuningContextSettings}, exactly the five + wire-bearing fields, byte-identical to this section's layout + (Section~\ref{sec:ops:kinds}, \tablenums{34} + \texttt{SetTuningContext}) --- now reaches the wire through the + canonical operation layer: \texttt{SetTuningContext} is \emph{born at + v3 unconditionally} (the same shape as \texttt{CreateInstrument}'s v2 + birth, Section~\ref{sec:evolution:major2}), since \texttt{smufl} and + \texttt{overrides} are mandatory appends with no lower-major layout for + this payload to fall back to. The \texttt{OperationEnvelopeBlock} + role's accept-set therefore \textbf{widens to major~3} + (Section~\ref{sec:evolution:gate}) --- schema major~3 stopped being the + data-model bump under which a chunk role's max does not move in + lockstep with the others; that distinction now belongs to no major. \item \textbf{The canonical base is unchanged.} The - \texttt{MaterializedState} embeds no tuning context, so it remains - major~0, byte-identical across the bump --- the same keystone as - majors~1 and~2, and a conformance test \SHOULD{} assert it again. + \texttt{MaterializedState} embeds no tuning context (nor any other + \texttt{Score} field value), so it remains major~0, byte-identical + across the bump --- the same keystone as majors~1 and~2, and a + conformance test \SHOULD{} assert it again. This is unaffected by G2b: + the canonical carrier of the tuning context is the operation log, not + the base, so the base's own stamping never moves. \item The manifest stays major~0 (nothing here touches it). \end{itemize} @@ -3005,10 +3036,14 @@ or migrate it on read (the reference implementation migrates). Canonical chunks parse or the bundle opens read-only. Migration \emph{composes}: a major-3 reader migrates a major-0 snapshot v0${\to}$v1${\to}$v2${\to}$v3 in one read, each step total and -default-filling. Because no operation payload changes at this major, a -major-2-only reader's op-block admission is exactly as before this bump --- -only the \texttt{Snapshot} role's accept-set widens, to \tablenums{3} -(Section~\ref{sec:evolution:gate}). +default-filling. As originally defined (Push~4b tranche 3b-i), no operation +payload changed at this major, so a major-2-only reader's op-block +admission was exactly as before the bump and only the \texttt{Snapshot} +role's accept-set widened, to \tablenums{3} +(Section~\ref{sec:evolution:gate}). Genesis tranche G2b (above) changes +this: a major-2-only reader now meets a possible major-3 op block +(\texttt{SetTuningContext}) and opens read-only, exactly the one-way door +Section~\ref{sec:evolution:gate} describes for every other accept-set raise. \subsection{Changed value layout} As in majors~1 and~2, the wire form appends new fields \emph{after} the diff --git a/spec/core_spec.pdf b/spec/core_spec.pdf index 10b3577..1141ea4 100644 Binary files a/spec/core_spec.pdf and b/spec/core_spec.pdf differ diff --git a/spec/core_spec.tex b/spec/core_spec.tex index 19866c5..e9ee7f4 100644 --- a/spec/core_spec.tex +++ b/spec/core_spec.tex @@ -6963,6 +6963,11 @@ pub enum OperationKind { // SetMetadata advisory-LWW pattern. SetCanvasLayoutDefaults(SetCanvasLayoutDefaultsOp), SetSpellingPrecedence(SetSpellingPrecedenceOp), + // Genesis tranche G2b: the sole genesis payload born at schema major 3, + // on the same advisory-LWW pattern. Carries TuningContextSettings, the + // authored subset of ScoreTuningContext (accidental_extensions is not + // on the wire and is left untouched by reduction). + SetTuningContext(SetTuningContextOp), // Layout-semantic operations SetUserSystemBreak(SetUserSystemBreakOp), @@ -11931,6 +11936,8 @@ pub enum OperationKindTag { // Genesis tranche G2a: the two major-0 settings setters. SetCanvasLayoutDefaults, SetSpellingPrecedence, + // Genesis tranche G2b: name-verbatim, as the two prior additions are. + SetTuningContext, } pub enum BarrierScope { diff --git a/spec/operation_catalog.pdf b/spec/operation_catalog.pdf index eecf314..4f8c48e 100644 Binary files a/spec/operation_catalog.pdf and b/spec/operation_catalog.pdf differ diff --git a/spec/operation_catalog.tex b/spec/operation_catalog.tex index 591a6de..6e11a20 100644 --- a/spec/operation_catalog.tex +++ b/spec/operation_catalog.tex @@ -231,7 +231,7 @@ {\Large\scshape\color{epiphanyslate}Operation Catalog}\\[6pt] {\large\itshape\color{epiphanyslate}A companion to the Core Specification}\\[14pt] {\color{epiphanygold}\rule{3in}{0.8pt}}\\[24pt] - {\normalsize\color{epiphanyink}Version 0.10.0 --- The genesis operation tranche (G1 + G2a)}\\[4pt] + {\normalsize\color{epiphanyink}Version 0.11.0 --- The genesis operation tranche reaches schema major 3 (G2b)}\\[4pt] {\small\color{epiphanyslate}Normative for the operation kinds it defines} \vfill \end{titlepage} @@ -382,6 +382,41 @@ clause is narrowed to the canvas object itself, per are no longer outside the operation set; the canvas is still never op-minted. +\medskip + +\noindent\textbf{Version 0.11.0 (genesis tranche G2b, +\texttt{spec/CONTRACT\_GENESIS\_G2B\_TUNING.md}).} Adds one operation kind, +\texttt{SetTuningContext} (Section~\ref{sec:k0:set-tuning-context}), the +third and final genesis-ladder setter deferred at 0.10.0. Carries +\texttt{TuningContextSettings}, the authored subset of +\texttt{ScoreTuningContext} --- exactly its five wire-bearing fields, byte- +identical to the existing schema-major-3 wire walk (Binary Format companion +\sectionsc{Schema Major 3}) --- rather than the full graph type, so that +\texttt{accidental\_extensions} (staged off the wire) cannot diverge between +a live-authored session and the same document reloaded from bytes. + +\textbf{The sole accept-set-raising surface among the nine genesis +settings/creates.} \texttt{SetTuningContext} is schema major~3 +\emph{unconditionally} (\texttt{ScoreTuningContext}'s \texttt{smufl}/ +\texttt{overrides} appends are mandatory, so no lower-major layout for this +payload exists at any value), which is why it alone --- not +\texttt{CreateInstrument} at major~2, and not either G2a setter at major~0 --- +drags the \texttt{OperationEnvelopeBlock} accept-set from \texttt{[0, 2]} to +\texttt{[0, 3]} (Binary Format companion \sectionsc{The Chunk-Level Gate}). +This is the reason the genesis ladder split \texttt{SetTuningContext} into +its own rung rather than landing it alongside \texttt{SetCanvasLayoutDefaults} +/ \texttt{SetSpellingPrecedence}: a one-way accept-set door is not something +to bury inside two unrelated major-0 setters. + +Undo restores the \textbf{seeded base} settings --- default or non-default +alike --- exactly as \texttt{SetMetadata} does; the never-authored / +authored-to-default distinction stays unobservable +(Section~\ref{sec:k0:set-tuning-context}). This closes P13-S13: the +canonical operation log is now the sole canonical carrier of the tuning +context (the canonical base embeds no \texttt{Score} field value for any +setting, \texttt{metadata} included), on the same reasoning that already +applied to \texttt{SetMetadata}. + % =========================================================================== \chapter{The Catalog Framework} \label{ch:framework} @@ -1247,6 +1282,79 @@ state rather than absence. \textbf{Re-anchoring.} Not applicable: the field is a score singleton with no tombstonable anchor. +\section{SetTuningContext} +\label{sec:k0:set-tuning-context} + +Ratified with the genesis operation tranche's G2b rung (\texttt{spec/ +CONTRACT\_GENESIS\_G2B\_TUNING.md}, executing \texttt{spec/ +RULING\_GENESIS\_PERSISTENCE.md}): the score's tuning context settings +(\texttt{Score.tuning\_context}) join the operation-authored surfaces, on +the same \texttt{SetMetadata} pattern as \texttt{SetCanvasLayoutDefaults} +and \texttt{SetSpellingPrecedence} above. Unlike either sibling, +\texttt{SetTuningContext} is the \textbf{sole genesis payload born at +schema major~3} and is the surface that raises the +\texttt{OperationEnvelopeBlock} accept-set from \texttt{[0, 2]} to +\texttt{[0, 3]} (Binary Format companion \sectionsc{The Chunk-Level Gate}). + +\textbf{Payload schema.} \texttt{SetTuningContextOp \{ settings: +TuningContextSettings \}}. \texttt{TuningContextSettings} is the +\textbf{authored subset} of \texttt{ScoreTuningContext}: exactly its five +wire-bearing fields (\texttt{default\_pitch\_space}, +\texttt{default\_tuning\_system}, \texttt{reference}, \texttt{smufl}, +\texttt{overrides}), in the codec's existing order --- not the full +six-field graph type. \texttt{accidental\_extensions} is deliberately +\textbf{not} a field of the carried payload: it is staged out of schema +major~3 (Binary Format companion \sectionsc{Schema Major 3}) and +\texttt{OperationSet::accept} stores the authored envelope as a +\emph{value}, so a payload carrying the full \texttt{ScoreTuningContext} +would let \texttt{accidental\_extensions} reach the graph on the authoring +replica while decoding to empty on every replica that received the +document through serialization --- the same document in two graph states, +depending only on whether it was just authored or just reloaded. The +subset type makes this divergence unrepresentable: a field the payload +does not carry cannot be set inconsistently. + +\textbf{Canonical encoding.} The length-framed canonical bytes of +\texttt{settings}. \texttt{TuningContextSettings}'s encoding is +\textbf{byte-identical} to \texttt{ScoreTuningContext}'s existing +five-field wire walk --- a type-level narrowing, not a new wire form. Since +\texttt{ScoreTuningContext}'s \texttt{smufl} and \texttt{overrides} appends +are mandatory (not \texttt{Option}-hidden), no lower-major layout for this +payload exists at any value: \texttt{SetTuningContext} is \textbf{born at +schema major~3 unconditionally}, the same shape as \texttt{CreateInstrument}'s +unconditional birth at major~2 (Section~\ref{sec:k0:create-instrument}), not +the value-dependent shape \texttt{CreateRegion}/\texttt{SetStaffLayout} use. + +\textbf{Reduction rule.} An advisory last-writer-wins field overwrite, +identical in discipline to \texttt{SetMetadata}: no working state, no +conflict, and no idempotence short-circuit. Graph-aware reduction writes +exactly the five subset fields onto \texttt{Score.tuning\_context} and +\textbf{leaves \texttt{accidental\_extensions} untouched} --- preserving +whatever the graph already held, since the operation carries no +information about that field and normalizing it away (clearing it at +construction, or rejecting a populated in-memory value) would either make +correctness depend on a clearing discipline the compiler cannot see, or +turn an in-memory-only field into an authoring error for callers who never +opted into its persistence. + +\textbf{Conflict cases.} None (advisory LWW). + +\textbf{Undo semantics.} A value-restoring field overwrite +(Section~\ref{sec:k0:undo}), with the chain's value type the \textbf{subset} +\texttt{TuningContextSettings}, not \texttt{ScoreTuningContext}: the write +chain is seeded from the base's five wire-bearing fields at ingest, so +undoing the first operational write restores the pre-operational settings +rather than absence, identically whether the base was seeded with the type +default or a genuine non-default value --- the never-authored / +authored-to-default distinction is unobservable and stays so, since +\texttt{ScoreTuningContext} is an always-valued \texttt{Score} field, like +\texttt{metadata}, with no absent state to return to. \texttt{accidental\_ +extensions} never participates in undo: no operation ever writes it, so +undo leaves it exactly as it found it. + +\textbf{Re-anchoring.} Not applicable: the field is a score singleton with no +tombstonable anchor. + \section{Repeat Structures} \label{sec:k0:repeat-structures} @@ -1604,8 +1712,8 @@ transaction minted is tombstoned. \texttt{StrictInverse} conflicts \emph{Value restoration} (this revision): for every last-writer-wins overwrite the target transaction performed --- event and identified-pitch modification, respelling, cross-cutting modification, metadata, canvas layout defaults, -spelling precedence, metric grid, meter change, tempo segment, staff layout, -and the user break advisories --- the reducer +spelling precedence, tuning context settings, metric grid, meter change, +tempo segment, staff layout, and the user break advisories --- the reducer maintains, per overwritten key, the \emph{canonical-order write chain} of (writer, value) pairs. Undoing the transaction restores each written key to its chain-predecessor value (or its absence, where the transaction introduced diff --git a/spec/text_projection.pdf b/spec/text_projection.pdf index 380ef35..26c6949 100644 Binary files a/spec/text_projection.pdf and b/spec/text_projection.pdf differ diff --git a/spec/text_projection.tex b/spec/text_projection.tex index c71dd5f..ac6dab0 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.10.0 --- The carried manifest schema version reaches the document line}\\[4pt] + {\normalsize\color{epiphanyink}Version 0.11.0 --- The genesis tuning-context setter 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 10 0)}, the version of the companion it implements. It + version: \texttt{(0 11 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.10.0, neither a canonical operation nor canonical reduced state can + version~0.11.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} @@ -1061,6 +1061,7 @@ kind ::= "(insert-event " bytes " " value ")" | "(create-instrument " value ")" | "(set-canvas-layout-defaults " value ")" | "(set-spelling-precedence " value ")" + | "(set-tuning-context " value ")" tuplet-comp ::= "not-in-tuplet" | "(replace-with-rest " value ")" | "(rewrite-tuplets (" bytes* "))" @@ -1137,7 +1138,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 10 0)) +(text-projection (0 11 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) @@ -1372,6 +1373,18 @@ absorb it, exactly as the binary decoder does. both claiming \texttt{(0 9 0)}. Cached projections at \texttt{(0 9 0)} do not migrate; a stale \texttt{TextProjection} chunk is regenerated, not converted. \\ + \today & Chapter 5 & 0.11.0 --- The genesis tuning-context setter reaches + the grammar (genesis tranche G2b, + \texttt{spec/CONTRACT\_GENESIS\_G2B\_TUNING.md}). The \texttt{kind} + production gains \texttt{"(set-tuning-context " value ")"} + (\texttt{req:textproj:operation-vocabulary}), the third appended kind since + the header was gated to a single version at 0.7.0. + + Same forcing reason as every prior grammar-extending bump: holding the + version while extending the grammar would leave two mutually incompatible + grammars both claiming \texttt{(0 10 0)}. Cached projections at + \texttt{(0 10 0)} do not migrate; a stale \texttt{TextProjection} chunk is + regenerated, not converted. \\ \bottomrule \end{longtable} diff --git a/spec/vectors/decode_vectors.txt b/spec/vectors/decode_vectors.txt index d94d835..14070a0 100644 --- a/spec/vectors/decode_vectors.txt +++ b/spec/vectors/decode_vectors.txt @@ -71,8 +71,9 @@ ops.operation_kind_tag accept - tag_30 1e ops.operation_kind_tag accept - tag_31 1f ops.operation_kind_tag accept - tag_32 20 ops.operation_kind_tag accept - tag_33 21 +ops.operation_kind_tag accept - tag_34 22 ops.operation_kind_tag accept - registered 1000000000000000000123456789abcdef -ops.operation_kind_tag reject unknown-discriminant tag_34_one_past_the_vocabulary 22 +ops.operation_kind_tag reject unknown-discriminant tag_35_one_past_the_vocabulary 23 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 @@ -85,6 +86,8 @@ ops.operation_envelope accept - set_canvas_layout_defaults 000000000000000100000 ops.operation_envelope reject trailing-bytes set_canvas_layout_defaults_trailing 000000000000000100000000000000020000000000000000000000000000000001000000000000000100000000000000000000010000000000000002000000000000000000002048000000080000000000000000805a40080000000000000000906240080000000000000000001e40080000000000000000001e40080000000000000000001e40080000000000000000001e4000 ops.operation_envelope accept - set_spelling_precedence 000000000000000100000000000000030000000000000000000000000000000001000000000000000100000000000000000000010000000000000003000000000000000000002109000000050000000403020100 ops.operation_envelope reject trailing-bytes set_spelling_precedence_trailing 00000000000000010000000000000003000000000000000000000000000000000100000000000000010000000000000000000001000000000000000300000000000000000000210900000005000000040302010000 +ops.operation_envelope accept - set_tuning_context 00000000000000010000000000000004000000000000000000000000000000000100000000000000010000000000000000000001000000000000000400000000000000000000223000000006000000636d6e2d3132060000007465742d313200050004080000000000000000907b40010028000100280000000000 +ops.operation_envelope reject trailing-bytes set_tuning_context_trailing 00000000000000010000000000000004000000000000000000000000000000000100000000000000010000000000000000000001000000000000000400000000000000000000223000000006000000636d6e2d3132060000007465742d313200050004080000000000000000907b4001002800010028000000000000 # 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 ee894da..0987071 100644 --- a/spec/vectors/textproj_document_vectors.txt +++ b/spec/vectors/textproj_document_vectors.txt @@ -22,16 +22,16 @@ # document bytes are normative. `` is lowercase with no separators. # textproj.document -textproj.document accept - minimal 28746578742d70726f6a656374696f6e202830203130203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a -textproj.document accept - lineage_custom_profile 28746578742d70726f6a656374696f6e202830203130203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a -textproj.document accept - extension_base_two_envelopes 28746578742d70726f6a656374696f6e202830203130203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a -textproj.document accept - rich_document 28746578742d70726f6a656374696f6e202830203130203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a -textproj.document reject wrong-header-version superseded_companion_version 28746578742d70726f6a656374696f6e2028302039203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a -textproj.document reject blob-line unreferenced_blob 28746578742d70726f6a656374696f6e202830203130203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28626c6f6220226170706c69636174696f6e2f6f637465742d73747265616d222028292023783031290a -textproj.document reject out-of-order-sections canonical_base_before_extension 28746578742d70726f6a656374696f6e202830203130203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a -textproj.document reject repeated-singular-section lineage_repeated 28746578742d70726f6a656374696f6e202830203130203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a -textproj.document reject operation-envelope-order envelopes_reversed 28746578742d70726f6a656374696f6e202830203130203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a -textproj.document reject profile-declaration-order profiles_reversed 28746578742d70726f6a656374696f6e202830203130203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a -textproj.document reject extension-declaration-order extensions_reversed 28746578742d70726f6a656374696f6e202830203130203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a -textproj.document reject extension-chunk-order extension_chunks_reversed 28746578742d70726f6a656374696f6e202830203130203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a -textproj.document reject missing-trailing-lf final_lf_missing 28746578742d70726f6a656374696f6e202830203130203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e20312028292074727565292929 +textproj.document accept - minimal 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a +textproj.document accept - lineage_custom_profile 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a +textproj.document accept - extension_base_two_envelopes 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a +textproj.document accept - rich_document 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a +textproj.document reject wrong-header-version superseded_companion_version 28746578742d70726f6a656374696f6e202830203130203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a +textproj.document reject blob-line unreferenced_blob 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28626c6f6220226170706c69636174696f6e2f6f637465742d73747265616d222028292023783031290a +textproj.document reject out-of-order-sections canonical_base_before_extension 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a +textproj.document reject repeated-singular-section lineage_repeated 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a +textproj.document reject operation-envelope-order envelopes_reversed 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a +textproj.document reject profile-declaration-order profiles_reversed 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a +textproj.document reject extension-declaration-order extensions_reversed 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a +textproj.document reject extension-chunk-order extension_chunks_reversed 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a +textproj.document reject missing-trailing-lf final_lf_missing 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e20312028292074727565292929