diff --git a/crates/epiphany-bundle/src/bundle.rs b/crates/epiphany-bundle/src/bundle.rs index 8ee84a3..83ccaf9 100644 --- a/crates/epiphany-bundle/src/bundle.rs +++ b/crates/epiphany-bundle/src/bundle.rs @@ -158,6 +158,15 @@ pub struct CommitContext<'a> { pub new_chunks: &'a [ChunkRef], /// The generation the new manifest must declare (active + 1). pub generation: u64, + /// The schema version the *previous* manifest was stamped with (G-minor, + /// `spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4, pin 6.2): new plumbing, not a + /// read-through — [`CommitContext::previous_manifest`] is a `&Manifest`, + /// and [`Manifest`] carries no schema-version field at all (it lives in + /// the superblock, `Superblock::manifest_schema_version`). A build + /// closure whose commit preserves the complete barrier content unchanged + /// reads this to preserve the carried version exactly (pin 6.6), rather + /// than recomputing it from scratch. + pub previous_manifest_version: SchemaVersion, } /// An open bundle over a block store. @@ -183,10 +192,29 @@ impl Bundle { /// A crash *during creation* may leave a half-formed file that [`Bundle::open`] /// rejects as corrupt — acceptable, since the file is not yet a bundle. The /// crash-safety guarantee is about *commits to an existing bundle*. - pub fn create( + /// + /// Stamps the manifest chunk at the **baseline** [`Manifest::SCHEMA`] + /// version. A manifest created here declares no canonical roots or + /// blobs (enforced below), so it cannot yet name an edit barrier whose + /// tag requires a higher epoch — see [`Bundle::create_versioned`] for a + /// caller (e.g. a repack seeding a non-empty initial manifest through a + /// different path) that must supply the version explicitly. + pub fn create(store: S, file_uuid: FileUuid, manifest: Manifest) -> Result { + Self::create_versioned(store, file_uuid, manifest, Manifest::SCHEMA) + } + + /// As [`Bundle::create`], but the manifest chunk is stamped at the given + /// `manifest_schema_version` rather than the baseline [`Manifest::SCHEMA`] + /// (G-minor pin 6.1: "producers supply the aggregate manifest + /// `SchemaVersion` explicitly"). `epiphany-bundle` itself never derives + /// this value — it has no dependency on `epiphany-ops` or + /// `epiphany-layout-ir` and cannot decode `prohibited_operation_kinds` — + /// so the caller (a producer with the right dependencies) computes it. + pub fn create_versioned( mut store: S, file_uuid: FileUuid, mut manifest: Manifest, + manifest_schema_version: SchemaVersion, ) -> Result { manifest.generation = 0; manifest.manifest_id = manifest.derive_id(); @@ -216,8 +244,11 @@ impl Bundle { let manifest_payload = manifest.encode(); enforce_limit(manifest_payload.len() as u64, MAX_MANIFEST_BYTES)?; let manifest = Manifest::decode(&manifest_payload)?; - let manifest_hash = - chunk_content_hash(ChunkKind::Manifest, Manifest::SCHEMA, &manifest_payload); + let manifest_hash = chunk_content_hash( + ChunkKind::Manifest, + manifest_schema_version, + &manifest_payload, + ); store.write_at(BODY_START, &manifest_payload)?; store.flush()?; @@ -226,7 +257,7 @@ impl Bundle { manifest_offset: BODY_START, manifest_length: manifest_payload.len() as u64, manifest_hash, - manifest_schema_version: Manifest::SCHEMA, + manifest_schema_version, reduction_algorithm_version: reduction_version_for(&manifest), profile_id: active_profile.profile_id, commit_state: CommitState::Committed, @@ -605,10 +636,32 @@ impl Bundle { /// appended bytes. On an error *at* the commit-point flush the durable result /// is indeterminate (the new superblock may or may not have landed); the /// bundle is poisoned read-only and the caller must reopen from storage. + /// + /// Stamps the new manifest chunk at the **baseline** [`Manifest::SCHEMA`] + /// version. See [`Bundle::commit_versioned`] for a caller that must + /// supply a non-baseline aggregate (e.g. because the manifest the + /// closure builds names an edit barrier prohibiting a post-baseline + /// `OperationKindTag`). pub fn commit( &mut self, new_chunks: &[StagedChunk], build: impl FnOnce(&CommitContext) -> Manifest, + ) -> Result<(), BundleError> { + self.commit_versioned(new_chunks, Manifest::SCHEMA, build) + } + + /// As [`Bundle::commit`], but the new manifest chunk is stamped at the + /// given `manifest_schema_version` rather than the baseline + /// [`Manifest::SCHEMA`] (G-minor pin 6.1). The build closure reads + /// [`CommitContext::previous_manifest_version`] if it needs to decide + /// whether the previous aggregate is still exact (pin 6.6: an ordinary + /// repack preserving the complete barrier content preserves the carried + /// version exactly). + pub fn commit_versioned( + &mut self, + new_chunks: &[StagedChunk], + manifest_schema_version: SchemaVersion, + build: impl FnOnce(&CommitContext) -> Manifest, ) -> Result<(), BundleError> { if self.read_only { return Err(BundleError::ReadOnly); @@ -675,6 +728,7 @@ impl Bundle { previous_manifest: &previous, new_chunks: &new_refs, generation: next_generation, + previous_manifest_version: self.superblock.manifest_schema_version, }); // Extension-root preservation (Chapter 8 §"Behavior Under Unknown @@ -720,8 +774,11 @@ impl Bundle { let manifest_payload = manifest.encode(); enforce_limit(manifest_payload.len() as u64, MAX_MANIFEST_BYTES)?; let manifest = Manifest::decode(&manifest_payload)?; - let manifest_hash = - chunk_content_hash(ChunkKind::Manifest, Manifest::SCHEMA, &manifest_payload); + let manifest_hash = chunk_content_hash( + ChunkKind::Manifest, + manifest_schema_version, + &manifest_payload, + ); let manifest_offset = cursor; self.store.write_at(manifest_offset, &manifest_payload)?; cursor += manifest_payload.len() as u64; @@ -734,7 +791,7 @@ impl Bundle { manifest_offset, manifest_length: manifest_payload.len() as u64, manifest_hash, - manifest_schema_version: Manifest::SCHEMA, + manifest_schema_version, reduction_algorithm_version: reduction_version_for(&manifest), profile_id: active_profile.profile_id, commit_state: CommitState::Committed, @@ -1293,11 +1350,22 @@ fn verified_slot( // Re-exported helper for harnesses that build raw images: the body start offset // and the content hash of a manifest payload. -/// The content hash of a manifest chunk payload (Chapter 8 §"Content Hashing"). +/// The content hash of a manifest chunk payload at the **baseline** +/// [`Manifest::SCHEMA`] version (Chapter 8 §"Content Hashing"). See +/// [`manifest_chunk_hash_versioned`] for a manifest stamped at a +/// non-baseline aggregate (G-minor, `spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4, +/// pin 7). pub fn manifest_chunk_hash(payload: &[u8]) -> ContentHash { chunk_content_hash(ChunkKind::Manifest, Manifest::SCHEMA, payload) } +/// As [`manifest_chunk_hash`], but against the given `schema` rather than the +/// baseline [`Manifest::SCHEMA`] — the hash a manifest naming a post-baseline +/// edit-barrier tag must be content-addressed under. +pub fn manifest_chunk_hash_versioned(payload: &[u8], schema: SchemaVersion) -> ContentHash { + chunk_content_hash(ChunkKind::Manifest, schema, payload) +} + #[cfg(test)] mod tests { use super::*; @@ -1431,6 +1499,123 @@ mod tests { .unwrap() } + // ----------------------------------------------------------------- + // G-minor (`spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4): s10, s11, s12. + // ----------------------------------------------------------------- + + #[test] + fn s10_a_repack_with_unchanged_barrier_content_preserves_the_carried_version() { + // A "repack" here: a second commit whose closure carries forward the + // manifest completely unchanged (no barrier content changed), reading + // the exact prior aggregate from `CommitContext::previous_manifest_ + // version` (pin 6.2/6.6) rather than recomputing it. The carried + // version must survive byte-for-byte. + let mut bundle = Bundle::create_versioned( + MemStore::new(), + FileUuid([9; 16]), + Manifest::empty(DocumentId([9; 16])), + SchemaVersion::new(0, 8), + ) + .unwrap(); + assert_eq!( + bundle.superblock().manifest_schema_version, + SchemaVersion::new(0, 8) + ); + + let mut observed_previous = None; + bundle + .commit_versioned(&[], SchemaVersion::new(0, 8), |ctx| { + observed_previous = Some(ctx.previous_manifest_version); + ctx.previous_manifest.clone() + }) + .unwrap(); + + assert_eq!( + observed_previous, + Some(SchemaVersion::new(0, 8)), + "CommitContext must expose the previous manifest version for the closure to read" + ); + assert_eq!( + bundle.superblock().manifest_schema_version, + SchemaVersion::new(0, 8), + "an unchanged-content repack preserves the carried version exactly" + ); + } + + #[test] + fn s11_bundle_rs_301_accepts_a_manifest_minor_mismatch_at_the_same_major() { + // `open`'s gate at :301 compares `.major` only. A bundle whose + // manifest minor differs from `Manifest::SCHEMA`'s (but whose major + // matches) must still open — the v0 rule that the minor is a record, + // not a gate (pin 7). This is the exact conformance regression pin 7 + // warns tightening the comparison to full-version equality would + // cause. + let bundle = Bundle::create_versioned( + MemStore::new(), + FileUuid([10; 16]), + Manifest::empty(DocumentId([10; 16])), + SchemaVersion::new(0, 8), // differs from Manifest::SCHEMA's minor (1) + ) + .unwrap(); + assert_ne!(SchemaVersion::new(0, 8), Manifest::SCHEMA); + assert_eq!(SchemaVersion::new(0, 8).major, Manifest::SCHEMA.major); + + let image = bundle.into_store().into_bytes(); + let reopened = Bundle::open(MemStore::from_bytes(image)) + .expect("a major-matching minor mismatch opens"); + assert!(!reopened.is_read_only()); + assert_eq!( + reopened.superblock().manifest_schema_version, + SchemaVersion::new(0, 8) + ); + } + + #[test] + fn s12_a_raised_minor_changes_the_chunk_id_and_therefore_the_manifest_id() { + // Two operation blocks with byte-identical payloads but different + // schema minors must have different `ChunkId`s — the minor enters the + // hash preimage (`canonical_bytes`: major then minor). Naming that + // `ChunkRef` from a manifest's `operation_roots` then changes the + // manifest body, and therefore the `ManifestId`. + let payload = vec![1u8, 2, 3]; + let id_v0 = crate::chunk::chunk_id( + ChunkKind::OperationEnvelopeBlock, + SchemaVersion::V0, + &payload, + ); + let id_minor_8 = crate::chunk::chunk_id( + ChunkKind::OperationEnvelopeBlock, + SchemaVersion::new(0, 8), + &payload, + ); + assert_ne!( + id_v0, id_minor_8, + "raising the minor must change the ChunkId" + ); + + let make_manifest = |chunk_id: epiphany_determinism::ChunkId, schema: SchemaVersion| { + let mut m = Manifest::empty(DocumentId([11; 16])); + m.operation_roots = vec![ChunkRef { + id: chunk_id, + kind: ChunkKind::OperationEnvelopeBlock, + schema_version: schema, + offset: 0, + compressed_length: payload.len() as u64, + uncompressed_length: payload.len() as u64, + compression: CompressionAlgorithm::None, + hash: chunk_id.content_hash(), + }]; + m + }; + let manifest_v0 = make_manifest(id_v0, SchemaVersion::V0); + let manifest_minor_8 = make_manifest(id_minor_8, SchemaVersion::new(0, 8)); + assert_ne!( + manifest_v0.derive_id(), + manifest_minor_8.derive_id(), + "a changed ChunkRef in operation_roots must change the ManifestId" + ); + } + #[test] fn create_then_open_round_trips() { let bundle = fresh_bundle(); diff --git a/crates/epiphany-bundle/src/ids.rs b/crates/epiphany-bundle/src/ids.rs index 5178f9f..d010cb3 100644 --- a/crates/epiphany-bundle/src/ids.rs +++ b/crates/epiphany-bundle/src/ids.rs @@ -201,12 +201,22 @@ impl SchemaVersion { SchemaVersion { major, minor } } - /// The current schema version at a given major: [`Self::V0`] for major 0, - /// [`Self::V1`] for major 1, [`Self::V2`] for major 2, [`Self::V3`] for - /// major 3, and `{major, 0}` for any higher (future) major. A writer maps - /// a chunk's derived schema major to a version this way — e.g. an - /// operation-envelope block stamps the max over its operations' - /// `schema_major()`. + /// The **baseline** schema version at a given major: [`Self::V0`] for + /// major 0, [`Self::V1`] for major 1, [`Self::V2`] for major 2, + /// [`Self::V3`] for major 3, and `{major, 0}` for any higher (future) + /// major. A writer maps a chunk's derived schema major to a version this + /// way — e.g. an operation-envelope block stamps the max over its + /// operations' `schema_major()`. + /// + /// This yields the **baseline** minor only (G-minor, + /// `spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4, pin 4): a writer whose payload + /// emits a post-baseline discriminant (an appended `OperationKind`, + /// `OperationKindTag`, `OperationPayload`, `ReanchorReason`, or + /// `PreconditionFailureReason` variant) must use + /// [`Self::for_major_at_epoch`] instead, which raises the minor to the + /// epoch that variant requires. This constructor is **not** replaced — + /// `for_major_at_epoch` needs exactly the baseline this computes as its + /// floor (pin 3: `max(baseline_minor(major), epoch_max)`). #[inline] pub const fn for_major(major: u16) -> Self { match major { @@ -218,6 +228,29 @@ impl SchemaVersion { } } + /// The schema version a writer stamps when its payload's discriminants + /// may require more than `major`'s own baseline minor (G-minor, pin 3): + /// `major` as given, and the minor raised to `epoch_max` when it exceeds + /// [`Self::for_major`]'s baseline minor for that major. `epoch_max` is + /// the maximum [`OperationKind::introduced_minor`](crate)-style epoch + /// actually emitted, or `None` for "no additive requirement" — the + /// distinct sentinel pin 3 requires (never `0`, which is a real baseline + /// minor for `V1`–`V3`). Baselines are never lowered: an `epoch_max` at + /// or below the baseline, or `None`, yields the baseline unchanged. + #[inline] + pub const fn for_major_at_epoch(major: u16, epoch_max: Option) -> Self { + let baseline = SchemaVersion::for_major(major); + if let Some(epoch) = epoch_max { + if epoch > baseline.minor { + return SchemaVersion { + major, + minor: epoch, + }; + } + } + baseline + } + /// Canonical 4 bytes for the hash preimage (Chapter 8 /// §"Domain-Separated Preimages"): major then minor, little-endian. #[inline] @@ -381,6 +414,49 @@ mod tests { assert_ne!(a, c, "a different generation derives a different id"); } + #[test] + fn for_major_at_epoch_keeps_the_baseline_when_no_epoch_applies() { + // s3 anchor: a major-0 payload emitting only baseline vocabulary + // stamps {0, 1} — V0's baseline — never {0, 0}. + assert_eq!( + SchemaVersion::for_major_at_epoch(0, None), + SchemaVersion::V0 + ); + assert_eq!(SchemaVersion::for_major_at_epoch(0, None).minor, 1); + } + + #[test] + fn for_major_at_epoch_raises_the_minor_when_the_epoch_exceeds_baseline() { + assert_eq!( + SchemaVersion::for_major_at_epoch(0, Some(8)), + SchemaVersion::new(0, 8) + ); + } + + #[test] + fn s6_major_and_minor_combine_independently_kind_28_example() { + // A major-2 block containing `CreateRepeatStructure` (kind 28: schema + // major 2, G-minor epoch 6) stamps exactly {2, 6} — the major comes + // from `schema_major()`, the minor from `introduced_minor()`, and + // this constructor combines them without either influencing the + // other (`epiphany-ops`' own `s6_major_and_minor_derive_ + // independently` test establishes the two inputs; this closes the + // loop by combining them the way a real writer does). + assert_eq!( + SchemaVersion::for_major_at_epoch(2, Some(6)), + SchemaVersion::new(2, 6) + ); + } + + #[test] + fn for_major_at_epoch_never_lowers_the_baseline() { + // V1's baseline minor is 0; an epoch below it must not lower it. + assert_eq!( + SchemaVersion::for_major_at_epoch(1, Some(0)), + SchemaVersion::V1 + ); + } + #[test] fn schema_version_canonical_bytes_are_major_then_minor_le() { let v = SchemaVersion::new(0x0102, 0x0304); diff --git a/crates/epiphany-bundle/src/lib.rs b/crates/epiphany-bundle/src/lib.rs index bd67edc..5b796b3 100644 --- a/crates/epiphany-bundle/src/lib.rs +++ b/crates/epiphany-bundle/src/lib.rs @@ -67,8 +67,8 @@ pub use block::{ MAX_BLOCK_DEFAULT, }; pub use bundle::{ - manifest_chunk_hash, Bundle, CommitContext, StagedChunk, BODY_START, MAX_BLOB_BYTES, - MAX_CHUNK_BYTES, MAX_MANIFEST_BYTES, SUPPORTED_SCHEMA_MAJOR, + manifest_chunk_hash, manifest_chunk_hash_versioned, Bundle, CommitContext, StagedChunk, + BODY_START, MAX_BLOB_BYTES, MAX_CHUNK_BYTES, MAX_MANIFEST_BYTES, SUPPORTED_SCHEMA_MAJOR, }; pub use chunk::{ chunk_content_hash, chunk_id, content_hash_for, ChunkKind, ChunkRef, CompressionAlgorithm, diff --git a/crates/epiphany-bundle/src/manifest.rs b/crates/epiphany-bundle/src/manifest.rs index 8c050d9..bd14fb9 100644 --- a/crates/epiphany-bundle/src/manifest.rs +++ b/crates/epiphany-bundle/src/manifest.rs @@ -587,7 +587,17 @@ impl Manifest { Ok(manifest) } - /// The schema version manifests are encoded against in this crate. + /// The **baseline** schema version manifests are encoded against in this + /// crate. Not the universally-emitted version (G-minor, + /// `spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4, pin 7): a manifest naming an + /// edit barrier that prohibits a post-baseline `OperationKindTag` + /// (24–33) must stamp that tag's epoch instead — see + /// `epiphany_layout_ir::barrier::edit_barriers_introduced_minor` for the + /// aggregate derivation, and `Bundle::create_versioned` / + /// `Bundle::commit_versioned` for the producer-supplied seam. This + /// constant remains the correct value for a manifest declaring only + /// baseline barrier tags (or none), and the manifest **major** always + /// stays 0 regardless of the minor stamped. pub const SCHEMA: SchemaVersion = SchemaVersion::V0; /// The *chunk-typed* canonical roots: the operation blocks and the canonical diff --git a/crates/epiphany-layout-ir/src/barrier.rs b/crates/epiphany-layout-ir/src/barrier.rs index dbafacf..ca8a341 100644 --- a/crates/epiphany-layout-ir/src/barrier.rs +++ b/crates/epiphany-layout-ir/src/barrier.rs @@ -773,6 +773,27 @@ pub fn decode_edit_barriers(bytes: &[u8]) -> DecodeResult> { Ok(barriers) } +/// The G-minor schema-minor epoch a set of **decoded** edit barriers requires +/// (`spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4, pin 6; audit §5.1 Part 2): the +/// maximum [`OperationKindTag::introduced_minor`] over every tag named in any +/// barrier's `prohibited_operation_kinds`, or `None` if every named tag is +/// baseline (including when `barriers` is empty). +/// +/// `epiphany-layout-ir` is the only crate that can both decode `EditBarrier` +/// and reach `OperationKindTag`'s epoch table (it depends on `epiphany-ops`; +/// `epiphany-bundle` deliberately does not, pin 6's "the bundle stays +/// opaque"). Callers with undecodable barrier bytes (a foreign extension's +/// blob, say) cannot call this at all — they must fail closed rather than +/// guess (pin 6.4): decode first with [`decode_edit_barriers`], and treat a +/// decode error as "the exact epoch cannot be established", not as baseline. +pub fn edit_barriers_introduced_minor(barriers: &[EditBarrier]) -> Option { + barriers + .iter() + .flat_map(|barrier| barrier.prohibited_operation_kinds.iter()) + .filter_map(|tag| tag.introduced_minor()) + .max() +} + /// Encodes a set of object kinds to the canonical blob stored in /// `ExtensionDeclaration.affected_object_kinds` (the same `push_set` framing /// as [`encode_edit_barriers`]; each element is the kind's 2 LE bytes). @@ -802,6 +823,38 @@ mod tests { TypedObjectId::Event(EventId::from_raw(raw)) } + // ----------------------------------------------------------------- + // G-minor (`spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4, pin 6.5): s9. + // ----------------------------------------------------------------- + + #[test] + fn s9_removing_the_sole_max_contributing_barrier_lowers_the_aggregate() { + let max_barrier = EditBarrier { + scope: BarrierScope::WholeScore, + affected_object_kinds: Vec::new(), + prohibited_operation_kinds: vec![OperationKindTag::CreateInstrument], // epoch 8 + condition: BarrierCondition::Always, + }; + let baseline_barrier = EditBarrier { + scope: BarrierScope::WholeScore, + affected_object_kinds: Vec::new(), + prohibited_operation_kinds: vec![OperationKindTag::InsertEvent], // baseline + condition: BarrierCondition::Always, + }; + let with_both = vec![max_barrier, baseline_barrier.clone()]; + assert_eq!( + edit_barriers_introduced_minor(&with_both), + Some(8), + "the sole max contributor sets the aggregate" + ); + let after_removal = vec![baseline_barrier]; + assert_eq!( + edit_barriers_introduced_minor(&after_removal), + None, + "removing the sole max contributor must lower the aggregate, not retain it" + ); + } + #[test] fn prohibits_matching_op_within_object_set() { let target = ev(1); diff --git a/crates/epiphany-layout-ir/src/lib.rs b/crates/epiphany-layout-ir/src/lib.rs index 31ef1c0..0ae7007 100644 --- a/crates/epiphany-layout-ir/src/lib.rs +++ b/crates/epiphany-layout-ir/src/lib.rs @@ -83,10 +83,10 @@ pub mod time_axis; pub mod vertical_band; pub use barrier::{ - decode_affected_object_kinds, decode_edit_barriers, encode_affected_object_kinds, - encode_edit_barriers, AlwaysLiveOracle, BarrierCondition, BarrierConditionRegistryId, - BarrierDecodeError, BarrierScope, BarrierScopeRegistryId, EditBarrier, EditContext, EditOracle, - ExtensionRef, ObjectKind, MAX_CONDITION_DEPTH, + decode_affected_object_kinds, decode_edit_barriers, edit_barriers_introduced_minor, + encode_affected_object_kinds, encode_edit_barriers, AlwaysLiveOracle, BarrierCondition, + BarrierConditionRegistryId, BarrierDecodeError, BarrierScope, BarrierScopeRegistryId, + EditBarrier, EditContext, EditOracle, ExtensionRef, ObjectKind, MAX_CONDITION_DEPTH, }; pub use cache::{ ConstrainedRegionCache, DependencyIndex, FineLayoutCache, LayoutCache, LogicalRegionCache, diff --git a/crates/epiphany-ops/src/effect.rs b/crates/epiphany-ops/src/effect.rs index 6244750..43e5156 100644 --- a/crates/epiphany-ops/src/effect.rs +++ b/crates/epiphany-ops/src/effect.rs @@ -214,6 +214,39 @@ impl PreconditionFailureReason { } } +impl PreconditionFailureReason { + /// The G-minor schema-minor epoch this variant requires + /// (`spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4, pin 1's ratified table), or + /// `None` for a baseline (0..=9) variant — the distinct "no additive + /// requirement" sentinel pin 3 requires, never `0`. Exhaustive with no + /// wildcard arm (pin 2): a future variant cannot compile without an + /// epoch assignment. + pub fn introduced_minor(&self) -> Option { + match self { + PreconditionFailureReason::TargetMissing + | PreconditionFailureReason::TargetTombstoned + | PreconditionFailureReason::WrongRegionTimeModel + | PreconditionFailureReason::TupletCompensationInvalid + | PreconditionFailureReason::EventDurationInvalid + | PreconditionFailureReason::PositionOutsideRegion + | PreconditionFailureReason::PitchSpaceMismatch + | PreconditionFailureReason::VoiceMissing + | PreconditionFailureReason::ExtensionPrecondition(_) + | PreconditionFailureReason::Registered(_) => None, + // Minor 2 (M2c). + PreconditionFailureReason::ContainerNotEmpty => Some(2), + // Minor 4 (Phase-3 first tranche). + PreconditionFailureReason::TempoMapMalformed => Some(4), + // Minor 5 (Pass-12 G-pass). + PreconditionFailureReason::SystemDerivedContentImmutable => Some(5), + PreconditionFailureReason::RecreateContentMismatch => Some(5), + // Minor 7 (Push 4a). + PreconditionFailureReason::AcousticRealizationPinned => Some(7), + PreconditionFailureReason::TranspositionOutOfRange => Some(7), + } + } +} + impl CanonicalEncode for PreconditionFailureReason { fn encode_canonical(&self, out: &mut Vec) { push_tag(out, self.discriminant()); @@ -343,6 +376,25 @@ impl ReanchorReason { } } +impl ReanchorReason { + /// The G-minor schema-minor epoch this variant requires, or `None` for a + /// baseline (0..=5) variant. See + /// [`PreconditionFailureReason::introduced_minor`] for the sentinel and + /// exhaustiveness discipline this mirrors. + pub fn introduced_minor(&self) -> Option { + match self { + ReanchorReason::SameVoiceNearer + | ReanchorReason::SameStaffInstanceNearer + | ReanchorReason::SameStaffNearer + | ReanchorReason::SameRegionNearer + | ReanchorReason::ExplicitFallback + | ReanchorReason::DeclaredByExtension(_) => None, + // Minor 5 (Pass-12 G-pass, P12-C4). + ReanchorReason::SameCanvasNearer => Some(5), + } + } +} + impl CanonicalEncode for ReanchorReason { fn encode_canonical(&self, out: &mut Vec) { push_tag(out, self.discriminant()); diff --git a/crates/epiphany-ops/src/lib.rs b/crates/epiphany-ops/src/lib.rs index f6204c2..9b84069 100644 --- a/crates/epiphany-ops/src/lib.rs +++ b/crates/epiphany-ops/src/lib.rs @@ -124,14 +124,14 @@ pub use envelope::{ pub use migrate::{migrate_v0_envelope, project_v1_to_v0, MigrationError}; pub use opset::{AcceptOutcome, OperationSet}; pub use payload::{ - ChangeRegionTimeModelOp, CreateCrossCuttingOp, CreateInstrumentOp, CreateRegionOp, - CreateRepeatStructureOp, CreateStaffInstanceOp, CreateStaffOp, CreateVoiceOp, - CrossCuttingValue, DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp, - DeleteRegionOp, DeleteRepeatStructureOp, DeleteStaffInstanceOp, DeleteVoiceOp, InsertEventOp, - InsertIdentifiedPitchOp, ModifyCrossCuttingOp, ModifyEventOp, ModifyIdentifiedPitchOp, - OperationKind, OperationKindTag, OperationPayload, PositionRemapping, ResolveConflictPayload, - ResolveEquivocationPayload, RespellPitchOp, SetCanvasLayoutDefaultsOp, SetMetadataOp, - SetMetricGridOp, SetSpellingPrecedenceOp, SetStaffLayoutOp, SetTempoSegmentOp, + operation_block_introduced_minor, ChangeRegionTimeModelOp, CreateCrossCuttingOp, + CreateInstrumentOp, CreateRegionOp, CreateRepeatStructureOp, CreateStaffInstanceOp, + CreateStaffOp, CreateVoiceOp, CrossCuttingValue, DeleteCrossCuttingOp, DeleteEventOp, + DeleteIdentifiedPitchOp, DeleteRegionOp, DeleteRepeatStructureOp, DeleteStaffInstanceOp, + DeleteVoiceOp, InsertEventOp, InsertIdentifiedPitchOp, ModifyCrossCuttingOp, ModifyEventOp, + ModifyIdentifiedPitchOp, OperationKind, OperationKindTag, OperationPayload, PositionRemapping, + ResolveConflictPayload, ResolveEquivocationPayload, RespellPitchOp, SetCanvasLayoutDefaultsOp, + SetMetadataOp, SetMetricGridOp, SetSpellingPrecedenceOp, SetStaffLayoutOp, SetTempoSegmentOp, SetTimeSignatureOp, SetUserPageBreakOp, SetUserSystemBreakOp, TransactionCategory, TransactionDescriptor, TransposeIntervalOp, TransposeOp, TupletCompensation, }; diff --git a/crates/epiphany-ops/src/payload.rs b/crates/epiphany-ops/src/payload.rs index 1ab6efa..ef46b28 100644 --- a/crates/epiphany-ops/src/payload.rs +++ b/crates/epiphany-ops/src/payload.rs @@ -45,7 +45,7 @@ use epiphany_determinism::{ use crate::conflict::{ConflictId, ResolutionAction}; use crate::encode::{push_canon, push_lp_bytes, push_seq, push_str, push_tag, push_u8_bool}; -use crate::envelope::EnvelopeHash; +use crate::envelope::{EnvelopeHash, OperationEnvelope}; use crate::support::OperationKindRegistryId; use crate::undo::UndoTransactionPayload; @@ -93,6 +93,23 @@ impl OperationPayload { | OperationPayload::ResolveEquivocation(_) => 0, } } + + /// The G-minor schema-minor epoch this payload's own **outer** + /// discriminant requires (`spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4, pin 1), + /// or `None` for "no additive requirement" — the distinct sentinel pin 3 + /// requires, never `0`. This is the outer `OperationPayload` variant + /// only; a `Primitive`'s nested `OperationKind` epoch is a separate + /// contribution an envelope's derivation combines with this one (see + /// `OperationEnvelope::introduced_minor`, below). + pub fn introduced_minor(&self) -> Option { + match self { + OperationPayload::Primitive(_) + | OperationPayload::ResolveConflict(_) + | OperationPayload::UndoTransaction(_) => None, + // Minor 3 (Push 3). + OperationPayload::ResolveEquivocation(_) => Some(3), + } + } } impl CanonicalEncode for OperationPayload { @@ -107,6 +124,40 @@ impl CanonicalEncode for OperationPayload { } } +impl OperationEnvelope { + /// The G-minor schema-minor epoch this envelope's canonical bytes require + /// (`spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4, pin 3): the maximum over its + /// outer [`OperationPayload::introduced_minor`] and — when the payload is + /// [`OperationPayload::Primitive`] — the nested + /// [`OperationKind::introduced_minor`]. `None` when neither imposes a + /// requirement (both are baseline). No other nested vocabulary an + /// envelope's encoder reaches (`OperationEnvelope::encode_canonical`) + /// carries a post-baseline append: the embedded graph values ride + /// `epiphany-core`'s codec, which the G-minor audit found clean + /// (`spec/AUDIT_GMINOR_VOCABULARIES.md` Part 2). + pub fn introduced_minor(&self) -> Option { + let payload_epoch = self.payload.introduced_minor(); + let kind_epoch = match &self.payload { + OperationPayload::Primitive(kind) => kind.introduced_minor(), + OperationPayload::ResolveConflict(_) + | OperationPayload::UndoTransaction(_) + | OperationPayload::ResolveEquivocation(_) => None, + }; + payload_epoch.into_iter().chain(kind_epoch).max() + } +} + +/// An operation-envelope block's required G-minor schema-minor epoch (pin 3): +/// the maximum [`OperationEnvelope::introduced_minor`] over every envelope it +/// carries, or `None` if the block carries no envelope or every envelope is +/// baseline. Callers combine this with the block's schema **major** (an +/// independent derivation, `OperationEnvelope::schema_major`) via +/// `epiphany_bundle::SchemaVersion::for_major_at_epoch` — major and minor +/// never influence each other (pin 3). +pub fn operation_block_introduced_minor(envelopes: &[OperationEnvelope]) -> Option { + envelopes.iter().filter_map(|e| e.introduced_minor()).max() +} + /// The catalog of primitive operation kinds reduced by this crate (Chapter 6 /// §"Operation Envelopes", representative subset of §6.10). // `InsertEvent` carries a whole `Event`, so this variant is intentionally larger @@ -320,6 +371,62 @@ impl OperationKind { } } + /// The G-minor schema-minor epoch this kind's own discriminant requires + /// (`spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4, pin 1's ratified table), or + /// `None` for a baseline (golden-locked 0..=23) variant — the distinct + /// "no additive requirement" sentinel pin 3 requires, never `0`. + /// + /// **Deliberately a separate match from [`Self::discriminant`]** (pin 2): + /// that hand-written match is the site Push 4a's `TransposeInterval` + /// append went wrong by being consulted for only one of the vocabulary's + /// several responsibilities. Exhaustive here too, with no wildcard arm, + /// so a future variant cannot compile without an epoch assignment. + pub fn introduced_minor(&self) -> Option { + match self { + OperationKind::InsertEvent(_) + | OperationKind::DeleteEvent(_) + | OperationKind::RespellPitch(_) + | OperationKind::CreateCrossCutting(_) + | OperationKind::ChangeRegionTimeModel(_) + | OperationKind::SetUserSystemBreak(_) + | OperationKind::DeclareTransaction(_) + | OperationKind::Registered(..) + | OperationKind::ModifyEvent(_) + | OperationKind::Transpose(_) + | OperationKind::InsertIdentifiedPitch(_) + | OperationKind::DeleteIdentifiedPitch(_) + | OperationKind::ModifyIdentifiedPitch(_) + | OperationKind::DeleteCrossCutting(_) + | OperationKind::ModifyCrossCutting(_) + | OperationKind::CreateRegion(_) + | OperationKind::DeleteRegion(_) + | OperationKind::CreateStaffInstance(_) + | OperationKind::DeleteStaffInstance(_) + | OperationKind::CreateVoice(_) + | OperationKind::DeleteVoice(_) + | OperationKind::SetMetadata(_) + | OperationKind::SetMetricGrid(_) + | OperationKind::SetUserPageBreak(_) => None, + // Minor 4 (Phase-3 first tranche). + OperationKind::CreateStaff(_) + | OperationKind::SetTimeSignature(_) + | OperationKind::SetTempoSegment(_) + | OperationKind::SetStaffLayout(_) => Some(4), + // Minor 6 (schema-major-2 repeat-authoring revision). + OperationKind::CreateRepeatStructure(_) | OperationKind::DeleteRepeatStructure(_) => { + Some(6) + } + // Minor 7 (Push 4a). + OperationKind::TransposeInterval(_) => Some(7), + // Minor 8 (Genesis tranche G1). + OperationKind::CreateInstrument(_) => Some(8), + // Minor 9 (Genesis tranche G2a). + OperationKind::SetCanvasLayoutDefaults(_) | OperationKind::SetSpellingPrecedence(_) => { + Some(9) + } + } + } + /// The discriminator-only [`OperationKindTag`] for this kind. Used by edit /// barriers (Chapter 7/8 `prohibited_operation_kinds`) to name a kind /// without its payload. @@ -485,7 +592,7 @@ pub const REGISTERED_TAG_DISCRIMINANT: u8 = 16; /// separate hand-maintained lists — two of them asserting the tag was *unknown* /// — stayed green (Push 5 / P4). macro_rules! operation_kind_tag_vocabulary { - ($($variant:ident = $disc:literal => $catalog:literal),+ $(,)?) => { + ($($variant:ident = $disc:literal => $catalog:literal @ $epoch:expr),+ $(,)?) => { impl OperationKindTag { /// Every payload-free tag, in discriminant order. [`Registered`] /// is excluded: it carries an id and has no bare encoding. @@ -525,44 +632,58 @@ macro_rules! operation_kind_tag_vocabulary { OperationKindTag::Registered(_) => "registered", } } + + /// The G-minor schema-minor epoch this tag requires + /// (`spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4, pin 1), or `None` for + /// "no additive requirement" — never `0`, a real baseline minor + /// for `V1`–`V3` (pin 3). **Co-located inside this macro** (pin + /// 2), not a sibling match: the macro is the compile-enforced + /// source of truth for this vocabulary, exhaustive over every + /// generated arm with no wildcard, including `Registered`. + pub fn introduced_minor(&self) -> Option { + match self { + $(OperationKindTag::$variant => $epoch,)+ + OperationKindTag::Registered(_) => None, + } + } } }; } operation_kind_tag_vocabulary! { - InsertEvent = 0 => "insert-event", - DeleteEvent = 1 => "delete-event", - ModifyEvent = 2 => "modify-event", - RespellPitch = 3 => "respell-pitch", - Transpose = 4 => "transpose", - CreateCrossCutting = 5 => "create-cross-cutting", - DeleteCrossCutting = 6 => "delete-cross-cutting", - ModifyCrossCutting = 7 => "modify-cross-cutting", - ChangeRegionTimeModel = 8 => "change-region-time-model", - InsertRegion = 9 => "create-region", - DeleteRegion = 10 => "delete-region", - InsertStaffInstance = 11 => "create-staff-instance", - DeleteStaffInstance = 12 => "delete-staff-instance", - SetUserSystemBreak = 13 => "set-user-system-break", - SetUserPageBreak = 14 => "set-user-page-break", - DeclareTransaction = 15 => "declare-transaction", - InsertIdentifiedPitch = 17 => "insert-identified-pitch", - DeleteIdentifiedPitch = 18 => "delete-identified-pitch", - ModifyIdentifiedPitch = 19 => "modify-identified-pitch", - CreateVoice = 20 => "create-voice", - DeleteVoice = 21 => "delete-voice", - SetMetadata = 22 => "set-metadata", - SetMetricGrid = 23 => "set-metric-grid", - InsertStaff = 24 => "create-staff", - SetTimeSignature = 25 => "set-time-signature", - SetTempoSegment = 26 => "set-tempo-segment", - SetStaffLayout = 27 => "set-staff-layout", - CreateRepeatStructure = 28 => "create-repeat-structure", - DeleteRepeatStructure = 29 => "delete-repeat-structure", - TransposeInterval = 30 => "transpose-interval", - CreateInstrument = 31 => "create-instrument", - SetCanvasLayoutDefaults = 32 => "set-canvas-layout-defaults", - SetSpellingPrecedence = 33 => "set-spelling-precedence", + InsertEvent = 0 => "insert-event" @ None, + DeleteEvent = 1 => "delete-event" @ None, + ModifyEvent = 2 => "modify-event" @ None, + RespellPitch = 3 => "respell-pitch" @ None, + Transpose = 4 => "transpose" @ None, + CreateCrossCutting = 5 => "create-cross-cutting" @ None, + DeleteCrossCutting = 6 => "delete-cross-cutting" @ None, + ModifyCrossCutting = 7 => "modify-cross-cutting" @ None, + ChangeRegionTimeModel = 8 => "change-region-time-model" @ None, + InsertRegion = 9 => "create-region" @ None, + DeleteRegion = 10 => "delete-region" @ None, + InsertStaffInstance = 11 => "create-staff-instance" @ None, + DeleteStaffInstance = 12 => "delete-staff-instance" @ None, + SetUserSystemBreak = 13 => "set-user-system-break" @ None, + SetUserPageBreak = 14 => "set-user-page-break" @ None, + DeclareTransaction = 15 => "declare-transaction" @ None, + InsertIdentifiedPitch = 17 => "insert-identified-pitch" @ None, + DeleteIdentifiedPitch = 18 => "delete-identified-pitch" @ None, + ModifyIdentifiedPitch = 19 => "modify-identified-pitch" @ None, + CreateVoice = 20 => "create-voice" @ None, + DeleteVoice = 21 => "delete-voice" @ None, + SetMetadata = 22 => "set-metadata" @ None, + SetMetricGrid = 23 => "set-metric-grid" @ None, + InsertStaff = 24 => "create-staff" @ Some(4), + SetTimeSignature = 25 => "set-time-signature" @ Some(4), + SetTempoSegment = 26 => "set-tempo-segment" @ Some(4), + SetStaffLayout = 27 => "set-staff-layout" @ Some(4), + CreateRepeatStructure = 28 => "create-repeat-structure" @ Some(6), + DeleteRepeatStructure = 29 => "delete-repeat-structure" @ Some(6), + TransposeInterval = 30 => "transpose-interval" @ Some(7), + CreateInstrument = 31 => "create-instrument" @ Some(8), + SetCanvasLayoutDefaults = 32 => "set-canvas-layout-defaults" @ Some(9), + SetSpellingPrecedence = 33 => "set-spelling-precedence" @ Some(9), } impl CanonicalEncode for OperationKindTag { @@ -1688,6 +1809,121 @@ mod tests { use super::*; use epiphany_core::{RegionId, ReplicaId, SlurId}; + fn envelope(id: u64, payload: OperationPayload) -> OperationEnvelope { + use crate::causal::CausalContext; + use crate::stamp::{HybridLogicalClock, OperationStamp}; + use crate::support::AuthorId; + use epiphany_core::WallClockTime; + let oid = OperationId::new(ReplicaId(1), id); + OperationEnvelope { + id: oid, + author: AuthorId(1), + stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(id as i64), 0), oid), + causal_context: CausalContext::new(), + transaction: None, + payload, + } + } + + fn create_instrument_envelope(id: u64) -> OperationEnvelope { + envelope( + id, + OperationPayload::Primitive(OperationKind::CreateInstrument(CreateInstrumentOp { + instrument: crate::valuegen::instrument(epiphany_core::InstrumentId::new( + ReplicaId(1), + id, + )), + })), + ) + } + + fn set_user_page_break_envelope(id: u64) -> OperationEnvelope { + // Baseline kind 23 (golden-locked 0..=23): no additive requirement. + envelope( + id, + OperationPayload::Primitive(OperationKind::SetUserPageBreak(SetUserPageBreakOp { + region: RegionId::new(ReplicaId(1), id), + anchor: crate::valuegen::region_start_anchor( + RegionId::new(ReplicaId(1), id), + MusicalPosition::origin(), + ), + present: true, + })), + ) + } + + fn resolve_equivocation_envelope(id: u64) -> OperationEnvelope { + envelope( + id, + OperationPayload::ResolveEquivocation(ResolveEquivocationPayload { + target: OperationId::new(ReplicaId(1), id), + chosen: EnvelopeHash([0; 32]), + }), + ) + } + + fn create_repeat_structure_envelope(id: u64) -> OperationEnvelope { + let repeat_id = RepeatStructureId::new(ReplicaId(1), id); + let a = EventId::new(ReplicaId(1), id * 10 + 1); + let b = EventId::new(ReplicaId(1), id * 10 + 2); + envelope( + id, + OperationPayload::Primitive(OperationKind::CreateRepeatStructure( + CreateRepeatStructureOp { + repeat: crate::valuegen::repeat_structure(repeat_id, a, b), + }, + )), + ) + } + + // ----------------------------------------------------------------- + // G-minor block/envelope derivation: s4, s5, s6. + // ----------------------------------------------------------------- + + #[test] + fn s4_a_block_containing_kind_31_stamps_minor_8() { + let envelopes = vec![create_instrument_envelope(1)]; + assert_eq!(operation_block_introduced_minor(&envelopes), Some(8)); + } + + #[test] + fn s5_mixing_an_old_kind_23_with_resolve_equivocation_stamps_3_not_1() { + // `SetUserPageBreak` is kind 23 (baseline, no additive requirement); + // `ResolveEquivocation` is the outer payload appended at minor 3. The + // block's minor must be the *epoch* max (3), never the highest + // *discriminant* (23) — the rejected policy pin 3 names explicitly. + let envelopes = vec![ + set_user_page_break_envelope(1), + resolve_equivocation_envelope(2), + ]; + assert_eq!(operation_block_introduced_minor(&envelopes), Some(3)); + } + + #[test] + fn s6_major_and_minor_derive_independently() { + // `CreateRepeatStructure` (kind 28) is schema-major 2 (born at v2) and + // epoch 6 (schema-major-2 repeat-authoring revision). A major-2 block + // containing it must stamp minor 6 regardless of major — the two + // derivations must never influence each other. + let envelopes = vec![create_repeat_structure_envelope(1)]; + let major = envelopes.iter().map(|e| e.schema_major()).max().unwrap(); + assert_eq!( + major, 2, + "major derives from schema_major, independent of epoch" + ); + let epoch_max = operation_block_introduced_minor(&envelopes); + assert_eq!( + epoch_max, + Some(6), + "minor epoch derives from introduced_minor, independent of major" + ); + // `SchemaVersion::for_major_at_epoch(2, Some(6))` (tested directly in + // `epiphany-bundle`'s `ids.rs`, which this crate does not depend on) + // combines these two independent values into `{2, 6}` — this test's + // job is only to show the two inputs to that combination are each + // correct and mutually uninfluenced. + } + #[test] fn operation_kind_wire_discriminants_are_golden() { // GOLDEN LOCK: the discriminant byte leads every canonically-encoded @@ -2162,6 +2398,137 @@ mod tests { } } + // ----------------------------------------------------------------- + // G-minor (`spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4, pin 1's ratified + // epoch table): s1, s2. + // ----------------------------------------------------------------- + + #[test] + fn s1_operation_kind_tag_epochs_match_pin1s_table() { + // Every post-baseline `OperationKindTag` epoch, transcribed from pin + // 1's ratified table. + for (tag, expected) in [ + (OperationKindTag::InsertStaff, 4u16), + (OperationKindTag::SetTimeSignature, 4), + (OperationKindTag::SetTempoSegment, 4), + (OperationKindTag::SetStaffLayout, 4), + (OperationKindTag::CreateRepeatStructure, 6), + (OperationKindTag::DeleteRepeatStructure, 6), + (OperationKindTag::TransposeInterval, 7), + (OperationKindTag::CreateInstrument, 8), + (OperationKindTag::SetCanvasLayoutDefaults, 9), + (OperationKindTag::SetSpellingPrecedence, 9), + ] { + assert_eq!( + tag.introduced_minor(), + Some(expected), + "{tag:?} must require epoch {expected}" + ); + } + } + + #[test] + fn s1_operation_kind_epochs_match_tag_epochs_across_every_generated_kind() { + // `OperationKind`'s own epoch table mirrors `OperationKindTag`'s (same + // ten events); rather than hand-build all thirty-four payloads, this + // checks every kind the corpus generator reaches agrees with its own + // tag's already pin-1-verified epoch (s1 above) — so a divergence in + // either table is caught. + use crate::fuzz::gen_envelope_set; + use epiphany_determinism::fuzz::SplitMix64; + let mut rng = SplitMix64::new(0xC0FFEE); + let envelopes = gen_envelope_set(&mut rng, 400); + let mut seen_post_baseline = false; + for envelope in &envelopes { + if let OperationPayload::Primitive(kind) = &envelope.payload { + assert_eq!( + kind.introduced_minor(), + kind.tag().introduced_minor(), + "{:?}: OperationKind and OperationKindTag epochs disagree", + kind.tag() + ); + seen_post_baseline |= kind.introduced_minor().is_some(); + } + } + assert!( + seen_post_baseline, + "fixture reach: the generated set never reached a post-baseline kind" + ); + } + + #[test] + fn s1_operation_payload_and_reanchor_and_precondition_epochs_match_pin1() { + assert_eq!( + OperationPayload::ResolveEquivocation(ResolveEquivocationPayload { + target: OperationId::new(epiphany_core::ReplicaId(1), 1), + chosen: EnvelopeHash([0; 32]), + }) + .introduced_minor(), + Some(3) + ); + assert_eq!( + crate::effect::ReanchorReason::SameCanvasNearer.introduced_minor(), + Some(5) + ); + for (reason, expected) in [ + ( + crate::effect::PreconditionFailureReason::ContainerNotEmpty, + 2u16, + ), + ( + crate::effect::PreconditionFailureReason::TempoMapMalformed, + 4, + ), + ( + crate::effect::PreconditionFailureReason::SystemDerivedContentImmutable, + 5, + ), + ( + crate::effect::PreconditionFailureReason::RecreateContentMismatch, + 5, + ), + ( + crate::effect::PreconditionFailureReason::AcousticRealizationPinned, + 7, + ), + ( + crate::effect::PreconditionFailureReason::TranspositionOutOfRange, + 7, + ), + ] { + assert_eq!(reason.introduced_minor(), Some(expected), "{reason:?}"); + } + } + + #[test] + fn s2_every_baseline_variant_returns_no_additive_requirement() { + // A representative baseline member of each of the five vocabularies. + assert_eq!(OperationKindTag::InsertEvent.introduced_minor(), None); + assert_eq!(OperationKindTag::SetMetricGrid.introduced_minor(), None); + assert_eq!( + OperationPayload::Primitive(OperationKind::InsertEvent(InsertEventOp { + staff_instance: StaffInstanceId::new(epiphany_core::ReplicaId(1), 1), + event: crate::valuegen::insert_event_value( + EventId::new(epiphany_core::ReplicaId(1), 2), + VoiceId::new(epiphany_core::ReplicaId(1), 3), + MusicalPosition::origin(), + MusicalDuration::whole(), + &[], + ), + })) + .introduced_minor(), + None + ); + assert_eq!( + crate::effect::ReanchorReason::SameVoiceNearer.introduced_minor(), + None + ); + assert_eq!( + crate::effect::PreconditionFailureReason::TargetMissing.introduced_minor(), + None + ); + } + #[test] fn reassign_remapping_is_order_independent() { let e1 = EventId::new(ReplicaId(1), 1); diff --git a/crates/epiphany-ops/src/reduce.rs b/crates/epiphany-ops/src/reduce.rs index 6f0a195..6edf4bd 100644 --- a/crates/epiphany-ops/src/reduce.rs +++ b/crates/epiphany-ops/src/reduce.rs @@ -607,6 +607,64 @@ impl MaterializedState { pub fn is_clean(&self) -> bool { self.conflicts.is_empty() && self.anomalies.is_empty() && self.pending.is_empty() } + + /// The canonical base's own G-minor schema-minor epoch (pin 5): the + /// maximum over every later-added discriminant its **own bytes** emit — + /// today, [`ReanchorReason::SameCanvasNearer`] (nested inside + /// [`RepairKind::Reanchored`], itself inside + /// [`OperationEffect::AppliedWithRepair`]) and any post-baseline + /// [`PreconditionFailureReason`] (nested inside + /// [`NoOpReason::PreconditionFailedUnderReduction`], itself inside + /// [`OperationEffect::NoOp`]). `None` if every effect is baseline. + /// + /// **Never `OperationKind`, `OperationKindTag`, or `OperationPayload`** + /// (pin 5): `MaterializedState`'s encoder + /// (`MaterializedState::canonical_bytes`) never emits any of those three + /// vocabularies, so a newer *source* operation alone — e.g. one whose + /// primitive kind is `CreateInstrument` (minor 8) — never moves this + /// value merely by having been reduced; only the effect it *produces* + /// can. + pub fn introduced_minor(&self) -> Option { + self.effects + .iter() + .filter_map(|(_, effect)| effect_introduced_minor(effect)) + .max() + } +} + +/// The G-minor epoch one [`OperationEffect`] contributes to +/// [`MaterializedState::introduced_minor`] (pin 5): walks the two nested +/// vocabularies the canonical base's encoder actually reaches — +/// `RepairKind::Reanchored`'s [`ReanchorReason`] and +/// `NoOpReason::PreconditionFailedUnderReduction`'s +/// [`PreconditionFailureReason`] — and returns their maximum, or `None` if +/// neither applies or both are baseline. +fn effect_introduced_minor(effect: &OperationEffect) -> Option { + match effect { + OperationEffect::AppliedWithRepair { repairs } => repairs + .iter() + .filter_map(|repair| match &repair.kind { + RepairKind::Reanchored { reason, .. } => reason.introduced_minor(), + RepairKind::SpannerTruncated { .. } + | RepairKind::Orphaned + | RepairKind::CascadeDeleted + | RepairKind::AttachmentTombstoned + | RepairKind::VoicePromoted { .. } + | RepairKind::TupletCompensated { .. } + | RepairKind::Registered(_) => None, + }) + .max(), + OperationEffect::NoOp { reason } => match reason { + NoOpReason::PreconditionFailedUnderReduction { reason } => reason.introduced_minor(), + NoOpReason::TargetTombstoned + | NoOpReason::AlreadyApplied + | NoOpReason::SupersededByLaterOperation { .. } + | NoOpReason::TransactionConflict => None, + }, + OperationEffect::Applied + | OperationEffect::Conflicted { .. } + | OperationEffect::TombstonedTarget { .. } => None, + } } /// Reduces an [`OperationSet`] to its canonical [`MaterializedState`]. @@ -7729,6 +7787,49 @@ mod tests { MusicalPosition(RationalTime::from_int(n as i32)) } + // ----------------------------------------------------------------- + // G-minor (`spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4, pin 5): s7. + // ----------------------------------------------------------------- + + #[test] + fn s7_a_canonical_base_whose_effects_include_same_canvas_nearer_stamps_5() { + use epiphany_core::EventId; + let from = TypedObjectId::Event(EventId::new(ReplicaId(1), 1)); + let to = TypedObjectId::Event(EventId::new(ReplicaId(1), 2)); + let state = MaterializedState { + effects: vec![( + OperationId::new(ReplicaId(1), 1), + OperationEffect::AppliedWithRepair { + repairs: vec![RepairRecord { + kind: RepairKind::Reanchored { + from, + to, + reason: ReanchorReason::SameCanvasNearer, + }, + target: from, + }], + }, + )], + ..Default::default() + }; + assert_eq!(state.introduced_minor(), Some(5)); + } + + #[test] + fn s7_a_canonical_base_without_the_effect_stamps_baseline_even_from_kind_33_sources() { + // The base's own bytes carry only a plain `Applied` effect — never an + // `OperationKind`, `OperationKindTag`, or `OperationPayload` — so even + // though the *source operation* that produced it (not modeled in this + // state at all, by construction: pin 5) might have been kind 33 + // (`SetSpellingPrecedence`, epoch 9), the base itself must not be + // dragged to epoch 9. It has no additive requirement. + let state = MaterializedState { + effects: vec![(OperationId::new(ReplicaId(1), 1), OperationEffect::Applied)], + ..Default::default() + }; + assert_eq!(state.introduced_minor(), None); + } + fn insert( replica: u64, counter: u64, diff --git a/crates/epiphany-testkit/examples/conformance_suite.rs b/crates/epiphany-testkit/examples/conformance_suite.rs index bc87d01..f8cfc1a 100644 --- a/crates/epiphany-testkit/examples/conformance_suite.rs +++ b/crates/epiphany-testkit/examples/conformance_suite.rs @@ -12,8 +12,8 @@ //! first violation. use epiphany_testkit::{ - bundle_harness, convergence, corpus, editloop, equivocation, fixtures, generators, layout_stub, - negative, prepass_harness, roundtrip, textproj, Rng, + bundle_harness, convergence, corpus, editloop, equivocation, fixtures, generators, gminor, + layout_stub, negative, prepass_harness, roundtrip, textproj, Rng, }; /// The suite's total gate count, printed in every `[N/TOTAL_GATES]` line and @@ -210,6 +210,23 @@ fn main() { } } + // 7f. G-minor (spec/PLAN_GMINOR_SCHEMA_MINOR.md §4, pin 11): the + // independent oracle over the manifest's carried schema minor, built + // from known, decodable, in-tree fixtures — never routed through + // `TOTAL_GATES`, the same convention 7b-7e already use (this is a + // sub-gate of gate 7, not a new top-level numbered gate). It is NOT + // evidence that `epiphany-textproj` validates arbitrary hand edits + // (pin 11.4): that layer stays a preserving producer by ruled design + // (a), carrying whatever SchemaVersion a document declares verbatim. + eprintln!("[7f ] G-minor: manifest schema-minor oracle over in-tree fixtures"); + { + let (checked, not_checkable) = gminor::run_gate(); + eprintln!( + " {checked} fixture(s) checked (equality oracle), {not_checkable} \ + reported not-checkable (never counted as a pass)" + ); + } + // 8. T2 W3 — the [9/9] golden conformance gate (`golden-gate` feature // only; absent without it, and the line above prints "8/8" unchanged). // Re-derives the three T1a golden states headlessly and rasterizes diff --git a/crates/epiphany-testkit/src/bundle_harness.rs b/crates/epiphany-testkit/src/bundle_harness.rs index e9534b4..9a886df 100644 --- a/crates/epiphany-testkit/src/bundle_harness.rs +++ b/crates/epiphany-testkit/src/bundle_harness.rs @@ -20,15 +20,19 @@ use epiphany_bundle::{ Tear, }; use epiphany_determinism::CanonicalEncode; -use epiphany_ops::{peek_operation_id, OperationEnvelope}; +use epiphany_ops::{operation_block_introduced_minor, peek_operation_id, OperationEnvelope}; /// Stages real operation envelopes into a single op-envelope block, **deriving** /// the block's schema version from its operations: the block major is the max /// over `OperationEnvelope::schema_major` under minimal stamping (a v1 /// `CreateRegion` → major 1; a v2 cross-cutting/staff/metadata value → -/// major 2), mapped to a version by [`SchemaVersion::for_major`]. This is the -/// writer-side derivation every real-envelope staging path must use so a -/// block carrying a versioned payload is never mis-stamped major 0. +/// major 2), and the block minor is the max over +/// `OperationEnvelope::introduced_minor` (G-minor, +/// `spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4) — the block's actually-emitted +/// discriminants, never a fixed baseline. Both are mapped to a version by +/// [`SchemaVersion::for_major_at_epoch`]. This is the writer-side derivation +/// every real-envelope staging path must use so a block carrying a versioned +/// payload is never mis-stamped. pub fn stage_operation_block(envelopes: &[OperationEnvelope]) -> StagedChunk { let payloads: Vec> = envelopes.iter().map(|e| e.to_canonical_bytes()).collect(); let major = envelopes @@ -36,7 +40,11 @@ pub fn stage_operation_block(envelopes: &[OperationEnvelope]) -> StagedChunk { .map(|e| e.schema_major()) .max() .unwrap_or(0); - StagedChunk::operation_block_versioned(encode_block(&payloads), SchemaVersion::for_major(major)) + let epoch_max = operation_block_introduced_minor(envelopes); + StagedChunk::operation_block_versioned( + encode_block(&payloads), + SchemaVersion::for_major_at_epoch(major, epoch_max), + ) } use crate::generators; diff --git a/crates/epiphany-testkit/src/gminor.rs b/crates/epiphany-testkit/src/gminor.rs new file mode 100644 index 0000000..b19dd1f --- /dev/null +++ b/crates/epiphany-testkit/src/gminor.rs @@ -0,0 +1,348 @@ +//! The `[7f]` conformance gate (G-minor, `spec/PLAN_GMINOR_SCHEMA_MINOR.md` +//! §4, pin 11): an independent oracle over the manifest's carried schema +//! minor, built from **known, decodable, in-tree artifacts**. +//! +//! `epiphany-textproj` deliberately never decodes edit-barrier bytes (pin 8: +//! it has no `epiphany-layout-ir` dependency), so it cannot itself check +//! whether a manifest's carried `SchemaVersion` matches what its declared +//! edit barriers require. This module is the independent check that *can*: +//! `epiphany-testkit` depends on `epiphany-layout-ir`, so it can decode +//! `ExtensionDeclaration::edit_barriers`, walk every barrier's +//! `prohibited_operation_kinds`, and recompute the exact aggregate minor the +//! manifest should carry (`epiphany_layout_ir::barrier::edit_barriers_introduced_minor`). +//! +//! **What this gate is not.** It is not evidence that `epiphany-textproj` +//! validates arbitrary hand-edited documents — pin 11's ruled design (a) is +//! that `textproj` stays a *preserving* producer, carrying whatever +//! `SchemaVersion` a document declares verbatim, and a hand-edited document +//! whose barrier bytes changed while its carried version did not is +//! undetectable at that layer by construction. This gate validates a +//! separate, narrower claim: that *this crate's own fixtures*, built directly +//! against `epiphany-bundle`, are exactly and correctly stamped. An +//! undecodable barrier blob (a foreign extension's bytes, a corrupt encoding) +//! is reported as **not-checkable**, never silently counted as a pass. + +use epiphany_bundle::{ + Bundle, DocumentId, ExtensionDeclaration, ExtensionId, FileUuid, Manifest, MemStore, + SchemaVersion, SemVer, +}; +use epiphany_layout_ir::{ + decode_edit_barriers, edit_barriers_introduced_minor, encode_edit_barriers, BarrierCondition, + BarrierScope, EditBarrier, +}; +use epiphany_ops::OperationKindTag; + +/// The oracle's verdict for one manifest. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GminorVerdict { + /// Every barrier blob decoded; `expected` is the recomputed aggregate, + /// `actual` is the carried superblock value, and `matches` is whether + /// they are **exactly equal** — not `>=` (pin 11.1: equality alone + /// catches over-stamping after a contributing barrier is removed). + Checked { + expected: SchemaVersion, + actual: SchemaVersion, + matches: bool, + }, + /// At least one barrier blob failed to decode (a foreign extension, or + /// deliberately corrupt bytes). The exact epoch cannot be established, so + /// this is reported as not-checkable — **never** as a pass (pin 11.3). + NotCheckable(String), +} + +/// A single edit barrier naming `tags`, with an otherwise-trivial +/// whole-score, unconditional scope — the minimum shape needed to exercise +/// `prohibited_operation_kinds`. +fn barrier(tags: &[OperationKindTag]) -> EditBarrier { + EditBarrier { + scope: BarrierScope::WholeScore, + affected_object_kinds: Vec::new(), + prohibited_operation_kinds: tags.to_vec(), + condition: BarrierCondition::Always, + } +} + +fn extension_declaration(id_byte: u8, barriers: &[EditBarrier]) -> ExtensionDeclaration { + ExtensionDeclaration { + extension_id: ExtensionId([id_byte; 16]), + version: SemVer::new(1, 0, 0), + required: false, + preserved_chunk_roots: Vec::new(), + affected_object_kinds: Vec::new(), + edit_barriers: encode_edit_barriers(barriers), + } +} + +/// Builds a bundle whose manifest declares `extensions` and is stamped at +/// `stamped_version` — the two independent knobs every fixture below varies. +fn build_bundle( + seed: u8, + extensions: Vec, + stamped_version: SchemaVersion, +) -> Bundle { + let mut manifest = Manifest::empty(DocumentId([seed; 16])); + manifest.extension_declarations = extensions; + Bundle::create_versioned( + MemStore::new(), + FileUuid([seed; 16]), + manifest, + stamped_version, + ) + .expect("fixture manifest is emittable") +} + +/// The independent oracle (pin 11): recomputes the exact aggregate minor a +/// bundle's manifest should carry from its decodable edit barriers, and +/// compares it **by equality** to the carried superblock value. +pub fn check(bundle: &Bundle) -> GminorVerdict { + let mut all_barriers = Vec::new(); + for declaration in &bundle.manifest().extension_declarations { + match decode_edit_barriers(&declaration.edit_barriers) { + Ok(mut decoded) => all_barriers.append(&mut decoded), + Err(error) => { + return GminorVerdict::NotCheckable(format!( + "extension {:?}: edit_barriers did not decode: {error:?}", + declaration.extension_id + )) + } + } + } + let epoch_max = edit_barriers_introduced_minor(&all_barriers); + let expected = SchemaVersion::for_major_at_epoch(0, epoch_max); + let actual = bundle.superblock().manifest_schema_version; + GminorVerdict::Checked { + expected, + actual, + matches: expected == actual, + } +} + +/// Fixture: a manifest naming barrier tag 31 (`CreateInstrument`, epoch 8), +/// correctly stamped at exactly that epoch. Positive: the oracle must find +/// `matches: true`. +pub fn fixture_correctly_stamped_tag_31() -> Bundle { + let declarations = vec![extension_declaration( + 1, + &[barrier(&[OperationKindTag::CreateInstrument])], + )]; + build_bundle(1, declarations, SchemaVersion::new(0, 8)) +} + +/// Fixture: a manifest naming only baseline tags, correctly stamped at the +/// baseline `{0, 1}`. Positive. +pub fn fixture_baseline_only() -> Bundle { + let declarations = vec![extension_declaration( + 2, + &[barrier(&[ + OperationKindTag::InsertEvent, + OperationKindTag::DeleteEvent, + ])], + )]; + build_bundle(2, declarations, SchemaVersion::V0) +} + +/// Negative fixture (pin 11.2, required #1): a blob naming tag 31 (epoch 8), +/// but the manifest carries the **baseline** version — under-stamped. The +/// oracle must find `matches: false`. +pub fn fixture_understamped() -> Bundle { + let declarations = vec![extension_declaration( + 3, + &[barrier(&[OperationKindTag::CreateInstrument])], + )]; + build_bundle(3, declarations, SchemaVersion::V0) +} + +/// Negative fixture (pin 11.2, required #2): two barriers, one naming tag 31 +/// (epoch 8, the sole max contributor) and one baseline-only. The barrier +/// contributing the maximum is then **removed** (only the baseline barrier +/// remains), but the manifest retains the old aggregate `{0, 8}` — exactly +/// the over-stamp pin 6.5 warns "blindly retaining the previous aggregate" +/// produces. The oracle must find `matches: false`. +pub fn fixture_overstamped_after_barrier_removal() -> Bundle { + let declarations = vec![extension_declaration( + 4, + &[barrier(&[OperationKindTag::InsertEvent])], + )]; + build_bundle(4, declarations, SchemaVersion::new(0, 8)) +} + +/// Fixture: a manifest declaring an extension whose `edit_barriers` blob is +/// deliberately corrupt (not a valid canonical `EditBarrier` set encoding). +/// The oracle must report `NotCheckable`, never a pass (pin 11.3). +pub fn fixture_undecodable_barrier_blob() -> Bundle { + let declaration = ExtensionDeclaration { + extension_id: ExtensionId([5; 16]), + version: SemVer::new(1, 0, 0), + required: false, + preserved_chunk_roots: Vec::new(), + affected_object_kinds: Vec::new(), + // Not a canonical edit-barrier-set encoding: garbage bytes that a + // real foreign/corrupt extension could plausibly carry. + edit_barriers: vec![0xFF, 0x00, 0x13, 0x37, 0xAB], + }; + build_bundle(5, vec![declaration], SchemaVersion::V0) +} + +/// Runs the whole `[7f]` gate: every fixture, checked against its expected +/// verdict shape. Returns `(checked, not_checkable)` — the two counts the +/// conformance suite reports (pin 11.4: describing the gate's actual reach +/// honestly, not implying it validates arbitrary edits). +pub fn run_gate() -> (usize, usize) { + let mut checked = 0usize; + let mut not_checkable = 0usize; + + let positive_correct = check(&fixture_correctly_stamped_tag_31()); + assert_eq!( + positive_correct, + GminorVerdict::Checked { + expected: SchemaVersion::new(0, 8), + actual: SchemaVersion::new(0, 8), + matches: true, + }, + "a manifest naming tag 31, correctly stamped at minor 8, must check as matching" + ); + checked += 1; + + let positive_baseline = check(&fixture_baseline_only()); + assert_eq!( + positive_baseline, + GminorVerdict::Checked { + expected: SchemaVersion::V0, + actual: SchemaVersion::V0, + matches: true, + }, + "a manifest naming only baseline tags must check as matching its baseline stamp" + ); + checked += 1; + + let negative_understamped = check(&fixture_understamped()); + assert_eq!( + negative_understamped, + GminorVerdict::Checked { + expected: SchemaVersion::new(0, 8), + actual: SchemaVersion::V0, + matches: false, + }, + "an under-stamped manifest (tag 31 present, baseline carried) must be caught" + ); + checked += 1; + + let negative_overstamped = check(&fixture_overstamped_after_barrier_removal()); + assert_eq!( + negative_overstamped, + GminorVerdict::Checked { + expected: SchemaVersion::V0, + actual: SchemaVersion::new(0, 8), + matches: false, + }, + "an over-stamped manifest (max contributor removed, old aggregate retained) must be caught" + ); + checked += 1; + + match check(&fixture_undecodable_barrier_blob()) { + GminorVerdict::NotCheckable(_) => not_checkable += 1, + other => panic!("an undecodable barrier blob must report NotCheckable, got {other:?}"), + } + + (checked, not_checkable) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn s8_manifest_naming_tag_31_stamps_8_baseline_tags_stamp_baseline() { + assert_eq!( + check(&fixture_correctly_stamped_tag_31()), + GminorVerdict::Checked { + expected: SchemaVersion::new(0, 8), + actual: SchemaVersion::new(0, 8), + matches: true, + } + ); + assert_eq!( + check(&fixture_baseline_only()), + GminorVerdict::Checked { + expected: SchemaVersion::V0, + actual: SchemaVersion::V0, + matches: true, + } + ); + } + + #[test] + fn s15_the_gate_fails_the_understamped_fixture() { + let verdict = check(&fixture_understamped()); + assert_eq!( + verdict, + GminorVerdict::Checked { + expected: SchemaVersion::new(0, 8), + actual: SchemaVersion::V0, + matches: false, + } + ); + } + + #[test] + fn s16_the_gate_fails_the_overstamped_fixture() { + let verdict = check(&fixture_overstamped_after_barrier_removal()); + assert_eq!( + verdict, + GminorVerdict::Checked { + expected: SchemaVersion::V0, + actual: SchemaVersion::new(0, 8), + matches: false, + } + ); + } + + #[test] + fn s17_an_undecodable_blob_is_reported_not_checkable_never_a_pass() { + assert!(matches!( + check(&fixture_undecodable_barrier_blob()), + GminorVerdict::NotCheckable(_) + )); + } + + #[test] + fn the_gate_reports_four_checked_and_one_not_checkable() { + assert_eq!(run_gate(), (4, 1)); + } + + #[test] + fn correctly_stamped_fixtures_match() { + assert!(matches!( + check(&fixture_correctly_stamped_tag_31()), + GminorVerdict::Checked { matches: true, .. } + )); + assert!(matches!( + check(&fixture_baseline_only()), + GminorVerdict::Checked { matches: true, .. } + )); + } + + #[test] + fn understamped_fixture_is_caught() { + assert!(matches!( + check(&fixture_understamped()), + GminorVerdict::Checked { matches: false, .. } + )); + } + + #[test] + fn overstamped_fixture_is_caught() { + assert!(matches!( + check(&fixture_overstamped_after_barrier_removal()), + GminorVerdict::Checked { matches: false, .. } + )); + } + + #[test] + fn undecodable_barrier_blob_is_not_checkable_not_a_pass() { + assert!(matches!( + check(&fixture_undecodable_barrier_blob()), + GminorVerdict::NotCheckable(_) + )); + } +} diff --git a/crates/epiphany-testkit/src/lib.rs b/crates/epiphany-testkit/src/lib.rs index dd59493..060b4b4 100644 --- a/crates/epiphany-testkit/src/lib.rs +++ b/crates/epiphany-testkit/src/lib.rs @@ -123,6 +123,7 @@ pub mod negative; pub mod bundle_harness; +pub mod gminor; pub mod layout_stub; // The editing-loop vertical slice: hit-test → score object → operation → reduce → diff --git a/crates/epiphany-testkit/src/textproj.rs b/crates/epiphany-testkit/src/textproj.rs index 9e0a0a5..854bbd4 100644 --- a/crates/epiphany-testkit/src/textproj.rs +++ b/crates/epiphany-testkit/src/textproj.rs @@ -9,7 +9,7 @@ //! intentionally regenerates layout, collapses duplicate blobs, and omits //! non-canonical accelerators; none of those changes operation semantics. -use epiphany_bundle::{DocumentId, FileUuid, MemStore, ProfileDeclaration}; +use epiphany_bundle::{DocumentId, FileUuid, MemStore, ProfileDeclaration, SchemaVersion}; use epiphany_ops::{OperationEnvelope, OperationSet}; use epiphany_textproj::parse::parse_document; use epiphany_textproj::project::{document_from_bundle, project_bundle}; @@ -36,6 +36,7 @@ pub fn assert_semantics_preserved(seed: u64) { let source = TextDocument { document_id: DocumentId(seed.to_le_bytes().repeat(2).try_into().expect("16 bytes")), + manifest_schema_version: SchemaVersion::V0, lineage_id: None, profiles: vec![ProfileDeclaration::full()], extensions: Vec::new(), diff --git a/crates/epiphany-testkit/tests/requirement_labels.rs b/crates/epiphany-testkit/tests/requirement_labels.rs index 862e822..edd8828 100644 --- a/crates/epiphany-testkit/tests/requirement_labels.rs +++ b/crates/epiphany-testkit/tests/requirement_labels.rs @@ -10,8 +10,10 @@ use std::fs; use std::path::{Path, PathBuf}; const CORE_REQUIREMENT_COUNT: usize = 212; -const SUITE_REQUIREMENT_COUNT: usize = 282; -const SUITE_LABEL_COUNT: usize = 282; +// +1 for req:textproj:manifest-schema-carried (G-minor, pins 8/11: +// spec/PLAN_GMINOR_SCHEMA_MINOR.md). +const SUITE_REQUIREMENT_COUNT: usize = 283; +const SUITE_LABEL_COUNT: usize = 283; /// The normative chapter-to-area assignment. Keeping this as data makes adding a /// requirement under the wrong chapter fail without encoding chapter names in diff --git a/crates/epiphany-textproj/src/lib.rs b/crates/epiphany-textproj/src/lib.rs index 8c502bc..4f8c011 100644 --- a/crates/epiphany-textproj/src/lib.rs +++ b/crates/epiphany-textproj/src/lib.rs @@ -31,7 +31,19 @@ use epiphany_ops::OperationEnvelope; /// `set-canvas-layout-defaults` and `set-spelling-precedence` to the `kind` /// production — the same reasoning: extending the grammar without moving this /// constant would leave two incompatible grammars both claiming `(0 8 0)`. -pub const COMPANION_VERSION: (u32, u32, u32) = (0, 9, 0); +/// +/// Bumped again 0.9.0 → 0.10.0 by the G-minor rung +/// (`spec/PLAN_GMINOR_SCHEMA_MINOR.md`), which added the carried manifest +/// [`SchemaVersion`] to the `document` production +/// (`document ::= "(document " bytes " " schema ")"`). **Not** because of +/// operation-block schema-minor stamping — block schemas are discarded during +/// projection and stay projection-invisible, exactly as before — but because +/// the manifest's aggregate `SchemaVersion` becomes a carried `TextDocument` +/// attribute that this companion cannot derive (`epiphany-textproj` has no +/// `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); /// A parsed canonical Text Projection document. /// @@ -49,6 +61,14 @@ pub const COMPANION_VERSION: (u32, u32, u32) = (0, 9, 0); pub struct TextDocument { /// Logical identity of the projected document. pub document_id: DocumentId, + /// The manifest's aggregate G-minor `SchemaVersion` + /// (`spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4, pins 8/11): carried verbatim, + /// never derived. This companion has no `epiphany-layout-ir` dependency + /// and cannot decode `ExtensionDeclaration::edit_barriers`, so it cannot + /// recompute this value from the document's other fields — the document + /// author is responsible for updating it when hand-editing barrier bytes + /// that change what tags they name (pin 11's ruled design (a)). + pub manifest_schema_version: SchemaVersion, /// Optional shared-ancestor identity used for document genealogy. pub lineage_id: Option, /// Profile declarations in canonical manifest order. diff --git a/crates/epiphany-textproj/src/parse.rs b/crates/epiphany-textproj/src/parse.rs index 3b479ab..d415439 100644 --- a/crates/epiphany-textproj/src/parse.rs +++ b/crates/epiphany-textproj/src/parse.rs @@ -96,11 +96,11 @@ pub fn parse_document(text: &str) -> Result { )?; // document: mandatory, immediately after the header. - let document_id = require_line( + let (document_id, manifest_schema_version) = require_line( &mut lines, "document", "a projection must carry a document line immediately after its header", - parse_document_id_line, + parse_document_line, )?; // lineage? @@ -176,6 +176,7 @@ pub fn parse_document(text: &str) -> Result { Ok(TextDocument { document_id, + manifest_schema_version, lineage_id, profiles, extensions, @@ -404,13 +405,18 @@ fn parse_header(s: &Sexp) -> Result<(), TextError> { Ok(()) } -/// `document ::= "(document " bytes ")"`. -fn parse_document_id_line(s: &Sexp) -> Result { - let fields = s.expect_struct("document", 1)?; - Ok(DocumentId(parse_id16( - &fields[0], - "a DocumentId is exactly 16 bytes", - )?)) +/// `document ::= "(document " bytes " " schema ")"`. +/// +/// The `schema` field is the carried manifest `SchemaVersion` (G-minor, +/// `spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4, pins 8/11; companion 0.10.0): +/// parsed back verbatim, never re-derived — this companion has no +/// `epiphany-layout-ir` dependency and cannot decode edit-barrier bytes to +/// check it. +fn parse_document_line(s: &Sexp) -> Result<(DocumentId, SchemaVersion), TextError> { + let fields = s.expect_struct("document", 2)?; + let document_id = DocumentId(parse_id16(&fields[0], "a DocumentId is exactly 16 bytes")?); + let manifest_schema_version = parse_schema_version(&fields[1])?; + Ok((document_id, manifest_schema_version)) } /// `lineage ::= "(lineage " bytes ")"`. @@ -643,12 +649,12 @@ mod tests { } // Bumped with `COMPANION_VERSION` (0.7.0 → 0.8.0, genesis G1; 0.8.0 → - // 0.9.0, genesis G2a). 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 9 0))"; - const DOCUMENT: &str = "(document #x00000000000000000000000000000001)"; + // 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))"; + const DOCUMENT: &str = "(document #x00000000000000000000000000000001 (schema 0 1))"; /// A minimal but complete valid projection: just the two mandatory lines. fn minimal_valid_document() -> String { @@ -852,10 +858,10 @@ mod tests { assert!(parse_document(&minimal_valid_document()).is_ok()); } - /// (s9) Genesis tranche G2a (`spec/CONTRACT_GENESIS_G2A_SETTINGS.md`): - /// `(0 8 0)` — the version this crate implemented *before* this - /// packet's `kind` grammar extension — must now be rejected, not merely - /// "some other version". This is the specific case + /// G-minor (`spec/PLAN_GMINOR_SCHEMA_MINOR.md`, s14): `(0 9 0)` — the + /// version this crate implemented *before* this packet's `document` + /// grammar extension — must now be rejected, not merely "some other + /// version". This is the specific case /// `req:textproj:header-version`'s reject-all-others clause exists to /// guard, and it is the one a lenient "any `(0 x 0)`" parser would still /// pass. @@ -865,7 +871,7 @@ mod tests { /// test dies, since `(0 8 0)` would then parse. #[test] fn the_immediately_superseded_companion_version_is_rejected() { - let text = projection(&["(text-projection (0 8 0))", DOCUMENT]); + let text = projection(&["(text-projection (0 9 0))", DOCUMENT]); assert_eq!( parse_document(&text), Err(TextError::NotCanonical( @@ -874,6 +880,17 @@ mod tests { ); } + /// s14: the committed companion version parses, and the immediately + /// superseded version is rejected — restated explicitly under its + /// contract label (`spec/CONTRACT_GMINOR_IMPLEMENTATION.md`), on top of + /// the two tests immediately above which already exercise both halves. + #[test] + fn s14_the_committed_corpus_parses_at_0_10_0_and_0_9_0_is_rejected() { + assert!(parse_document(&minimal_valid_document()).is_ok()); + let superseded = projection(&["(text-projection (0 9 0))", DOCUMENT]); + assert!(parse_document(&superseded).is_err()); + } + #[test] fn a_blob_line_is_always_rejected() { let blob_line = "(blob \"audio/wav\" () #x00)"; diff --git a/crates/epiphany-textproj/src/project.rs b/crates/epiphany-textproj/src/project.rs index be1d64f..ce41a42 100644 --- a/crates/epiphany-textproj/src/project.rs +++ b/crates/epiphany-textproj/src/project.rs @@ -171,11 +171,15 @@ pub fn project_header() -> Sexp { ]) } -/// `document ::= "(document " bytes ")" LF`. -pub fn project_document(document_id: &DocumentId) -> Sexp { +/// `document ::= "(document " bytes " " schema ")" LF`. +/// +/// The `schema` field is the carried manifest `SchemaVersion` (G-minor, +/// `spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4, pins 8/11; companion 0.10.0). +pub fn project_document(document_id: &DocumentId, manifest_schema_version: &SchemaVersion) -> Sexp { Sexp::List(vec![ Sexp::sym("document"), Sexp::Bytes(document_id.as_bytes().to_vec()), + project_schema(manifest_schema_version), ]) } @@ -491,6 +495,9 @@ pub fn document_from_bundle( Ok(TextDocument { document_id: manifest.document_id, + // Carried from the superblock, never derived (pin 8/11): `Manifest` + // itself has no schema-version field (it lives in the superblock). + manifest_schema_version: bundle.superblock().manifest_schema_version, lineage_id: manifest.lineage_id, // `Manifest::decode`'s own re-encode check (see `epiphany-bundle`) // guarantees a bundle's `profile_declarations` are already the @@ -518,7 +525,10 @@ pub fn document_from_bundle( pub fn project_text_document(document: &TextDocument) -> String { let mut lines: Vec = Vec::new(); lines.push(project_header()); - lines.push(project_document(&document.document_id)); + lines.push(project_document( + &document.document_id, + &document.manifest_schema_version, + )); if let Some(lineage_id) = &document.lineage_id { lines.push(project_lineage(lineage_id)); } @@ -587,8 +597,8 @@ mod tests { fn document_id_matches_the_worked_example() { let id = DocumentId([0x05; 16]); assert_eq!( - project_document(&id).render(), - "(document #x05050505050505050505050505050505)" + project_document(&id, &SchemaVersion::V0).render(), + "(document #x05050505050505050505050505050505 (schema 0 1))" ); } @@ -1095,6 +1105,7 @@ mod tests { let rich = document_from_bundle(&build_sample_bundle()).expect("bundle reads cleanly"); let minimal = TextDocument { document_id: DocumentId([6; 16]), + manifest_schema_version: SchemaVersion::V0, lineage_id: None, profiles: vec![ProfileDeclaration::full()], extensions: Vec::new(), diff --git a/crates/epiphany-textproj/src/serialize.rs b/crates/epiphany-textproj/src/serialize.rs index f34b391..d9162f2 100644 --- a/crates/epiphany-textproj/src/serialize.rs +++ b/crates/epiphany-textproj/src/serialize.rs @@ -57,7 +57,7 @@ use epiphany_bundle::{ FileUuid, Manifest, SchemaVersion, SnapshotRef, StagedChunk, }; use epiphany_determinism::CanonicalEncode; -use epiphany_ops::OperationEnvelope; +use epiphany_ops::{operation_block_introduced_minor, OperationEnvelope}; use crate::TextDocument; @@ -150,7 +150,12 @@ pub fn serialize_document( } staged.push(stage_operation_envelope_block(document)); - bundle.commit(&staged, |ctx| build_manifest(document, ctx))?; + // The carried manifest SchemaVersion is supplied explicitly (G-minor pin + // 6.1/8/11): this companion never derives it, it round-trips exactly + // what the document declares. + bundle.commit_versioned(&staged, document.manifest_schema_version, |ctx| { + build_manifest(document, ctx) + })?; Ok(bundle) } @@ -186,7 +191,11 @@ fn stage_operation_envelope_block(document: &TextDocument) -> StagedChunk { .map(OperationEnvelope::schema_major) .max() .unwrap_or(0); - StagedChunk::operation_block_versioned(encode_block(&payloads), SchemaVersion::for_major(major)) + let epoch_max = operation_block_introduced_minor(&document.envelopes); + StagedChunk::operation_block_versioned( + encode_block(&payloads), + SchemaVersion::for_major_at_epoch(major, epoch_max), + ) } /// Builds the committed manifest from the previous (empty) manifest and the @@ -279,6 +288,7 @@ mod tests { fn minimal_document(seed: u64) -> TextDocument { TextDocument { document_id: DocumentId([seed as u8; 16]), + manifest_schema_version: SchemaVersion::V0, lineage_id: None, profiles: vec![base_profile()], extensions: Vec::new(), @@ -341,6 +351,20 @@ mod tests { Bundle::open(MemStore::from_bytes(image)).expect("the serialized bundle reopens") } + #[test] + fn s13_text_document_round_trips_the_carried_manifest_schema_version() { + let mut document = minimal_document(42); + document.manifest_schema_version = SchemaVersion::new(0, 8); + let reopened = serialize_and_reopen(&document); + let bundle_document = + crate::project::document_from_bundle(&reopened).expect("bundle reads cleanly"); + assert_eq!( + bundle_document.manifest_schema_version, + SchemaVersion::new(0, 8), + "the carried manifest SchemaVersion must round-trip exactly, never the baseline" + ); + } + #[test] fn document_identity_round_trips() { let mut document = minimal_document(1); @@ -470,11 +494,12 @@ mod tests { expected_major > 0, "fixture must include a schema-major-bearing operation to exercise the derivation" ); + let expected_epoch_max = operation_block_introduced_minor(&document.envelopes); let reopened = serialize_and_reopen(&document); let root = reopened.manifest().operation_roots[0]; assert_eq!( root.schema_version, - SchemaVersion::for_major(expected_major) + SchemaVersion::for_major_at_epoch(expected_major, expected_epoch_max) ); } diff --git a/crates/epiphany-textproj/src/vectors.rs b/crates/epiphany-textproj/src/vectors.rs index e708326..61dfdf1 100644 --- a/crates/epiphany-textproj/src/vectors.rs +++ b/crates/epiphany-textproj/src/vectors.rs @@ -202,6 +202,7 @@ fn base(snapshot_byte: u8) -> TextCanonicalBase { fn accept_documents() -> Vec<(&'static str, String)> { let minimal = TextDocument { document_id: DocumentId([1; 16]), + manifest_schema_version: SchemaVersion::V0, lineage_id: None, profiles: profiles(false), extensions: Vec::new(), @@ -211,6 +212,7 @@ fn accept_documents() -> Vec<(&'static str, String)> { }; let lineage_custom = TextDocument { document_id: DocumentId([2; 16]), + manifest_schema_version: SchemaVersion::V0, lineage_id: Some(LineageId([0x12; 16])), profiles: profiles(true), extensions: Vec::new(), @@ -220,6 +222,10 @@ fn accept_documents() -> Vec<(&'static str, String)> { }; let extension_base_multi = TextDocument { document_id: DocumentId([3; 16]), + // A non-baseline carried version, so the corpus exercises the + // `document` line's schema field at a value other than the + // ubiquitous baseline (G-minor pin 8/11). + manifest_schema_version: SchemaVersion::new(0, 8), lineage_id: None, profiles: profiles(false), extensions: vec![extension(1, &[1, 2])], @@ -229,6 +235,7 @@ fn accept_documents() -> Vec<(&'static str, String)> { }; let rich = TextDocument { document_id: DocumentId([4; 16]), + manifest_schema_version: SchemaVersion::V0, lineage_id: Some(LineageId([0x14; 16])), profiles: profiles(true), extensions: vec![extension(1, &[1, 2]), extension(2, &[3])], @@ -322,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. Genesis - // G2a moved `COMPANION_VERSION` to 0.9.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.8.0, the - // immediately superseded companion, which is the better test anyway: - // rejecting the version right behind you is exactly the deferred + // 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 + // 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 + // 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 10 0))", "(text-projection (0 9 0))", - "(text-projection (0 8 0))", ); vectors.push(( SURFACE, diff --git a/spec/binary_format.pdf b/spec/binary_format.pdf index b760e28..52d675f 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 303ba75..45eb98b 100644 --- a/spec/binary_format.tex +++ b/spec/binary_format.tex @@ -240,7 +240,7 @@ {\Large\scshape\color{epiphanyslate}Binary Format}\\[6pt] {\large\itshape\color{epiphanyslate}A companion to the Core Specification}\\[14pt] {\color{epiphanygold}\rule{3in}{0.8pt}}\\[24pt] - {\normalsize\color{epiphanyink}Version 0.12.0 --- The genesis operation tranche reaches the wire (G1 + G2a)}\\[4pt] + {\normalsize\color{epiphanyink}Version 0.13.0 --- The chunk schema minor becomes a derived record (G-minor)}\\[4pt] {\small\color{epiphanyslate}Normative for the byte layouts it defines} \vfill \end{titlepage} @@ -2370,24 +2370,54 @@ 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{30}$ + \item \texttt{OperationKind}: append at ${\geq}\,\tablenums{34}$ (Requirement~\ref{req:binfmt:kind-discriminants}; the Phase-3 tranche took \tablenums{24}--\tablenums{27} under this mechanism, - and the repeat pair \tablenums{28}/\tablenums{29} in the - schema-major-2 revision --- 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{30}$ - (Requirement~\ref{req:binfmt:kind-tag}); + 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 + \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); \item \texttt{OperationPayload}: append at ${\geq}\,\tablenums{4}$ - (Section~\ref{sec:ops:payload}); + (Section~\ref{sec:ops:payload}; \texttt{ResolveEquivocation} = 3, Push~3, + is the one and only append this vocabulary has had); \item the value-layer unions enumerated as closed in Requirement~\ref{req:binfmt:frozen-layout} may gain appended variants only through a ratified revision of this document, also minor-additive. \end{itemize} +\paragraph{The schema-minor derivation (G-minor).} Every appended +discriminant above is annotated with a \emph{global additive epoch}: one +schema-minor value shared across every vocabulary, assigned in the order the +revision that introduced it landed +(\texttt{spec/PLAN\_GMINOR\_SCHEMA\_MINOR.md}~\S4). An envelope's required +minor is the maximum epoch over every discriminant it actually emits --- its +outer \texttt{OperationPayload} variant, its primitive \texttt{OperationKind} +when \texttt{Primitive}, and any nested additive variant its encoder reaches; +a block's is the maximum over its envelopes. The emitted version is +$\{\text{major}, \max(\text{baseline\_minor}(\text{major}), +\text{epoch\_max})\}$: existing baselines (\tablenums{1} for major~0, +\tablenums{0} for majors~1--3) are never lowered, and a block emitting only +baseline vocabulary keeps its major's baseline minor rather than being +normalized to zero. This is content-minimal per-role stamping, not a single +document-wide counter: the canonical base, the manifest, and each operation +block are each stamped from only the discriminants their own bytes emit +(Section~\ref{sec:evolution:additive}'s per-chunk framing extends +unchanged). The manifest reaches \texttt{OperationKindTag} independently of +any operation envelope, through +\texttt{ExtensionDeclaration.edit\_barriers} $\to$ +\texttt{EditBarrier.prohibited\_operation\_kinds}: a manifest naming a +barrier on a post-baseline tag takes that tag's epoch, and the manifest +major stays~0 regardless. + \texttt{ChunkKind} is \textbf{closed} --- it has no \texttt{Registered} variant and no append story inside major~0, because its discriminant enters every chunk's hash preimage; a new chunk kind is a format-major event. @@ -3541,6 +3571,21 @@ only}: implementations need not agree on an error taxonomy. Catalog \sectionsc{CreateInstrument} (retroactive), \sectionsc{SetCanvasLayoutDefaults}, \sectionsc{SetSpellingPrecedence}, 0.10.0. \\ + \today & Schema Evolution & 0.13.0 --- G-minor + (\texttt{spec/CONTRACT\_GMINOR\_IMPLEMENTATION.md}, + \texttt{spec/PLAN\_GMINOR\_SCHEMA\_MINOR.md}): implements the minor + \MUST{} of Section~\ref{sec:evolution:gate} for the first time + (\texttt{binary\_format.tex:2330}, unchanged in substance, wired to a real + writer-side derivation for the first time). Adds the global additive-epoch + derivation (this chapter, \sectionsc{What ``Additive'' Means Here}) and + repairs the stale \tablenums{30} next-free-slot narrative this section + previously carried, which omitted \tablenums{30}--\tablenums{33} + (\texttt{TransposeInterval}, \texttt{CreateInstrument}, + \texttt{SetCanvasLayoutDefaults}, \texttt{SetSpellingPrecedence}) from both + the number and the history; the normative kind and tag tables were already + current. No discriminant, wire layout, or accept-set changes: this is a + correction to prose and a new writer-side derivation, not a new appended + variant. \\ \bottomrule \end{longtable} diff --git a/spec/text_projection.pdf b/spec/text_projection.pdf index 9a338a8..380ef35 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 a98d436..c71dd5f 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.9.0 --- The genesis settings setters reach the grammar}\\[4pt] + {\normalsize\color{epiphanyink}Version 0.10.0 --- The carried manifest schema version reaches the document line}\\[4pt] {\small\color{epiphanyslate}Normative for the text form it defines} \vfill \end{titlepage} @@ -454,8 +454,10 @@ A projection is, in order: \begin{enumerate} \item a \texttt{(text-projection )} header line, naming the version of \emph{this companion} the text conforms to; - \item a \texttt{(document \#x)} line, and a - \texttt{(lineage \#x)} line if the manifest declares one; + \item a \texttt{(document \#x (schema ))} line --- + the manifest's carried \texttt{SchemaVersion} is projected alongside the + document id, verbatim and never re-derived (Requirement~\ref{req:textproj:manifest-schema-carried}) --- + and a \texttt{(lineage \#x)} line if the manifest declares one; \item zero or more \texttt{(profile ...)} lines, in canonical order; \item zero or more \texttt{(extension ...)} lines, in canonical order; \item at most one \texttt{(canonical-base ...)} line; @@ -464,10 +466,24 @@ A projection is, in order: (core specification Appendix~D). \end{enumerate} +\begin{requirement} + \label{req:textproj:manifest-schema-carried} + The \texttt{document} line \MUST{} carry the manifest's aggregate + \texttt{SchemaVersion} (core specification Chapter~8, + \sectionsc{Schema Versioning}) as its second field. A binary-to-text + projector \MUST{} copy this value verbatim from the bundle superblock, and + a text-to-binary serializer \MUST{} use the value declared by the document + line verbatim as the bundle's manifest schema version. This companion + \MUSTNOT{} derive or validate the value from + \texttt{ExtensionDeclaration.edit\_barriers}; those bytes are opaque at + this layer. A document author editing opaque barrier bytes is responsible + for updating the field to match. +\end{requirement} + \begin{requirement} \label{req:textproj:header-version} A parser implementing this companion \MUST{} accept exactly one header - version: \texttt{(0 9 0)}, the version of the companion it implements. It + version: \texttt{(0 10 0)}, the version of the companion it implements. It \MUST{} reject any other version at line one. Multi-version acceptance and text migrate-on-read are deferred in the same @@ -518,7 +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.9.0, neither a canonical operation nor canonical reduced state can + version~0.10.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} @@ -960,7 +976,8 @@ projection ::= header document lineage? profile* extension* header ::= "(text-projection " version ")" LF version ::= "(" integer " " integer " " integer ")" -document ::= "(document " bytes ")" LF +document ::= "(document " bytes " " schema ")" LF + ; id, then the carried manifest SchemaVersion (G-minor) lineage ::= "(lineage " bytes ")" LF profile ::= "(profile " profile-id " " version " " constraints ")" LF @@ -1120,8 +1137,8 @@ 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 9 0)) -(document #x05050505050505050505050505050505) +(text-projection (0 10 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) (envelope #x00000000000000070000000000000001 #x00000000000000000000000011223344 (stamp 42 7 #x00000000000000070000000000000001) (causal ((#x0000000000000001 3)) (#x00000000000000020000000000000009)) (some #x00000000000000070000000000000005) (primitive (transpose-interval (#x00000000000000070000000000000001 #x00000000000000070000000000000002) (transposition-interval 4 7)))) @@ -1338,6 +1355,22 @@ absorb it, exactly as the binary decoder does. extending the grammar would leave two mutually incompatible grammars both claiming \texttt{(0 8 0)}. Cached projections at \texttt{(0 8 0)} do not migrate; a stale \texttt{TextProjection} chunk is regenerated, not + converted. \\ + \today & Chapters 8, 5 & 0.10.0 --- The carried manifest schema version + reaches the \texttt{document} line (the G-minor rung, + \texttt{spec/PLAN\_GMINOR\_SCHEMA\_MINOR.md}). The \texttt{document} + production gains a second field: the manifest's aggregate + \texttt{SchemaVersion}, carried verbatim and never derived + (\texttt{req:textproj:manifest-schema-carried}). + + This bump is caused by the manifest attribute alone, \emph{not} by + operation-block schema-minor stamping: an operation block's physical schema + is discarded during projection exactly as before, and a reader must not + infer that op-block minors reach the text surface from this entry. The reasoning is the + same forcing argument as every prior grammar-extending bump: holding the + version while adding a field would leave two mutually incompatible grammars + 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. \\ \bottomrule \end{longtable} diff --git a/spec/vectors/textproj_document_vectors.txt b/spec/vectors/textproj_document_vectors.txt index 0da573a..ee894da 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 28746578742d70726f6a656374696f6e2028302039203029290a28646f63756d656e742023783031303130313031303130313031303130313031303130313031303130313031290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a -textproj.document accept - lineage_custom_profile 28746578742d70726f6a656374696f6e2028302039203029290a28646f63756d656e742023783032303230323032303230323032303230323032303230323032303230323032290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a -textproj.document accept - extension_base_two_envelopes 28746578742d70726f6a656374696f6e2028302039203029290a28646f63756d656e742023783033303330333033303330333033303330333033303330333033303330333033290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a -textproj.document accept - rich_document 28746578742d70726f6a656374696f6e2028302039203029290a28646f63756d656e742023783034303430343034303430343034303430343034303430343034303430343034290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a -textproj.document reject wrong-header-version superseded_companion_version 28746578742d70726f6a656374696f6e2028302038203029290a28646f63756d656e742023783031303130313031303130313031303130313031303130313031303130313031290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a -textproj.document reject blob-line unreferenced_blob 28746578742d70726f6a656374696f6e2028302039203029290a28646f63756d656e742023783031303130313031303130313031303130313031303130313031303130313031290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28626c6f6220226170706c69636174696f6e2f6f637465742d73747265616d222028292023783031290a -textproj.document reject out-of-order-sections canonical_base_before_extension 28746578742d70726f6a656374696f6e2028302039203029290a28646f63756d656e742023783033303330333033303330333033303330333033303330333033303330333033290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a -textproj.document reject repeated-singular-section lineage_repeated 28746578742d70726f6a656374696f6e2028302039203029290a28646f63756d656e742023783032303230323032303230323032303230323032303230323032303230323032290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a -textproj.document reject operation-envelope-order envelopes_reversed 28746578742d70726f6a656374696f6e2028302039203029290a28646f63756d656e742023783033303330333033303330333033303330333033303330333033303330333033290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a -textproj.document reject profile-declaration-order profiles_reversed 28746578742d70726f6a656374696f6e2028302039203029290a28646f63756d656e742023783032303230323032303230323032303230323032303230323032303230323032290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a -textproj.document reject extension-declaration-order extensions_reversed 28746578742d70726f6a656374696f6e2028302039203029290a28646f63756d656e742023783034303430343034303430343034303430343034303430343034303430343034290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a -textproj.document reject extension-chunk-order extension_chunks_reversed 28746578742d70726f6a656374696f6e2028302039203029290a28646f63756d656e742023783033303330333033303330333033303330333033303330333033303330333033290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a -textproj.document reject missing-trailing-lf final_lf_missing 28746578742d70726f6a656374696f6e2028302039203029290a28646f63756d656e742023783031303130313031303130313031303130313031303130313031303130313031290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e20312028292074727565292929 +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