diff --git a/crates/epiphany-bundle/src/ids.rs b/crates/epiphany-bundle/src/ids.rs index d010cb3..723d8f9 100644 --- a/crates/epiphany-bundle/src/ids.rs +++ b/crates/epiphany-bundle/src/ids.rs @@ -191,8 +191,16 @@ impl SchemaVersion { /// Schema major 3 — the third data-model expansion major (Binary Format /// companion §"Schema Major 3", Push 4b tranche 3b-i): `ScoreTuningContext` /// gains `smufl` and `overrides` on the wire. Stamped on the acceleration - /// full-`Score` snapshot only — no operation payload embeds the tuning - /// context, so `OperationEnvelopeBlock`'s accept-set is untouched. + /// full-`Score` snapshot **and** on any operation-envelope block carrying a + /// `SetTuningContext` payload. + /// + /// **Genesis tranche G2b raised `OperationEnvelopeBlock`'s accept-set to + /// `[0, 3]`** (`spec/CONTRACT_GENESIS_G2B_TUNING.md`). This comment + /// previously said major 3 was snapshot-only because no operation payload + /// embedded the tuning context; `SetTuningContext` (kind/tag 34) is exactly + /// that payload, carrying `epiphany_core::TuningContextSettings`, so the + /// claim no longer holds. See `max_supported_major` in `bundle.rs`, which + /// carried the same falsehood. pub const V3: SchemaVersion = SchemaVersion { major: 3, minor: 0 }; /// Constructs a schema version. diff --git a/crates/epiphany-ops/src/payload.rs b/crates/epiphany-ops/src/payload.rs index e5d7a76..d73e75a 100644 --- a/crates/epiphany-ops/src/payload.rs +++ b/crates/epiphany-ops/src/payload.rs @@ -2470,6 +2470,9 @@ mod tests { (OperationKindTag::CreateInstrument, 8), (OperationKindTag::SetCanvasLayoutDefaults, 9), (OperationKindTag::SetSpellingPrecedence, 9), + // Genesis G2b. Kept in step with pin 1's table; an epoch omitted + // here is an epoch this test cannot see go wrong. + (OperationKindTag::SetTuningContext, 10), ] { assert_eq!( tag.introduced_minor(), diff --git a/crates/epiphany-ops/src/textproj_kind.rs b/crates/epiphany-ops/src/textproj_kind.rs index 911b51a..281addb 100644 --- a/crates/epiphany-ops/src/textproj_kind.rs +++ b/crates/epiphany-ops/src/textproj_kind.rs @@ -89,7 +89,7 @@ fn parse_bytes(s: &Sexp) -> Result, TextError> { } } -/// Projects and strictly parses all 31 `kind` productions from +/// Projects and strictly parses all 35 `kind` productions from /// `spec/text_projection.tex`. impl TextValue for OperationKind { fn project(&self) -> Sexp { diff --git a/crates/epiphany-testkit/src/generators.rs b/crates/epiphany-testkit/src/generators.rs index 588f939..3bf90ed 100644 --- a/crates/epiphany-testkit/src/generators.rs +++ b/crates/epiphany-testkit/src/generators.rs @@ -1837,7 +1837,9 @@ mod tests { /// where that set's ceiling sits. /// /// The accept-set ceiling itself (`max_supported_major( - /// OperationEnvelopeBlock) == 2`) is **already** asserted in a compiled + /// OperationEnvelopeBlock)`, raised from 2 to **3** by genesis tranche + /// G2b, which introduced the first operation payload born at major 3) + /// is **already** asserted in a compiled /// test inside `epiphany-bundle` (`bundle.rs:1322`), which predates this /// packet and which this packet leaves untouched. An earlier draft of /// this comment claimed that half of s6 was unassertable because @@ -1910,6 +1912,7 @@ mod tests { let mut rng = Rng::new(17); let (mut saw_transpose_interval, mut saw_create_instrument) = (false, false); let (mut saw_canvas_layout_defaults, mut saw_spelling_precedence) = (false, false); + let mut saw_tuning_context = false; for _ in 0..2000 { let OperationPayload::Primitive(kind) = operation_payload(&mut rng, 8, 8) else { continue; @@ -1919,6 +1922,7 @@ mod tests { OperationKind::CreateInstrument(_) => saw_create_instrument = true, OperationKind::SetCanvasLayoutDefaults(_) => saw_canvas_layout_defaults = true, OperationKind::SetSpellingPrecedence(_) => saw_spelling_precedence = true, + OperationKind::SetTuningContext(_) => saw_tuning_context = true, _ => {} } } @@ -1938,6 +1942,10 @@ mod tests { saw_spelling_precedence, "SetSpellingPrecedence (kind 33) never drawn in 2000 samples" ); + assert!( + saw_tuning_context, + "SetTuningContext (kind 34, G2b debt) never drawn in 2000 samples" + ); } #[test] diff --git a/crates/epiphany-testkit/src/roundtrip.rs b/crates/epiphany-testkit/src/roundtrip.rs index 35a05c9..ff40d54 100644 --- a/crates/epiphany-testkit/src/roundtrip.rs +++ b/crates/epiphany-testkit/src/roundtrip.rs @@ -683,6 +683,52 @@ mod tests { assert_eq!(blocks, vec![env.to_canonical_bytes()]); } + /// (G2b, t3+t4 as an integration) A **real** `SetTuningContext` envelope, + /// staged through the production writer path and reopened, must stamp + /// `{3, 10}` and be admitted **read-write**. + /// + /// The unit-level t3/t4 did not prove this. t3 computed + /// `max(schema_major())` over bare `OperationKind`s — never building a + /// block — and t4 only asserted the ceiling constant. Both would stay + /// green if `stage_operation_block` mis-derived the stamp or if the + /// accept-set gate rejected the block it now admits. This test is the one + /// that actually crosses ops → testkit → bundle. + #[test] + fn a_set_tuning_context_block_stamps_3_10_and_reopens_read_write() { + use epiphany_core::{OperationId, ReplicaId, WallClockTime}; + use epiphany_ops::{ + valuegen, AuthorId, CausalContext, HybridLogicalClock, OperationEnvelope, + OperationKind, OperationPayload, OperationStamp, SetTuningContextOp, + }; + + let id = OperationId::new(ReplicaId(1), 1); + let env = OperationEnvelope { + id, + author: AuthorId(0xAB), + stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(100), 0), id), + causal_context: CausalContext::new(), + transaction: None, + payload: OperationPayload::Primitive(OperationKind::SetTuningContext( + SetTuningContextOp { + settings: valuegen::tuning_context_settings(3), + }, + )), + }; + + let staged = crate::bundle_harness::stage_operation_block(&[env]); + assert_eq!( + staged.schema_version, + SchemaVersion::new(3, 10), + "a block carrying SetTuningContext stamps major 3 (the payload is born at v3) and minor 10 (its ratified G2b epoch)" + ); + + let reopened = reopen_with_op_block(0xD2_0003, staged); + assert!( + !reopened.is_read_only(), + "major 3 is inside the raised op-block accept-set [0, 3], so the bundle must open read-write" + ); + } + #[test] fn op_block_beyond_the_accept_set_opens_read_only() { use epiphany_bundle::IntegrityAnomaly; diff --git a/crates/epiphany-textproj/src/vectors.rs b/crates/epiphany-textproj/src/vectors.rs index 30fa27c..2f4cca0 100644 --- a/crates/epiphany-textproj/src/vectors.rs +++ b/crates/epiphany-textproj/src/vectors.rs @@ -24,7 +24,7 @@ use epiphany_bundle::{ use epiphany_core::{OperationId, RegionId, ReplicaId, WallClockTime}; use epiphany_ops::{ AuthorId, CausalContext, DeleteRegionOp, HybridLogicalClock, OperationEnvelope, OperationKind, - OperationPayload, OperationStamp, + OperationPayload, OperationStamp, SetTuningContextOp, }; use crate::parse::parse_document; @@ -158,6 +158,25 @@ fn sample_envelope(counter: u64, physical_time: i64) -> OperationEnvelope { } } +/// A `SetTuningContext` envelope (kind 34, genesis G2b). The committed corpus +/// must contain a real `set-tuning-context` document: the typed all-kind +/// round-trip test proves the *production* parses, but only a committed vector +/// proves the emitted document text is stable across implementations, which is +/// what this corpus exists for. +fn tuning_context_envelope(counter: u64, physical_time: i64) -> OperationEnvelope { + let id = OperationId::new(ReplicaId(1), counter); + OperationEnvelope { + id, + author: AuthorId(0xAB), + stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(physical_time), 0), id), + causal_context: CausalContext::new(), + transaction: None, + payload: OperationPayload::Primitive(OperationKind::SetTuningContext(SetTuningContextOp { + settings: epiphany_ops::valuegen::tuning_context_settings(7), + })), + } +} + fn profiles(custom: bool) -> Vec { let mut profiles = vec![ProfileDeclaration::full()]; if custom { @@ -248,8 +267,22 @@ fn accept_documents() -> Vec<(&'static str, String)> { ], }; + // Genesis G2b: kind 34 in the committed corpus, carried at minor 10 — + // the epoch a block containing this operation actually requires. + let tuning_context = TextDocument { + document_id: DocumentId([5; 16]), + manifest_schema_version: SchemaVersion::new(0, 10), + lineage_id: None, + profiles: profiles(false), + extensions: Vec::new(), + canonical_base: None, + blobs: Vec::new(), + envelopes: vec![tuning_context_envelope(7, 700)], + }; + vec![ ("minimal", project_text_document(&minimal)), + ("set_tuning_context", project_text_document(&tuning_context)), ( "lineage_custom_profile", project_text_document(&lineage_custom), @@ -319,10 +352,23 @@ fn duplicate_first_line(text: &str, head: &str) -> String { /// Every whole-document vector in stable committed order. pub fn document_vectors() -> Vec { let accepts = accept_documents(); - let minimal = &accepts[0].1; - let lineage_custom = &accepts[1].1; - let extension_base_multi = &accepts[2].1; - let rich = &accepts[3].1; + // Bound **by name, not by index**. These were positional (`accepts[0]`.. + // `accepts[3]`), which silently coupled every negative vector to the + // insertion order of the accept list: adding the G2b + // `set_tuning_context` document shifted all four and made the reject + // builders operate on the wrong source text. By-name binding makes the + // accept list reorderable and extendable without touching this block. + let by_name = |name: &str| -> &String { + &accepts + .iter() + .find(|(n, _)| *n == name) + .unwrap_or_else(|| panic!("accept document is absent: {name}")) + .1 + }; + let minimal = by_name("minimal"); + let lineage_custom = by_name("lineage_custom_profile"); + let extension_base_multi = by_name("extension_base_two_envelopes"); + let rich = by_name("rich_document"); let mut vectors: Vec = accepts .iter() @@ -659,7 +705,7 @@ mod tests { #[test] fn the_reference_implementation_agrees_with_every_vector() { match verify(COMMITTED) { - Ok(count) => assert_eq!(count, 13, "the corpus has unexpectedly thinned"), + Ok(count) => assert_eq!(count, 14, "the corpus has unexpectedly thinned"), Err(failures) => panic!( "{} disagreement(s):\n{}", failures.len(), diff --git a/spec/vectors/textproj_document_vectors.txt b/spec/vectors/textproj_document_vectors.txt index 0987071..6b7f00e 100644 --- a/spec/vectors/textproj_document_vectors.txt +++ b/spec/vectors/textproj_document_vectors.txt @@ -23,6 +23,7 @@ # textproj.document textproj.document accept - minimal 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a +textproj.document accept - set_tuning_context 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830353035303530353035303530353035303530353035303530353035303530352028736368656d61203020313029290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303037202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203730302030202378303030303030303030303030303030313030303030303030303030303030303729202863617573616c2028292028292920282920287072696d697469766520287365742d74756e696e672d636f6e74657874202874756e696e672d636f6e746578742d73657474696e67732022636d6e2d31322220227465742d31322220287265666572656e63652d70697463682028636d6e2061203020342920237830303030303030303030663037623430292028736d75666c2d76657273696f6e2d726571756972656d656e742028736d75666c2d76657273696f6e2031203430292028736d75666c2d76657273696f6e20312034302929202829292929290a textproj.document accept - lineage_custom_profile 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a textproj.document accept - extension_base_two_envelopes 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a textproj.document accept - rich_document 28746578742d70726f6a656374696f6e202830203131203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a