diff --git a/crates/epiphany-bundle/src/bundle.rs b/crates/epiphany-bundle/src/bundle.rs index 37fe0c6..6d24c95 100644 --- a/crates/epiphany-bundle/src/bundle.rs +++ b/crates/epiphany-bundle/src/bundle.rs @@ -18,7 +18,7 @@ use crate::chunk::{ }; use crate::codec::DecodeError; use crate::error::{BundleError, IntegrityAnomaly}; -use crate::header::{FixedHeader, SLOT_A_OFFSET, SLOT_B_OFFSET}; +use crate::header::{FixedHeader, FormatEpoch, SLOT_A_OFFSET, SLOT_B_OFFSET}; use crate::ids::{BlobId, FileUuid, ReductionAlgorithmVersion, SchemaVersion, WallClockTime}; use crate::manifest::{BlobRef, Manifest, ProfileDeclaration}; use crate::opindex::OperationIndex; @@ -408,6 +408,19 @@ impl Bundle { "canonical base profile is not declared by the manifest", ))); } + // Format-epoch matrix (`CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3, + // rows 2 and 5i). Corruption precedence runs first (both checks + // above): only once the base is structurally sound do we ask + // whether this epoch may open it at all. A legacy (major-0) + // container can never open with a base — the epoch is not + // retroactive (row 2, permanent). A major-1 container is the + // right epoch, but until P13-S27 lands there is no + // reduction-authority capability to validate it against (row 5i, + // interim — TEMPORARY, removed by P13-S27, pin 3a). + return Err(match header.epoch { + FormatEpoch::Legacy => BundleError::LegacyBundleHasCanonicalBase, + FormatEpoch::Current => BundleError::ReductionAuthorityUnavailable, + }); } // An unknown *required* extension forces read-only (Chapter 8 §"Behavior // Under Unknown Extensions"). @@ -761,6 +774,28 @@ impl Bundle { // least one declared profile. let active_profile = validate_emittable_manifest(&manifest)?; + // Format-epoch matrix (`CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3, rows 3 + // and 6i): a commit may never introduce (or replace) a canonical + // base. `self.manifest.canonical_base` is always `None` here — `open` + // and `create` both already refuse ever producing a live bundle whose + // base is `Some`, so any `Some` reaching this point is necessarily a + // fresh introduction. Row 3 — a legacy (major-0) container can never + // become base-bearing in place, because its header can never say the + // base was validated (permanent; the epoch is not inheritable). Row + // 6i — a major-1 container is the right epoch, but until P13-S27 + // lands there is no reduction-authority capability to validate a + // newly introduced base against (interim — TEMPORARY, removed by + // P13-S27, pin 3a). Placed after `validate_emittable_manifest` (which + // already rejects a base whose profile is undeclared) so that a + // structurally malformed base is still reported as malformed, not + // masked by this categorical epoch refusal. + if manifest.canonical_base.is_some() { + return Err(match self.header.epoch { + FormatEpoch::Legacy => BundleError::LegacyBaseIntroductionRejected, + FormatEpoch::Current => BundleError::ReductionAuthorityUnavailable, + }); + } + // Before publishing, validate that every canonical root the new manifest // declares actually resolves to a present, hash-intact chunk of the right // kind and shape (the new chunks are written and flushed; retained roots @@ -1489,49 +1524,51 @@ mod tests { // Major 2: the MaterializedState embeds no data-model-major values, // and re-stamping byte-identical content churns its address). With // the Snapshot KIND now admitting the major-2 acceleration form, the - // per-role check must catch a mis-stamped base: read-only + anomaly, - // at commit and again on reopen. - let mut bundle = fresh_bundle(); - let base = StagedChunk { + // per-role check (`mis_stamped_canonical_base`) must catch a + // mis-stamped base. + // + // CONTRACT_FORMAT_EPOCH_MAJOR1.md pin 3a (the "named trap"): this test + // used to commit the mis-stamped base directly and assert read-only + // immediately, both at commit and at reopen. Neither half is + // reachable any more — pin 3a's epoch guard (rows 2/3/5i/6i, both + // exercised above in this module) now refuses *any* canonical base, + // mis-stamped or not, before `mis_stamped_canonical_base` is ever + // consulted, at both `open` and `commit`. That guard is a different + // axis (container format major) from this one (data-model schema + // major) and does not contradict pin 4 — `ReductionAuthorityUnavailable` + // must not degrade to read-only — so the fix is not to weaken pin 4 + // toward this test. It is to test what remains reachable: + // `mis_stamped_canonical_base` itself, a pure function of a + // `Manifest`, independent of the now-categorical bundle-lifecycle + // refusal that sits in front of it. + let mut m = Manifest::empty(DocumentId([9; 16])); + let payload = vec![7u8, 7, 7]; + let hash = chunk_content_hash(ChunkKind::Snapshot, SchemaVersion::V1, &payload); + let root = ChunkRef { + id: ChunkId(hash), kind: ChunkKind::Snapshot, schema_version: SchemaVersion::V1, - payload: vec![7u8, 7, 7], + offset: BODY_START, + compressed_length: payload.len() as u64, + uncompressed_length: payload.len() as u64, + compression: CompressionAlgorithm::None, + hash, }; - bundle - .commit(&[base], |ctx| { - let mut m = ctx.previous_manifest.clone(); - let root = ctx.new_chunks[0]; - let mut sid = [0u8; 16]; - sid.copy_from_slice(&root.hash.as_bytes()[..16]); - m.canonical_base = Some(SnapshotRef { - snapshot_id: SnapshotId(sid), - covers_causal_frontier: FrontierBytes::from_bytes(vec![]), - reduction_algorithm_version: ReductionAlgorithmVersion(0), - profile_id: ProfileId::Full, - hash: root.hash, - root, - }); - m - }) - .expect("a structurally-valid mis-stamped base is publishable"); - assert!( - bundle.is_read_only(), - "a mis-stamped canonical base forces read-only at commit" + m.canonical_base = Some(SnapshotRef { + snapshot_id: SnapshotId([1; 16]), + covers_causal_frontier: FrontierBytes::empty(), + reduction_algorithm_version: ReductionAlgorithmVersion(0), + profile_id: ProfileId::Full, + hash: root.hash, + root, + }); + assert_eq!( + mis_stamped_canonical_base(&m), + Some(1), + "a canonical base root stamped above major 0 is still flagged by the per-role \ + check, even though pin 3a's epoch guard now intercepts every base before this \ + check ever runs" ); - assert!(bundle.anomalies().iter().any(|a| matches!( - a, - IntegrityAnomaly::UnsupportedCanonicalChunkMajor { schema_major: 1 } - ))); - let image = bundle.into_store().into_bytes(); - let reopened = Bundle::open(MemStore::from_bytes(image)).expect("reopen"); - assert!( - reopened.is_read_only(), - "a mis-stamped canonical base forces read-only at open" - ); - assert!(reopened.anomalies().iter().any(|a| matches!( - a, - IntegrityAnomaly::UnsupportedCanonicalChunkMajor { schema_major: 1 } - ))); } fn fresh_bundle() -> Bundle { @@ -1543,6 +1580,307 @@ mod tests { .unwrap() } + // ----------------------------------------------------------------- + // CONTRACT_FORMAT_EPOCH_MAJOR1.md: format-epoch matrix (pins 1-4). + // + // After this rung no `create`/`commit` path may ever produce a + // base-bearing bundle (pin 3c): `create` already refused one, and rows + // 3/6i below now refuse committing one into either epoch, so a + // base-bearing fixture can only be built as a hand-crafted image, the + // same technique `craft_image`/`craft_image_with_manifest_bytes` already + // use further down this file. + // ----------------------------------------------------------------- + + /// A fixed header at `format_major`, bypassing `FixedHeader::new` (which + /// always stamps the *current* epoch) so a legacy (major-0) fixture can + /// be built too. Same technique as `craft_image` below: public fields, + /// public `encode()`. + fn header_for_major(format_major: u16, file_uuid: FileUuid) -> FixedHeader { + let epoch = if format_major == 0 { + FormatEpoch::Legacy + } else { + FormatEpoch::Current + }; + FixedHeader { + format_major, + format_minor: 0, + header_length: crate::header::HEADER_LEN as u32, + superblock_a_offset: SLOT_A_OFFSET, + superblock_b_offset: SLOT_B_OFFSET, + file_uuid, + epoch, + } + } + + /// A minimal valid bundle image at `format_major`, with no canonical base + /// (matrix rows 1 and 4). + fn craft_image_epoch(format_major: u16) -> Vec { + let mut m = Manifest::empty(DocumentId([1; 16])); + m.profile_declarations = vec![ProfileDeclaration::full()]; + let payload = m.encode(); + let mut image = vec![0u8; BODY_START as usize]; + image.extend_from_slice(&payload); + let sb = Superblock { + generation: 0, + manifest_offset: BODY_START, + manifest_length: payload.len() as u64, + manifest_hash: manifest_chunk_hash(&payload), + manifest_schema_version: SchemaVersion::V0, + reduction_algorithm_version: ReductionAlgorithmVersion(0), + profile_id: ProfileId::Full, + commit_state: CommitState::Committed, + commit_timestamp: WallClockTime(0), + }; + image[0..crate::header::HEADER_LEN as usize] + .copy_from_slice(&header_for_major(format_major, FileUuid([1; 16])).encode()); + image[SLOT_A_OFFSET as usize..SLOT_A_OFFSET as usize + SUPERBLOCK_LEN as usize] + .copy_from_slice(&sb.encode()); + image + } + + /// A bundle image at `format_major` carrying a canonical base wired + /// directly into the manifest and superblock bytes, bypassing + /// `create`/`commit` entirely — the only way left to build this fixture + /// (pin 3c). `base_schema_major` mis-stamps the base's own root + /// independent of the container's format major; `base_version` and + /// `superblock_version` are equal for a self-consistent base (rows + /// 2/5i/11) and unequal for the corrupt-base precedence fixture (test 7). + fn craft_image_with_base( + format_major: u16, + base_schema_major: u16, + base_version: ReductionAlgorithmVersion, + superblock_version: ReductionAlgorithmVersion, + ) -> Vec { + let base_schema = SchemaVersion::new(base_schema_major, 0); + let snap_payload = vec![7u8, 7, 7]; + let snap_hash = chunk_content_hash(ChunkKind::Snapshot, base_schema, &snap_payload); + let root = ChunkRef { + id: ChunkId(snap_hash), + kind: ChunkKind::Snapshot, + schema_version: base_schema, + offset: BODY_START, + compressed_length: snap_payload.len() as u64, + uncompressed_length: snap_payload.len() as u64, + compression: CompressionAlgorithm::None, + hash: snap_hash, + }; + + let mut m = Manifest::empty(DocumentId([1; 16])); + m.profile_declarations = vec![ProfileDeclaration::full()]; + m.canonical_base = Some(SnapshotRef { + snapshot_id: SnapshotId([1; 16]), + covers_causal_frontier: FrontierBytes::empty(), + reduction_algorithm_version: base_version, + profile_id: ProfileId::Full, + hash: root.hash, + root, + }); + + let manifest_payload = m.encode(); + let manifest_offset = BODY_START + snap_payload.len() as u64; + + let mut image = vec![0u8; BODY_START as usize]; + image.extend_from_slice(&snap_payload); + image.extend_from_slice(&manifest_payload); + + let sb = Superblock { + generation: 0, + manifest_offset, + manifest_length: manifest_payload.len() as u64, + manifest_hash: manifest_chunk_hash(&manifest_payload), + manifest_schema_version: SchemaVersion::V0, + reduction_algorithm_version: superblock_version, + profile_id: ProfileId::Full, + commit_state: CommitState::Committed, + commit_timestamp: WallClockTime(0), + }; + image[0..crate::header::HEADER_LEN as usize] + .copy_from_slice(&header_for_major(format_major, FileUuid([1; 16])).encode()); + image[SLOT_A_OFFSET as usize..SLOT_A_OFFSET as usize + SUPERBLOCK_LEN as usize] + .copy_from_slice(&sb.encode()); + image + } + + /// `Bundle` has no `Debug` impl, so `Result::expect_err`/`unwrap_err` + /// cannot be used directly on `Bundle::open`'s result; this extracts the + /// error by hand. + fn open_err(image: Vec, context: &str) -> BundleError { + match Bundle::open(MemStore::from_bytes(image)) { + Err(e) => e, + Ok(_) => panic!("{context}"), + } + } + + #[test] + fn a_legacy_major_0_bundle_without_a_base_opens() { + // Matrix row 1: a legacy container with no base opens cleanly — + // nothing unverifiable is exposed. + let image = craft_image_epoch(0); + let bundle = Bundle::open(MemStore::from_bytes(image)) + .expect("a legacy bundle without a base opens"); + assert!(!bundle.is_read_only()); + assert!(bundle.anomalies().is_empty()); + assert_eq!(bundle.header().epoch, FormatEpoch::Legacy); + } + + #[test] + fn a_legacy_major_0_bundle_with_a_base_is_rejected() { + // Matrix row 2: a legacy container already carrying a + // (self-consistent) base is a hard reject at open — a pre-epoch base + // was never validated against a reduction authority. + let image = craft_image_with_base( + 0, + 0, + ReductionAlgorithmVersion(0), + ReductionAlgorithmVersion(0), + ); + let err = open_err(image, "a legacy base-bearing bundle must not open"); + assert!(matches!(err, BundleError::LegacyBundleHasCanonicalBase)); + assert!(!matches!(err, BundleError::LegacyBaseIntroductionRejected)); + assert!(!matches!(err, BundleError::ReductionAuthorityUnavailable)); + } + + #[test] + fn adding_a_base_to_a_legacy_bundle_is_rejected_and_names_repack() { + // Matrix row 3: a legacy container that opened clean under row 1 + // must not become base-bearing in place — the epoch is not + // inheritable, because its header can never say the base was + // validated. + let image = craft_image_epoch(0); + let mut bundle = Bundle::open(MemStore::from_bytes(image)) + .expect("a legacy bundle without a base opens"); + let staged = StagedChunk { + kind: ChunkKind::Snapshot, + schema_version: SchemaVersion::V0, + payload: vec![7u8, 7, 7], + }; + let err = bundle + .commit(&[staged], |ctx| { + let mut m = ctx.previous_manifest.clone(); + let root = ctx.new_chunks[0]; + m.canonical_base = Some(SnapshotRef { + snapshot_id: SnapshotId([1; 16]), + covers_causal_frontier: FrontierBytes::empty(), + reduction_algorithm_version: ReductionAlgorithmVersion(0), + profile_id: ProfileId::Full, + hash: root.hash, + root, + }); + m + }) + .expect_err("committing a base into a legacy bundle must be refused"); + assert!(matches!(err, BundleError::LegacyBaseIntroductionRejected)); + assert!(!matches!(err, BundleError::LegacyBundleHasCanonicalBase)); + assert!(!matches!(err, BundleError::ReductionAuthorityUnavailable)); + assert!( + err.to_string().to_lowercase().contains("repack"), + "row-3 error must name repack: {err}" + ); + assert_eq!( + bundle.generation(), + 0, + "the refused commit did not advance the bundle" + ); + } + + #[test] + fn a_major_1_bundle_round_trips_and_refuses_to_introduce_a_base() { + // Matrix row 4 (round-trip half) + row 6i (refusal half). Renamed + // from "..._validates_its_base", which pin 3a forbids claiming until + // P13-S27 lands: this rung does not validate a base, it refuses one + // outright. + let mut bundle = fresh_bundle(); + assert_eq!(bundle.header().epoch, FormatEpoch::Current); + + // Round trip: ordinary non-base-bearing history commits and reopens. + let block = StagedChunk::operation_block(block::encode_block(&[vec![1u8, 2, 3]])); + bundle + .commit(&[block], |ctx| { + let mut m = ctx.previous_manifest.clone(); + m.operation_roots.push(ctx.new_chunks[0]); + m + }) + .expect("a major-1 bundle commits ordinary non-base-bearing history"); + let image = bundle.into_store().into_bytes(); + let mut reopened = + Bundle::open(MemStore::from_bytes(image)).expect("a major-1 bundle round trips"); + assert!(!reopened.is_read_only()); + assert_eq!(reopened.manifest().operation_roots.len(), 1); + + // Refusal: introducing a base is refused (row 6i), distinctly from + // both legacy errors. + let staged = StagedChunk { + kind: ChunkKind::Snapshot, + schema_version: SchemaVersion::V0, + payload: vec![9u8, 9, 9], + }; + let err = reopened + .commit(&[staged], |ctx| { + let mut m = ctx.previous_manifest.clone(); + let root = ctx.new_chunks[0]; + m.canonical_base = Some(SnapshotRef { + snapshot_id: SnapshotId([2; 16]), + covers_causal_frontier: FrontierBytes::empty(), + reduction_algorithm_version: ReductionAlgorithmVersion(0), + profile_id: ProfileId::Full, + hash: root.hash, + root, + }); + m + }) + .expect_err( + "a major-1 bundle must refuse to introduce a base during the S28 -> P13-S27 interval", + ); + assert!(matches!(err, BundleError::ReductionAuthorityUnavailable)); + assert!(!matches!(err, BundleError::LegacyBundleHasCanonicalBase)); + assert!(!matches!(err, BundleError::LegacyBaseIntroductionRejected)); + } + + #[test] + fn a_corrupt_base_fails_as_malformed_before_any_epoch_error() { + // Pin 3's precedence rule, in BOTH epochs: a base whose version + // disagrees with its superblock's is corrupt and must fail with the + // existing malformed-bundle error — never row 2's legacy error, and + // never row 5i's `ReductionAuthorityUnavailable`. Collapsing + // tampering into staleness would erase the distinction P13-S27's + // authority check rests on. Renamed from `..._corrupt_legacy_base...`: + // the major-1 half is the one an earlier draft missed. + for format_major in [0u16, 1u16] { + let image = craft_image_with_base( + format_major, + 0, + ReductionAlgorithmVersion(1), + ReductionAlgorithmVersion(2), // disagrees with the base's own version + ); + let err = open_err(image, "a corrupt base must not open"); + assert!( + matches!(err, BundleError::Decode(DecodeError::Malformed(_))), + "format_major {format_major}: expected the malformed-bundle error, got {err:?}" + ); + assert!(!matches!(err, BundleError::LegacyBundleHasCanonicalBase)); + assert!(!matches!(err, BundleError::ReductionAuthorityUnavailable)); + } + } + + #[test] + fn opening_a_major_1_bundle_that_already_carries_a_base_is_refused() { + // Matrix row 5i, the read-side branch pin 3a adds — an earlier draft + // omitted it entirely. + let image = craft_image_with_base( + 1, + 0, + ReductionAlgorithmVersion(0), + ReductionAlgorithmVersion(0), + ); + let err = open_err( + image, + "a major-1 bundle already carrying a base must not open during the interval", + ); + assert!(matches!(err, BundleError::ReductionAuthorityUnavailable)); + assert!(!matches!(err, BundleError::LegacyBundleHasCanonicalBase)); + assert!(!matches!(err, BundleError::LegacyBaseIntroductionRejected)); + } + // ----------------------------------------------------------------- // G-minor (`spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4): s10, s11, s12. // ----------------------------------------------------------------- diff --git a/crates/epiphany-bundle/src/error.rs b/crates/epiphany-bundle/src/error.rs index c24d623..7ad54df 100644 --- a/crates/epiphany-bundle/src/error.rs +++ b/crates/epiphany-bundle/src/error.rs @@ -116,6 +116,40 @@ pub enum BundleError { /// extension, or a recovery/anomaly open). Chapter 8 §"Behavior Under /// Unknown Extensions" / §"Superblock Selection". ReadOnly, + + /// A legacy (format major 0) container's manifest already carries a + /// canonical base (`spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md` matrix row 2). + /// A pre-epoch base was never validated against a reduction authority, so + /// it is not safe materialized state — the document already contains + /// unverifiable canonical state. **Permanent**: legacy containers never + /// gain that validation retroactively. The fix is to repack into a fresh + /// major-1 bundle; that flow is not built by this rung (pin 5) and this + /// error MUST NOT degrade to a read-only open — a pre-authority base is + /// not a restricted-but-correct view. + LegacyBundleHasCanonicalBase, + + /// A commit into a legacy (format major 0) container attempted to add or + /// replace the canonical base (matrix row 3). The document itself is + /// fine, but this operation cannot be performed in this container: a + /// bundle that opened clean under row 1 must not become base-bearing in + /// place, because its header can never say it was validated — the epoch + /// is not inheritable. **Permanent**, like + /// [`BundleError::LegacyBundleHasCanonicalBase`]. The fix is to repack + /// into a fresh major-1 bundle; not built by this rung (pin 5), and this + /// error MUST NOT degrade to a read-only open. + LegacyBaseIntroductionRejected, + + /// A major-1 container's canonical base cannot yet be validated: matrix + /// rows 5i/6i, opening a container that already carries a base, or + /// committing one into it. The container is the **right** epoch — this is + /// not a request to repack — but no reduction-authority capability exists + /// yet to validate the base against. **Temporary**: `P13-S27` supplies + /// that capability and replaces both branches with real validation, not + /// this categorical refusal. Never mentions repack (repacking a + /// already-correct-epoch container would be wrong advice), and MUST NOT + /// degrade to a read-only open — a pre-authority base is not a + /// restricted-but-correct view. + ReductionAuthorityUnavailable, } impl core::fmt::Display for BundleError { @@ -187,6 +221,18 @@ impl core::fmt::Display for BundleError { } BundleError::Decode(e) => write!(f, "decode error: {e}"), BundleError::ReadOnly => f.write_str("bundle is open read-only; edits are refused"), + BundleError::LegacyBundleHasCanonicalBase => f.write_str( + "legacy (format major 0) bundle carries a canonical base, which was never \ + validated against a reduction authority; repack into a fresh major-1 bundle", + ), + BundleError::LegacyBaseIntroductionRejected => f.write_str( + "cannot add or replace a canonical base in a legacy (format major 0) bundle; \ + repack into a fresh major-1 bundle", + ), + BundleError::ReductionAuthorityUnavailable => f.write_str( + "this major-1 container's canonical base cannot yet be validated: no \ + reduction-authority capability exists until P13-S27 lands", + ), } } } diff --git a/crates/epiphany-bundle/src/header.rs b/crates/epiphany-bundle/src/header.rs index f7f2a36..ecf401e 100644 --- a/crates/epiphany-bundle/src/header.rs +++ b/crates/epiphany-bundle/src/header.rs @@ -36,10 +36,39 @@ pub const SLOT_A_OFFSET: u64 = 64; pub const SLOT_B_OFFSET: u64 = 320; /// The format major version this crate writes and understands. -pub const FORMAT_MAJOR: u16 = 0; +/// +/// **Format epoch** (`spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md`): a major-1 +/// container is one whose every base-bearing commit was validated against a +/// supplied reduction authority. Major 0 is the pre-epoch **legacy** format — +/// still decoded, deliberately, but never trusted to carry a canonical base +/// (see [`FormatEpoch`]). A new major restarts minor numbering, which is why +/// [`FORMAT_MINOR`] resets to `0` here rather than continuing from legacy's +/// `1`. +pub const FORMAT_MAJOR: u16 = 1; /// The format minor version this crate writes. -pub const FORMAT_MINOR: u16 = 1; +/// +/// Reset to `0` by the major-1 epoch bump (`spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md` +/// pin 1): a new major restarts minor numbering. +pub const FORMAT_MINOR: u16 = 0; + +/// Which format-epoch a decoded header belongs to +/// (`spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 2). A named enum rather than a +/// boolean so the legacy case is a value the type system carries — every +/// consumer of the epoch matches this, rather than re-deriving +/// `format_major == 0` at each use site. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum FormatEpoch { + /// Format major 0: decoded deliberately, for backward compatibility, but + /// never trusted to carry a canonical base (the epoch is not retroactive + /// — `CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3, row 2/3). + Legacy, + /// Format major 1: the current epoch. Every base-bearing commit is meant + /// to be validated against a supplied reduction authority — the + /// capability itself lands with P13-S27; until then this rung refuses + /// base introduction outright (pin 3a). + Current, +} /// Byte range covered by the header CRC: everything before the CRC field. const HEADER_CRC_RANGE: usize = 60; @@ -59,6 +88,9 @@ pub struct FixedHeader { pub superblock_b_offset: u64, /// Physical-bundle UUID, set at creation, changed on Save As. pub file_uuid: FileUuid, + /// The format epoch this header's major classifies to (pin 2). Carried + /// on the header rather than re-derived at each call site. + pub epoch: FormatEpoch, } impl FixedHeader { @@ -71,6 +103,7 @@ impl FixedHeader { superblock_a_offset: SLOT_A_OFFSET, superblock_b_offset: SLOT_B_OFFSET, file_uuid, + epoch: FormatEpoch::Current, } } @@ -116,12 +149,20 @@ impl FixedHeader { let _magic = r.take_array::<8>()?; let format_major = r.get_u16()?; let format_minor = r.get_u16()?; - if format_major != FORMAT_MAJOR { - return Err(BundleError::UnsupportedFormatVersion { - major: format_major, - minor: format_minor, - }); - } + // Pin 2's explicit three-way classification: 0 is legacy (decoded + // deliberately, marked as such), FORMAT_MAJOR is current, anything + // else is unsupported — unchanged from the pre-epoch exact-major + // rejection for that third arm. + let epoch = match format_major { + 0 => FormatEpoch::Legacy, + FORMAT_MAJOR => FormatEpoch::Current, + _ => { + return Err(BundleError::UnsupportedFormatVersion { + major: format_major, + minor: format_minor, + }) + } + }; let header_length = r.get_u32()?; if header_length != HEADER_LEN as u32 { return Err(BundleError::UnsupportedHeaderLength { @@ -147,6 +188,7 @@ impl FixedHeader { superblock_a_offset, superblock_b_offset, file_uuid, + epoch, }) } } @@ -155,6 +197,14 @@ impl FixedHeader { mod tests { use super::*; + #[test] + fn format_epoch_constants_are_major_1_minor_0() { + // Gate 7 (`CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 1): the epoch bump, + // asserted in a test rather than only by reading the constants. + assert_eq!(FORMAT_MAJOR, 1); + assert_eq!(FORMAT_MINOR, 0); + } + #[test] fn header_round_trips() { let h = FixedHeader::new(FileUuid([0xAB; 16])); @@ -192,6 +242,21 @@ mod tests { )); } + #[test] + fn an_unknown_major_is_still_unsupported_format_version() { + // Pin 2's third arm: neither 0 (legacy) nor FORMAT_MAJOR (current) — + // still hard-rejected, exactly as the pre-epoch exact-major check was + // (`CONTRACT_FORMAT_EPOCH_MAJOR1.md`). + let mut bytes = FixedHeader::new(FileUuid::ZERO).encode(); + bytes[8..10].copy_from_slice(&2u16.to_le_bytes()); // format_major = 2 + let crc = crc32c(&bytes[0..60]); + bytes[60..64].copy_from_slice(&crc.to_le_bytes()); + assert!(matches!( + FixedHeader::decode(&bytes), + Err(BundleError::UnsupportedFormatVersion { major: 2, minor: 0 }) + )); + } + #[test] fn reserved_bytes_are_ignored_on_read() { // A future minor version may use reserved bytes; current readers must diff --git a/crates/epiphany-bundle/src/lib.rs b/crates/epiphany-bundle/src/lib.rs index 5b796b3..14b5c46 100644 --- a/crates/epiphany-bundle/src/lib.rs +++ b/crates/epiphany-bundle/src/lib.rs @@ -81,7 +81,7 @@ pub use crc::crc32c; pub use epiphany_determinism::{ChunkId, ContentHash}; pub use error::{BundleError, IntegrityAnomaly}; pub use header::{ - FixedHeader, FORMAT_MAJOR, FORMAT_MINOR, HEADER_LEN, SLOT_A_OFFSET, SLOT_B_OFFSET, + FixedHeader, FormatEpoch, FORMAT_MAJOR, FORMAT_MINOR, HEADER_LEN, SLOT_A_OFFSET, SLOT_B_OFFSET, }; pub use ids::{ BlobId, DocumentId, ExtensionId, FileUuid, FrontierBytes, LineageId, ManifestId, diff --git a/crates/epiphany-testkit/benches/bundle.rs b/crates/epiphany-testkit/benches/bundle.rs index 80266b3..d503589 100644 --- a/crates/epiphany-testkit/benches/bundle.rs +++ b/crates/epiphany-testkit/benches/bundle.rs @@ -17,8 +17,11 @@ //! deliberately not `std::env::temp_dir()`, which is commonly tmpfs on Linux, //! where fsync is a near-no-op and the commit budget would be measured against //! RAM. The corpus is moderate: 1,000 generated operation envelopes packed -//! into operation blocks plus a canonical `MaterializedState` snapshot wired -//! as the manifest's `canonical_base`. Both +//! into operation blocks plus a canonical `MaterializedState` snapshot. That +//! snapshot's manifest placement (`canonical_base`) is suspended for the S28 +//! -> P13-S27 interval (`CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3c) — see +//! `Fixture::base_root`'s doc comment — so it is staged and read back +//! directly by `ChunkRef` instead. Both //! are expected to **Pass** today, so a regression fails `cargo bench` loudly. //! The read row's honesty note: the spec sizes its 200 ms against a 100-page //! orchestral score; no such corpus generator exists yet, so this row is the @@ -34,9 +37,9 @@ //! envelope block, i.e. block append + manifest rewrite + superblock flip, //! every write fsync'd. This mirrors `bundle_harness`'s commit driver. //! * **open_bootstrap_read** — `FileStore::open` + `Bundle::open` (superblock -//! selection + header + manifest decode) + reading the `canonical_base` -//! snapshot and every operation block — the bytes a first interactive frame -//! needs. +//! selection + header + manifest decode) + reading the cold-reduction +//! snapshot (by direct `ChunkRef`, see above) and every operation block — +//! the bytes a first interactive frame needs. //! //! Criterion measures; the budget gate in `main` asserts (see //! `epiphany_testkit::budget` for the Pass/Xfail semantics and the documented @@ -50,9 +53,8 @@ use std::time::Duration; use criterion::{BatchSize, Criterion}; use epiphany_bundle::{ - pack_operation_blocks, Bundle, ChunkKind, CommitContext, DocumentId, FileStore, FileUuid, - FrontierBytes, Manifest, MemStore, ProfileId, ReductionAlgorithmVersion, SchemaVersion, - SnapshotId, SnapshotRef, StagedChunk, + pack_operation_blocks, Bundle, ChunkKind, ChunkRef, CommitContext, DocumentId, FileStore, + FileUuid, Manifest, MemStore, SchemaVersion, StagedChunk, }; use epiphany_determinism::CanonicalEncode; use epiphany_ops::{OperationEnvelope, OperationSet}; @@ -71,8 +73,21 @@ const EDIT_ENVELOPES: usize = 4; /// Everything the two rows measure against, built once from a fixed seed. struct Fixture { - /// The committed base image (blocks + canonical-base snapshot). + /// The committed base image (blocks + the corpus's cold-reduction + /// snapshot). base_image: Vec, + /// The cold-reduction snapshot's `ChunkRef`. + /// + /// **Suspended manifest placement** (`CONTRACT_FORMAT_EPOCH_MAJOR1.md` + /// pin 3c, amended 2026-08-07): this used to be `manifest.canonical_base` + /// — its correct semantic home — but pin 3a's format-epoch matrix refuses + /// every base introduction and every base-bearing open during the S28 -> + /// P13-S27 interval. The ref is carried here instead and read back + /// directly via `Bundle::read_chunk`, exactly as + /// `epiphany_testkit::roundtrip::assert_reduction_serialization_stable` + /// now does. **Not** re-homed to `acceleration_snapshots` — nothing in + /// `epiphany-bundle` verifies that field. + base_root: ChunkRef, /// The typical edit, staged (one small operation block). edit: Vec, /// Temp-dir file paths: one per row so commit growth never skews reads. @@ -99,8 +114,9 @@ fn staged_blocks(envelopes: &[OperationEnvelope]) -> Vec { } /// Builds the moderate base corpus: 1,000 envelopes committed as operation -/// blocks, then their cold reduction committed as a `Snapshot` chunk wired to -/// the manifest's `canonical_base` (the roundtrip harness's snapshot shape). +/// blocks, then their cold reduction committed as a `Snapshot` chunk — staged +/// but not wired into the manifest (pin 3c; see `Fixture::base_root`'s doc +/// comment). fn build_fixture(dir: &Path) -> Fixture { let mut rng = Rng::new(0x00F1_B0DE_0001); let envelopes = generators::operation_envelopes(&mut rng, BASE_ENVELOPES, 3, 40, 40); @@ -123,27 +139,18 @@ fn build_fixture(dir: &Path) -> Fixture { schema_version: SchemaVersion::V0, payload: canonical, }; - let frontier = generators::frontier_bytes(&envelopes); + let mut base_root = None; bundle .commit(&[snapshot], |ctx| { - let mut manifest = ctx.previous_manifest.clone(); - let root = ctx.new_chunks[0]; - let mut sid = [0u8; 16]; - sid.copy_from_slice(&root.hash.as_bytes()[..16]); - manifest.canonical_base = Some(SnapshotRef { - snapshot_id: SnapshotId(sid), - covers_causal_frontier: FrontierBytes::from_bytes(frontier.clone()), - reduction_algorithm_version: ReductionAlgorithmVersion(0), - profile_id: ProfileId::Full, - hash: root.hash, - root, - }); - manifest + base_root = Some(ctx.new_chunks[0]); + ctx.previous_manifest.clone() }) - .expect("commit canonical-base snapshot"); + .expect("commit cold-reduction snapshot"); + let base_root = base_root.expect("commit ran the build closure"); Fixture { base_image: bundle.into_store().into_bytes(), + base_root, edit: staged_blocks(&edit_envelopes), commit_path: dir.join("commit.epb"), read_path: dir.join("read.epb"), @@ -165,17 +172,15 @@ fn typical_edit_commit(mut bundle: Bundle, edit: &[StagedChunk]) -> u } /// The timed read: open (superblock selection + manifest decode) + the -/// bootstrap chunks — canonical-base snapshot and every operation block. -fn open_bootstrap_read(path: &Path) -> usize { +/// bootstrap chunks — the cold-reduction snapshot (read directly by its +/// `ChunkRef`; see `Fixture::base_root`'s doc comment for why it is not +/// `manifest.canonical_base` right now) and every operation block. +fn open_bootstrap_read(path: &Path, base_root: &ChunkRef) -> usize { let bundle = Bundle::open(FileStore::open(path).expect("open store")).expect("open bundle"); let manifest = bundle.manifest(); let mut bytes = 0usize; - let base = manifest - .canonical_base - .as_ref() - .expect("the fixture wires a canonical base"); bytes += bundle - .read_chunk(&base.root) + .read_chunk(base_root) .expect("snapshot chunk reads") .len(); for root in &manifest.operation_roots { @@ -206,7 +211,7 @@ fn criterion_measurements(criterion: &mut Criterion, fixture: &Fixture, quick: b fs::write(&fixture.read_path, &fixture.base_image).expect("write read-row image"); group.bench_function("open_bootstrap_read", |b| { - b.iter(|| open_bootstrap_read(&fixture.read_path)) + b.iter(|| open_bootstrap_read(&fixture.read_path, &fixture.base_root)) }); group.finish(); @@ -223,7 +228,7 @@ fn budget_gate(fixture: &Fixture, quick: bool) -> Vec { let read_median = budget::median_time( if quick { 8 } else { 25 }, || (), - |()| open_bootstrap_read(&fixture.read_path), + |()| open_bootstrap_read(&fixture.read_path, &fixture.base_root), ); vec![ budget::latency_gate( diff --git a/crates/epiphany-testkit/src/roundtrip.rs b/crates/epiphany-testkit/src/roundtrip.rs index ff40d54..9f910c2 100644 --- a/crates/epiphany-testkit/src/roundtrip.rs +++ b/crates/epiphany-testkit/src/roundtrip.rs @@ -225,14 +225,28 @@ fn canonical_score_bytes(envelopes: &[OperationEnvelope]) -> Vec { /// (acceptance criterion 4): the operation /// set reduces to canonical bytes; re-reducing the same set yields byte-identical /// bytes; and those bytes survive content-addressed storage in a real bundle — -/// stored as a `Snapshot` chunk referenced by the manifest's `canonical_base` -/// (its correct semantic home), hash-verified on reopen and read back +/// stored as a `Snapshot` chunk, hash-verified on reopen and read back /// byte-identically. /// -/// The snapshot's `covers_causal_frontier` is the frontier the snapshot actually -/// materializes ([`crate::generators::frontier_bytes`] over the reduced -/// envelopes), so it is semantically consistent — not a falsely-empty frontier -/// that would invite a replay layer to reapply already-materialized effects. +/// **Canonical-base wiring suspended** (`CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin +/// 3c, amended 2026-08-07). The snapshot's correct semantic home is the +/// manifest's `canonical_base` — that has not changed — but pin 3a's +/// format-epoch matrix refuses every base introduction (`Bundle::commit`) and +/// every base-bearing open (`Bundle::open`) during the S28 → P13-S27 +/// interval, in both format epochs. So this harness cannot wire the snapshot +/// there right now: it stages the snapshot as an ordinary chunk instead and +/// carries the resulting `ChunkRef` across the reopen out of band, reading it +/// back directly via [`Bundle::read_chunk`] (which hash-verifies any +/// `ChunkRef`, not only a canonical one). The +/// serialize → load → decode → reserialize cycle below is unchanged; only the +/// snapshot's manifest placement is suspended. **Not** re-homed to +/// `acceleration_snapshots` — that field is verified nowhere in +/// `epiphany-bundle` (not in `open`, not in `verify_canonical_chunks`), so a +/// reference there would verify nothing while looking like preserved +/// coverage. Two assertions lapse until P13-S27 restores the wiring: +/// `verify_canonical_chunks`'s base branch (its `base.hash != base.root.hash` +/// cross-check), and the reopened manifest actually carrying the base. Both +/// are owed back in `spec/CONTRACT_P13S27_REDUCTION_AUTHORITY.md`. /// /// After reopen, the snapshot payload is decoded through /// [`MaterializedState::decode_canonical`], compared structurally with the @@ -248,9 +262,13 @@ pub fn assert_reduction_serialization_stable(envelopes: &[OperationEnvelope], se "re-reduction changed the canonical score bytes" ); - // serialize: stage the canonical state as a real **Snapshot** chunk and - // reference it from the manifest's `canonical_base` — its correct semantic - // home (a materialized snapshot), with the right chunk kind. + // serialize: stage the canonical state as a real **Snapshot** chunk. + // P13-S27 / CONTRACT_FORMAT_EPOCH_MAJOR1.md pin 3c: the manifest's + // `canonical_base` is the snapshot's correct semantic home, but that + // wiring is suspended for the S28 -> P13-S27 interval (see the doc + // comment above) — the chunk is staged but not referenced from any + // manifest root, and its `ChunkRef` is captured directly from the commit + // closure instead. let mut rng = Rng::new(seed); let uuid = FileUuid(rng.array16()); let doc = DocumentId(rng.array16()); @@ -261,42 +279,26 @@ pub fn assert_reduction_serialization_stable(envelopes: &[OperationEnvelope], se schema_version: SchemaVersion::V0, payload: canonical.clone(), }; + let mut snapshot_root = None; bundle .commit(&[snapshot], |ctx| { - let mut m = ctx.previous_manifest.clone(); - let root = ctx.new_chunks[0]; - let mut sid = [0u8; 16]; - sid.copy_from_slice(&root.hash.as_bytes()[..16]); - m.canonical_base = Some(SnapshotRef { - snapshot_id: SnapshotId(sid), - // The frontier the snapshot actually materializes (covering every - // reduced envelope), not a falsely-empty one. - covers_causal_frontier: FrontierBytes::from_bytes(generators::frontier_bytes( - envelopes, - )), - reduction_algorithm_version: ReductionAlgorithmVersion(0), - profile_id: ProfileId::Full, - hash: root.hash, - root, - }); - m + snapshot_root = Some(ctx.new_chunks[0]); + ctx.previous_manifest.clone() }) .expect("commit snapshot"); + let snapshot_root = snapshot_root.expect("commit ran the build closure"); let image = bundle.into_store().into_bytes(); - // load: reopen from exactly those bytes; the snapshot chunk is hash-verified - // on open and read back byte-identically. + // load: reopen from exactly those bytes; the snapshot chunk is read back + // directly by its `ChunkRef` (not through `manifest.canonical_base`, + // suspended above) — `read_chunk` hash-verifies any `ChunkRef`, so the + // cycle's integrity guarantee is unchanged. let reopened = Bundle::open(MemStore::from_bytes(image)).expect("reopen bundle"); reopened .verify_canonical_chunks() .expect("canonical chunks intact"); - let base = reopened - .manifest() - .canonical_base - .as_ref() - .expect("a canonical base"); let loaded = reopened - .read_chunk(&base.root) + .read_chunk(&snapshot_root) .expect("read snapshot chunk back"); assert_eq!( loaded, canonical, diff --git a/crates/epiphany-testkit/tests/requirement_labels.rs b/crates/epiphany-testkit/tests/requirement_labels.rs index edd8828..b42f742 100644 --- a/crates/epiphany-testkit/tests/requirement_labels.rs +++ b/crates/epiphany-testkit/tests/requirement_labels.rs @@ -9,11 +9,14 @@ use std::collections::{BTreeMap, BTreeSet}; use std::fs; use std::path::{Path, PathBuf}; -const CORE_REQUIREMENT_COUNT: usize = 212; +// +1 for req:format:container-epoch (the format-epoch rung, pin 7: +// spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md) — the container major becomes an epoch, +// and Chapter 8 states the classification and the epoch matrix normatively. +const CORE_REQUIREMENT_COUNT: usize = 213; // +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; +// spec/PLAN_GMINOR_SCHEMA_MINOR.md); +1 for req:format:container-epoch (above). +const SUITE_REQUIREMENT_COUNT: usize = 284; +const SUITE_LABEL_COUNT: usize = 284; /// 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 5a59283..c0c2345 100644 --- a/crates/epiphany-textproj/src/lib.rs +++ b/crates/epiphany-textproj/src/lib.rs @@ -56,7 +56,18 @@ use epiphany_ops::OperationEnvelope; /// and `create-view` to the `kind` production — the same reasoning as every /// prior kind append: extending the grammar without moving this constant /// would leave two incompatible grammars both claiming `(0 11 0)`. -pub const COMPANION_VERSION: (u32, u32, u32) = (0, 13, 0); +/// +/// Bumped again 0.12.0 → 0.13.0 by the genesis tranche G3b, which appended +/// `create-measure` to the `kind` production. +/// +/// Bumped again 0.13.0 → 0.14.0 by `spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md` +/// pin 3b: a canonical base can no longer be projected, parsed, or +/// serialized by this companion at all — refusing a document the companion +/// previously serialized is a semantic change, so the bump is load-bearing, +/// not cosmetic. The `canonical-base` grammar production is retained (it is +/// what the refusal is defined against, and what a future rebuild/repack +/// flow will emit), but no document containing it round-trips. +pub const COMPANION_VERSION: (u32, u32, u32) = (0, 14, 0); /// A parsed canonical Text Projection document. /// diff --git a/crates/epiphany-textproj/src/parse.rs b/crates/epiphany-textproj/src/parse.rs index 4a5c03f..b002ef8 100644 --- a/crates/epiphany-textproj/src/parse.rs +++ b/crates/epiphany-textproj/src/parse.rs @@ -136,6 +136,18 @@ pub fn parse_document(text: &str) -> Result { // canonical-base? let canonical_base = take_line(&mut lines, "canonical-base", parse_canonical_base)?; + // `CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3b: text projection cannot mint a + // canonical base. The grammar still defines the production — it is what + // this refusal is defined against, and what a future rebuild/repack flow + // will emit — but no document containing it parses. `NotCanonical` + // matches the existing `(blob ...)` rejection just below: syntactically + // well-formed, but never a text this companion can accept. + if canonical_base.is_some() { + return Err(TextError::NotCanonical( + "a (canonical-base ...) line cannot be canonical text: no reduction-authority \ + validation exists for text projection to have produced it", + )); + } // blob*: collected only so the rejection below can fire; see the module // documentation and `req:textproj:reject-unreferenced-blobs`. @@ -650,12 +662,13 @@ mod tests { // Bumped with `COMPANION_VERSION` (0.7.0 → 0.8.0, genesis G1; 0.8.0 → // 0.9.0, genesis G2a; 0.9.0 → 0.10.0, G-minor; 0.10.0 → 0.11.0, genesis - // G2b; 0.11.0 → 0.12.0, genesis G3a; 0.12.0 → 0.13.0, genesis G3b). Kept + // G2b; 0.11.0 → 0.12.0, genesis G3a; 0.12.0 → 0.13.0, genesis G3b; + // 0.13.0 → 0.14.0, `CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3b). Kept // a literal because `projection` takes `&[&str]` and a formatted String // would ripple through every call site; // `the_test_header_tracks_the_implemented_version` below fails loudly if // the two ever drift. - const HEADER: &str = "(text-projection (0 13 0))"; + const HEADER: &str = "(text-projection (0 14 0))"; const DOCUMENT: &str = "(document #x00000000000000000000000000000001 (schema 0 1))"; /// A minimal but complete valid projection: just the two mandatory lines. @@ -757,28 +770,37 @@ mod tests { .retain_named_checkpoints ); assert!(document.extensions.is_empty()); - - let base = document - .canonical_base - .as_ref() - .expect("the worked example carries a canonical base"); - assert_eq!( - base.snapshot_id, - SnapshotId([0x1f, 0x8b, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - ); - assert_eq!(base.covers_causal_frontier, FrontierBytes::empty()); - assert_eq!( - base.reduction_algorithm_version, - ReductionAlgorithmVersion(1) - ); - assert_eq!(base.profile_id, ProfileId::Full); - assert_eq!(base.root_schema_version, SchemaVersion::V0); - assert_eq!(base.root_payload, vec![0u8, 0u8]); + // Pin 3b (`CONTRACT_FORMAT_EPOCH_MAJOR1.md`): the worked example no + // longer carries a canonical base — the companion's own chapter + // retired that spelling to a second, explicitly-refused listing + // (`spec/text_projection.tex`, "A Worked Example"). + assert!(document.canonical_base.is_none()); assert!(document.blobs.is_empty()); assert_eq!(document.envelopes.len(), 1); } + /// The base-bearing spelling `the_worked_example_parses_to_its_documented_fields` + /// used to exercise, before pin 3b retired it from the worked example + /// itself: still grammar-valid, no longer canonical text. Pinned as a + /// literal here (rather than re-reading the spec's second listing) so + /// this test does not depend on the exact prose the orchestrator chose to + /// wrap it in. + #[test] + fn parsing_base_bearing_text_is_refused() { + let base = + "(canonical-base #x1f8b0000000000000000000000000000 #x 1 full (schema 0 1) #x0000)"; + let text = projection(&[HEADER, DOCUMENT, base]); + let err = parse_document(&text).expect_err("base-bearing text must be refused"); + assert_eq!( + err, + TextError::NotCanonical( + "a (canonical-base ...) line cannot be canonical text: no reduction-authority \ + validation exists for text projection to have produced it", + ) + ); + } + // ----------------------------------------------------------------- // A synthetic document exercising lineage, multiple profiles, an // extension with preserved chunks, a canonical base, and multiple @@ -794,8 +816,11 @@ mod tests { let extension = "(extension #x00000000000000000000000000000003 (1 0 0) true \ ((chunk operation-envelope-block (schema 0 1) #xaa) (chunk snapshot (schema 0 1) #xbb)) \ #xaabb #xccdd)"; - let base = - "(canonical-base #x00000000000000000000000000000004 #x 1 full (schema 0 1) #x0102)"; + // Pin 3b (`CONTRACT_FORMAT_EPOCH_MAJOR1.md`): this document used to + // also carry a `(canonical-base ...)` line here, exercising every + // section at once. Base-bearing text is refused outright now — see + // `parsing_base_bearing_text_is_refused` — so this fixture drops it + // and keeps every other feature it was written to exercise together. let e1 = sample_envelope(1, 1, 10); let e2 = sample_envelope(2, 1, 20); @@ -809,7 +834,6 @@ mod tests { profile_full, profile_read_only, extension, - base, &e1_line, &e2_line, ]); @@ -832,7 +856,7 @@ mod tests { vec![0xaa, 0xbb] ); assert_eq!(document.extensions[0].edit_barriers, vec![0xcc, 0xdd]); - assert!(document.canonical_base.is_some()); + assert!(document.canonical_base.is_none()); assert_eq!(document.envelopes, vec![e1, e2]); } diff --git a/crates/epiphany-textproj/src/project.rs b/crates/epiphany-textproj/src/project.rs index ce41a42..b1d3874 100644 --- a/crates/epiphany-textproj/src/project.rs +++ b/crates/epiphany-textproj/src/project.rs @@ -41,8 +41,8 @@ use std::collections::BTreeSet; use epiphany_bundle::{ - BlobId, BlockStore, Bundle, BundleError, ChunkKind, DocumentId, LineageId, ProfileConstraints, - ProfileDeclaration, ProfileId, RetentionPolicy, SchemaVersion, SemVer, + BlobId, BlockStore, Bundle, BundleError, ChunkKind, DocumentId, LineageId, Manifest, + ProfileConstraints, ProfileDeclaration, ProfileId, RetentionPolicy, SchemaVersion, SemVer, }; use epiphany_core::textvalue::Sexp; use epiphany_ops::{ @@ -67,6 +67,15 @@ pub enum ProjectError { /// A stored operation-envelope block held bytes that do not decode to a /// canonical [`OperationEnvelope`]. Envelope(EnvelopeDecodeError), + /// The bundle's manifest carries a canonical base + /// (`spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3b: "text projection + /// cannot mint a canonical base"). Symmetric with + /// [`crate::serialize::SerializeError::CanonicalBaseUnsupported`] and + /// [`crate::parse`]'s own refusal — `req:textproj:roundtrip` quantifies + /// over every bundle and every valid text, so all three sides move + /// together: a document this companion cannot produce from text must + /// also never be produced *as* text. + CanonicalBaseUnsupported, } impl core::fmt::Display for ProjectError { @@ -74,6 +83,10 @@ impl core::fmt::Display for ProjectError { match self { ProjectError::Bundle(e) => write!(f, "bundle read failed: {e}"), ProjectError::Envelope(e) => write!(f, "operation envelope failed to decode: {e}"), + ProjectError::CanonicalBaseUnsupported => f.write_str( + "the bundle's manifest carries a canonical base, which this companion cannot \ + project: text projection cannot mint a canonical base", + ), } } } @@ -83,6 +96,7 @@ impl std::error::Error for ProjectError { match self { ProjectError::Bundle(e) => Some(e), ProjectError::Envelope(e) => Some(e), + ProjectError::CanonicalBaseUnsupported => None, } } } @@ -425,6 +439,33 @@ fn canonical_blobs( // Bundle -> TextDocument. // =========================================================================== +/// The check [`document_from_bundle`] and [`project_bundle`] apply before +/// doing any work (pin 3b): a base-bearing bundle is refused, symmetric with +/// [`crate::serialize::serialize_document`]'s and [`crate::parse::parse_document`]'s +/// own refusals. +/// +/// Exposed as its own function, over a bare [`Manifest`] rather than inlined +/// into `document_from_bundle`, so it is independently unit-testable. That +/// indirection is load-bearing, not stylistic: during the S28 → P13-S27 +/// interval, `CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3a's bundle-layer +/// refusals (rows 2/3/5i/6i of its epoch matrix) close *every* path — +/// `create`, `open`, and `commit` alike — that could ever produce a live +/// `Bundle` whose manifest carries a canonical base, in any crate, not only +/// this one (pin 3c: "no bundle anywhere may carry a canonical base ... in +/// production, in tests, or in the conformance suite"). So this predicate +/// cannot be driven end-to-end through a real `Bundle::open`/`create`/`commit` +/// call right now — there is no way to construct the `&Bundle` argument +/// `document_from_bundle` would need. `projecting_a_base_bearing_bundle_is_refused` +/// (below) tests this function directly against a hand-built `Manifest` +/// instead, and its own doc comment records this as a contract-execution +/// finding, not something patched around. +fn reject_canonical_base(manifest: &Manifest) -> Result<(), ProjectError> { + if manifest.canonical_base.is_some() { + return Err(ProjectError::CanonicalBaseUnsupported); + } + Ok(()) +} + /// Builds a [`TextDocument`] from a bundle's current manifest, reading every /// chunk and blob payload the projection carries inline: each extension's /// preserved chunks, the canonical base's root chunk, and (today, always @@ -440,6 +481,7 @@ pub fn document_from_bundle( bundle: &Bundle, ) -> Result { let manifest = bundle.manifest(); + reject_canonical_base(manifest)?; let mut decoded = Vec::new(); for root in &manifest.operation_roots { @@ -563,9 +605,9 @@ mod tests { use std::path::{Path, PathBuf}; use epiphany_bundle::{ - encode_block, BlobRef, CompressionAlgorithm, ExtensionDeclaration, ExtensionId, FileUuid, - FrontierBytes, Manifest, MemStore, ProfileRegistryId, ReductionAlgorithmVersion, - SnapshotId, SnapshotRef, StagedChunk, + chunk_content_hash, encode_block, BlobRef, ChunkRef, CompressionAlgorithm, + ExtensionDeclaration, ExtensionId, FileUuid, FrontierBytes, Manifest, MemStore, + ProfileRegistryId, ReductionAlgorithmVersion, SnapshotId, SnapshotRef, StagedChunk, }; use epiphany_core::textvalue::read_sexp; use epiphany_core::{OperationId, RegionId, ReplicaId, WallClockTime}; @@ -896,12 +938,12 @@ mod tests { payload: b"extension-chunk-payload".to_vec(), }; - let base_chunk = StagedChunk { - kind: ChunkKind::Snapshot, - schema_version: SchemaVersion::V0, - payload: b"canonical-base-payload".to_vec(), - }; - + // `CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3c: no bundle anywhere may + // carry a canonical base for the S28 -> P13-S27 interval — `commit` + // now refuses one outright, so this fixture used to stage a + // `base_chunk` and wire it into `manifest.canonical_base` here can no + // longer do so. The base-bearing scenario is exercised separately, at + // the `Manifest` level, by `projecting_a_base_bearing_bundle_is_refused`. let blob_chunk = StagedChunk { kind: ChunkKind::Blob, schema_version: SchemaVersion::V0, @@ -915,13 +957,7 @@ mod tests { bundle .commit( - &[ - op_block_0, - op_block_1, - extension_chunk, - base_chunk, - blob_chunk, - ], + &[op_block_0, op_block_1, extension_chunk, blob_chunk], |ctx| { let mut manifest = ctx.previous_manifest.clone(); manifest.operation_roots = vec![ctx.new_chunks[0], ctx.new_chunks[1]]; @@ -933,22 +969,14 @@ mod tests { affected_object_kinds: vec![0xAA], edit_barriers: vec![0xBB, 0xCC], }]; - manifest.canonical_base = Some(SnapshotRef { - snapshot_id: SnapshotId([7; 16]), - covers_causal_frontier: FrontierBytes::from_bytes(vec![1, 2, 3]), - reduction_algorithm_version: ReductionAlgorithmVersion(1), - profile_id: ProfileId::Full, - root: ctx.new_chunks[3], - hash: ctx.new_chunks[3].hash, - }); manifest.blob_roots = vec![BlobRef { - blob_id: BlobId(ctx.new_chunks[4].hash), + blob_id: BlobId(ctx.new_chunks[3].hash), media_type: "application/octet-stream".to_owned(), - offset: ctx.new_chunks[4].offset, - compressed_length: ctx.new_chunks[4].compressed_length, - uncompressed_length: ctx.new_chunks[4].uncompressed_length, + offset: ctx.new_chunks[3].offset, + compressed_length: ctx.new_chunks[3].compressed_length, + uncompressed_length: ctx.new_chunks[3].uncompressed_length, compression: CompressionAlgorithm::None, - hash: ctx.new_chunks[4].hash, + hash: ctx.new_chunks[3].hash, declared_max_uncompressed_length: None, }]; manifest @@ -995,9 +1023,9 @@ mod tests { assert_eq!(extension.chunks[0].kind, ChunkKind::ExtensionData); assert_eq!(extension.chunks[0].payload, b"extension-chunk-payload"); - let base = document.canonical_base.as_ref().expect("base was staged"); - assert_eq!(base.snapshot_id, SnapshotId([7; 16])); - assert_eq!(base.root_payload, b"canonical-base-payload"); + // `build_sample_bundle` no longer stages a canonical base (pin 3c: no + // bundle anywhere may carry one during the S28 -> P13-S27 interval). + assert!(document.canonical_base.is_none()); // The trap: a non-empty `blob_roots` still yields zero canonical // blobs, because nothing can reach one yet. @@ -1041,7 +1069,6 @@ mod tests { "(lineage", "(profile", "(extension", - "(canonical-base", "(envelope", "(envelope", ], @@ -1095,6 +1122,54 @@ mod tests { } } + #[test] + fn projecting_a_base_bearing_bundle_is_refused() { + // Pin 3b's projection side: `document_from_bundle`/`project_bundle` + // refuse a bundle whose manifest carries a canonical base. + // + // This cannot be driven end-to-end through a live `Bundle` right + // now: `CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3a's refusals (this same + // rung, `epiphany-bundle`) close *every* path that could produce + // one — `Bundle::create` already rejected an initial base before this + // rung, and `Bundle::open`/`Bundle::commit` now refuse one + // categorically in both format epochs (matrix rows 2/3/5i/6i) — so no + // crate, including this one, can construct a `Bundle` whose manifest + // carries a canonical base during the S28 -> P13-S27 interval (pin + // 3c: "no bundle anywhere may carry a canonical base ... in + // production, in tests, or in the conformance suite"). That is + // reported as a contract-execution finding, not patched around here. + // + // What IS independently checkable, without a live `Bundle`, is the + // exact predicate `document_from_bundle` guards on + // (`reject_canonical_base`): a `Manifest` — a public, freestanding + // type — carrying `canonical_base = Some(...)`. + let mut manifest = Manifest::empty(DocumentId([1; 16])); + let payload = b"snapshot".to_vec(); + let hash = chunk_content_hash(ChunkKind::Snapshot, SchemaVersion::V0, &payload); + let root = ChunkRef { + id: epiphany_bundle::ChunkId(hash), + kind: ChunkKind::Snapshot, + schema_version: SchemaVersion::V0, + offset: epiphany_bundle::BODY_START, + compressed_length: payload.len() as u64, + uncompressed_length: payload.len() as u64, + compression: CompressionAlgorithm::None, + hash, + }; + manifest.canonical_base = Some(SnapshotRef { + snapshot_id: SnapshotId([1; 16]), + covers_causal_frontier: FrontierBytes::empty(), + reduction_algorithm_version: ReductionAlgorithmVersion(0), + profile_id: ProfileId::Full, + hash: root.hash, + root, + }); + assert!(matches!( + reject_canonical_base(&manifest), + Err(ProjectError::CanonicalBaseUnsupported) + )); + } + // ----------------------------------------------------------------- // Suite reach: count, don't just claim, coverage of an extension, a // canonical base, and a multi-envelope document. @@ -1103,13 +1178,27 @@ mod tests { #[test] fn test_suite_reach_covers_extension_base_and_multi_envelope_documents() { let rich = document_from_bundle(&build_sample_bundle()).expect("bundle reads cleanly"); + // `build_sample_bundle` no longer carries a canonical base (pin 3c), + // so `with_canonical_base` reach is exercised here instead, directly + // on a `TextDocument` value — no live `Bundle` is involved, so pin + // 3a's bundle-layer refusal does not apply (this is not projection, + // parsing, or serialization; see + // `projecting_a_base_bearing_bundle_is_refused`, which covers the + // refusal itself). let minimal = TextDocument { document_id: DocumentId([6; 16]), manifest_schema_version: SchemaVersion::V0, lineage_id: None, profiles: vec![ProfileDeclaration::full()], extensions: Vec::new(), - canonical_base: None, + canonical_base: Some(TextCanonicalBase { + snapshot_id: SnapshotId([6; 16]), + covers_causal_frontier: FrontierBytes::empty(), + reduction_algorithm_version: ReductionAlgorithmVersion(1), + profile_id: ProfileId::Full, + root_schema_version: SchemaVersion::V0, + root_payload: b"reach-only-canonical-base".to_vec(), + }), blobs: Vec::new(), envelopes: vec![sample_envelope(1, 1)], }; diff --git a/crates/epiphany-textproj/src/serialize.rs b/crates/epiphany-textproj/src/serialize.rs index d9162f2..a5f97f6 100644 --- a/crates/epiphany-textproj/src/serialize.rs +++ b/crates/epiphany-textproj/src/serialize.rs @@ -70,6 +70,21 @@ pub enum SerializeError { /// non-canonical root into the manifest or silently drop the caller's /// payload bytes. Neither is acceptable, so serialization refuses instead. NonEmptyBlobs, + /// The document carries a canonical base + /// (`spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3b: "text projection cannot + /// mint a canonical base"). `TextDocument` has no container-major or epoch + /// field, and `parse` accepts an unbounded reduction-algorithm version, so + /// an old or hand-authored text document could otherwise be laundered + /// straight through the format-epoch boundary into a brand-new major-1 + /// container. A dedicated variant, not a `SerializeError::Bundle` + /// passthrough: the container-layer refusal + /// (`epiphany_bundle::BundleError::ReductionAuthorityUnavailable`) is + /// temporary and interim-only, while this refusal is a permanent property + /// of what the text medium can prove — a text format cannot carry + /// unforgeable provenance, so refusal is the only rule it can actually + /// enforce. This is real capability loss: base-bearing documents stop + /// round-tripping through text until a repack flow exists. + CanonicalBaseUnsupported, /// The bundle itself rejected the manifest or a staged chunk — e.g. the /// document declares no profile this implementation understands, or a /// staged root fails `Bundle::commit`'s structural validation. @@ -82,6 +97,10 @@ impl fmt::Display for SerializeError { SerializeError::NonEmptyBlobs => f.write_str( "the document carries a populated blobs vector, but no blob can be canonical today", ), + SerializeError::CanonicalBaseUnsupported => f.write_str( + "the document carries a canonical base, which this companion cannot serialize: \ + text projection cannot mint a canonical base", + ), SerializeError::Bundle(error) => { write!(f, "the bundle rejected the serialized document: {error}") } @@ -94,6 +113,7 @@ impl std::error::Error for SerializeError { match self { SerializeError::Bundle(error) => Some(error), SerializeError::NonEmptyBlobs => None, + SerializeError::CanonicalBaseUnsupported => None, } } } @@ -114,8 +134,12 @@ impl From for SerializeError { /// [`ChunkRef`](epiphany_bundle::ChunkRef)s that commit assigns. /// /// Returns [`SerializeError::NonEmptyBlobs`] if `document.blobs` is non-empty -/// (see the module documentation), or [`SerializeError::Bundle`] if the bundle -/// itself refuses the manifest or a staged root. +/// (see the module documentation), [`SerializeError::CanonicalBaseUnsupported`] +/// if `document.canonical_base` is present (pin 3b: text projection cannot +/// mint a canonical base — checked as defence for a directly constructed +/// `TextDocument`, since `parse` also refuses one on the way in), or +/// [`SerializeError::Bundle`] if the bundle itself refuses the manifest or a +/// staged root. pub fn serialize_document( document: &TextDocument, store: S, @@ -124,6 +148,9 @@ pub fn serialize_document( if !document.blobs.is_empty() { return Err(SerializeError::NonEmptyBlobs); } + if document.canonical_base.is_some() { + return Err(SerializeError::CanonicalBaseUnsupported); + } let mut bundle = Bundle::create(store, file_uuid, empty_manifest(document))?; @@ -335,11 +362,16 @@ mod tests { document } - /// Carries an extension, a canonical base, and many envelopes at once — the - /// document every round-trip law needs to be checked against together. + /// Carries an extension and many envelopes at once — the document every + /// round-trip law needs to be checked against together. + /// + /// Used to carry a canonical base too (hence the name); pin 3b's + /// base-bearing exclusion means that capability moved to + /// `document_with_canonical_base` alone, so this fixture keeps every + /// other feature and drops the base + /// (`CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3b). fn rich_document(seed: u64) -> TextDocument { let mut document = document_with_extension(seed); - document.canonical_base = document_with_canonical_base(seed).canonical_base; document.envelopes = envelopes(seed, 60); document } @@ -365,6 +397,18 @@ mod tests { ); } + #[test] + fn text_projection_serialize_produces_a_major_1_container() { + // Pin 6: every writer path stamps the epoch, including text + // projection. `serialize_document` (via `build_manifest`) is the one + // production writer that reaches a canonical base — `project.rs:936` + // is a `#[cfg(test)]` fixture writer, not production. + let document = minimal_document(11); + let bundle = serialize_document(&document, MemStore::new(), FileUuid([1; 16])) + .expect("a well-formed document serializes"); + assert_eq!(bundle.header().format_major, epiphany_bundle::FORMAT_MAJOR); + } + #[test] fn document_identity_round_trips() { let mut document = minimal_document(1); @@ -393,31 +437,23 @@ mod tests { } #[test] - fn canonical_base_round_trips_snapshot_id_and_payload() { + fn serializing_a_text_document_with_a_canonical_base_is_refused() { + // Pin 3b: text projection cannot mint a canonical base. Built from + // the existing base-bearing fixture (`document_with_canonical_base`), + // which used to round-trip through `serialize_document` / + // `document_from_bundle` before this rung — that capability is a + // real, intentional loss (base-bearing documents stop round-tripping + // through text until a repack flow exists). let document = document_with_canonical_base(4); - let reopened = serialize_and_reopen(&document); - let expected = document - .canonical_base - .as_ref() - .expect("fixture carries a canonical base"); - let base = reopened - .manifest() - .canonical_base - .as_ref() - .expect("canonical base present after reopen"); - assert_eq!(base.snapshot_id, expected.snapshot_id); - assert_eq!(base.covers_causal_frontier, expected.covers_causal_frontier); - assert_eq!( - base.reduction_algorithm_version, - expected.reduction_algorithm_version - ); - assert_eq!(base.profile_id, expected.profile_id); - assert_eq!(base.root.kind, ChunkKind::Snapshot); - assert_eq!(base.root.schema_version, expected.root_schema_version); - let payload = reopened - .read_chunk(&base.root) - .expect("root chunk reads and verifies"); - assert_eq!(payload, expected.root_payload); + let result = serialize_document(&document, MemStore::new(), FileUuid([1; 16])); + assert!(matches!( + result, + Err(SerializeError::CanonicalBaseUnsupported) + )); + // M8 depends on this distinction: the dedicated variant, never a + // `SerializeError::Bundle` passthrough from the container-layer + // refusal this would otherwise fall through to. + assert!(!matches!(result, Err(SerializeError::Bundle(_)))); } #[test] @@ -533,7 +569,9 @@ mod tests { let document = rich_document(10); let reopened = serialize_and_reopen(&document); assert_eq!(reopened.manifest().document_id, document.document_id); - assert!(reopened.manifest().canonical_base.is_some()); + // `rich_document` no longer carries a base (pin 3b) — every other + // root it carries still round-trips together. + assert!(reopened.manifest().canonical_base.is_none()); assert_eq!(reopened.manifest().extension_declarations.len(), 1); assert_eq!(reopened.manifest().operation_roots.len(), 1); reopened @@ -579,6 +617,17 @@ mod tests { ); for document in &documents { + if document.canonical_base.is_some() { + // Pin 3b: a base-bearing document is refused outright, not + // round-tripped — the one round-trip law this suite must now + // except. + let result = serialize_document(document, MemStore::new(), FileUuid([1; 16])); + assert!(matches!( + result, + Err(SerializeError::CanonicalBaseUnsupported) + )); + continue; + } let reopened = serialize_and_reopen(document); assert_eq!(reopened.manifest().document_id, document.document_id); } diff --git a/crates/epiphany-textproj/src/vectors.rs b/crates/epiphany-textproj/src/vectors.rs index 2726a76..02d58ac 100644 --- a/crates/epiphany-textproj/src/vectors.rs +++ b/crates/epiphany-textproj/src/vectors.rs @@ -123,16 +123,26 @@ pub struct ReachCounts { /// The exact non-vacuity contract of this corpus: four accepted documents, two /// reaching each optional/rich feature, and one actually rejected vector for -/// each of the nine distinct rejection classes implemented by this layer. +/// each of the ten distinct rejection classes implemented by this layer. +/// +/// `canonical_bases` is pinned at **0**, not two: +/// `CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3b refuses projecting, parsing, and +/// serializing any base-bearing document, so a canonical base is no longer +/// reachable through text at all. The two formerly base-bearing accepts +/// (`extension_base_two_envelopes`, `rich_document`) keep every other feature +/// they exercised but lose their base, and the base-bearing spelling survives +/// only as the new `canonical_base_present` reject vector (class +/// `canonical-base-unsupported`). pub fn expected_reach() -> ReachCounts { ReachCounts { extensions: 2, - canonical_bases: 2, + canonical_bases: 0, custom_profiles: 2, lineages: 2, multi_envelope: 2, reject_classes: [ ("blob-line", 1), + ("canonical-base-unsupported", 1), ("extension-chunk-order", 1), ("extension-declaration-order", 1), ("missing-trailing-lf", 1), @@ -341,7 +351,12 @@ fn accept_documents() -> Vec<(&'static str, String)> { blobs: Vec::new(), envelopes: vec![sample_envelope(1, 100)], }; - let extension_base_multi = TextDocument { + // `CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3b: text projection can no longer + // carry a canonical base at all, so this document (still named for the + // two envelopes it exercises, not for a base it no longer carries) keeps + // its extension and non-baseline schema version but drops the base that + // used to make it `extension_base_multi`. + let extension_two_envelopes = 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 @@ -350,17 +365,19 @@ fn accept_documents() -> Vec<(&'static str, String)> { lineage_id: None, profiles: profiles(false), extensions: vec![extension(1, &[1, 2])], - canonical_base: Some(base(3)), + canonical_base: None, blobs: Vec::new(), envelopes: vec![sample_envelope(2, 200), sample_envelope(3, 300)], }; + // Base removed (pin 3b); lineage, custom profile, two extensions, and + // multiple envelopes are all retained. 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])], - canonical_base: Some(base(4)), + canonical_base: None, blobs: Vec::new(), envelopes: vec![ sample_envelope(4, 400), @@ -459,7 +476,7 @@ fn accept_documents() -> Vec<(&'static str, String)> { ), ( "extension_base_two_envelopes", - project_text_document(&extension_base_multi), + project_text_document(&extension_two_envelopes), ), ("rich_document", project_text_document(&rich)), ("create_staff_group", project_text_document(&staff_group)), @@ -548,7 +565,7 @@ pub fn document_vectors() -> Vec { }; let minimal = by_name("minimal"); let lineage_custom = by_name("lineage_custom_profile"); - let extension_base_multi = by_name("extension_base_two_envelopes"); + let extension_two_envelopes = by_name("extension_base_two_envelopes"); let rich = by_name("rich_document"); let mut vectors: Vec = accepts @@ -556,17 +573,16 @@ 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 - // tranche G3b moved `COMPANION_VERSION` to 0.13.0, which had been this - // vector's "future" version — leaving it would have made the negative - // vector assert that the *correct* header is rejected. It now names - // 0.12.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. + // `CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3b moved `COMPANION_VERSION` to + // 0.14.0; this vector now names 0.13.0, the immediately superseded + // companion (previously 0.12.0, when the committed version was 0.13.0) — + // 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 14 0))", "(text-projection (0 13 0))", - "(text-projection (0 12 0))", ); vectors.push(( SURFACE, @@ -590,12 +606,18 @@ pub fn document_vectors() -> Vec { blob.into_bytes(), )); + // Re-expressed on a non-base section pair (pin 3b: neither accept carries + // a canonical base to invert against any more). `projection`'s order is + // `header document lineage? profile* extension* canonical-base? blob* + // envelope*`, so a profile/extension inversion reaches + // `out-of-order-sections` exactly as the old canonical-base/extension + // inversion did, without a base. vectors.push(( SURFACE, "reject", "out-of-order-sections", "canonical_base_before_extension", - swap_first_lines(extension_base_multi, "(extension ", "(canonical-base ").into_bytes(), + swap_first_lines(extension_two_envelopes, "(profile ", "(extension ").into_bytes(), )); vectors.push(( @@ -611,7 +633,7 @@ pub fn document_vectors() -> Vec { "reject", "operation-envelope-order", "envelopes_reversed", - swap_first_two_lines(extension_base_multi, "(envelope ").into_bytes(), + swap_first_two_lines(extension_two_envelopes, "(envelope ").into_bytes(), )); vectors.push(( @@ -631,7 +653,7 @@ pub fn document_vectors() -> Vec { )); let chunks_reversed = replace_once( - extension_base_multi, + extension_two_envelopes, "((chunk extension-data (schema 0 1) #x01) (chunk extension-data (schema 0 1) #x02))", "((chunk extension-data (schema 0 1) #x02) (chunk extension-data (schema 0 1) #x01))", ); @@ -653,6 +675,28 @@ pub fn document_vectors() -> Vec { missing_lf, )); + // NEW (pin 3b): a base-bearing text, refused by the parse-side check — + // built from the pre-change base-bearing spelling, so the corpus keeps a + // base-bearing text as a negative rather than losing the spelling + // entirely. + let canonical_base_present = TextDocument { + document_id: DocumentId([11; 16]), + manifest_schema_version: SchemaVersion::V0, + lineage_id: None, + profiles: profiles(false), + extensions: Vec::new(), + canonical_base: Some(base(11)), + blobs: Vec::new(), + envelopes: Vec::new(), + }; + vectors.push(( + SURFACE, + "reject", + "canonical-base-unsupported", + "canonical_base_present", + project_text_document(&canonical_base_present).into_bytes(), + )); + vectors } @@ -886,7 +930,7 @@ mod tests { #[test] fn the_reference_implementation_agrees_with_every_vector() { match verify(COMMITTED) { - Ok(count) => assert_eq!(count, 19, "the corpus has unexpectedly thinned"), + Ok(count) => assert_eq!(count, 20, "the corpus has unexpectedly thinned"), Err(failures) => panic!( "{} disagreement(s):\n{}", failures.len(), @@ -957,20 +1001,21 @@ mod tests { } /// (t12) Genesis tranche G3b: text projection round-trips the new - /// `create-measure` kind, and the companion version is **0.13.0**, with - /// the negative vector rejecting **0.12.0** (the immediately superseded + /// `create-measure` kind, and the companion version is **0.14.0** + /// (`CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3b bumped it from 0.13.0), with + /// the negative vector rejecting **0.13.0** (the immediately superseded /// companion). /// /// **Mutation:** drop `OperationKindTag::CreateMeasure` from /// `OperationKind::parse` in `textproj_kind.rs`; must fail. Separately, - /// leave `COMPANION_VERSION` at `(0, 12, 0)`; the negative vector must + /// leave `COMPANION_VERSION` at `(0, 13, 0)`; the negative vector must /// fail. #[test] - fn t12_g3b_kinds_round_trip_and_companion_is_0_13_0_rejecting_0_12_0() { + fn t12_g3b_kinds_round_trip_and_companion_is_0_14_0_rejecting_0_13_0() { assert_eq!( crate::COMPANION_VERSION, - (0, 13, 0), - "the companion version must be 0.13.0" + (0, 14, 0), + "the companion version must be 0.14.0" ); let name = "create_measure"; @@ -988,7 +1033,7 @@ mod tests { ); // The negative vector must reject exactly the immediately superseded - // companion, 0.12.0. + // companion, 0.13.0. let rows = parse(COMMITTED).expect("the committed corpus parses"); let superseded = rows .iter() @@ -997,8 +1042,8 @@ mod tests { assert_eq!(superseded.verdict, "reject"); let text = String::from_utf8(superseded.text.clone()).expect("utf8"); assert!( - text.contains("(text-projection (0 12 0))"), - "the negative vector must name the immediately superseded companion 0.12.0, got: {text}" + text.contains("(text-projection (0 13 0))"), + "the negative vector must name the immediately superseded companion 0.13.0, got: {text}" ); assert!( parse_document(&text).is_err(), diff --git a/spec/CONTRACT_P13S27_REDUCTION_AUTHORITY.md b/spec/CONTRACT_P13S27_REDUCTION_AUTHORITY.md index 46d301c..8a899e0 100644 --- a/spec/CONTRACT_P13S27_REDUCTION_AUTHORITY.md +++ b/spec/CONTRACT_P13S27_REDUCTION_AUTHORITY.md @@ -1,14 +1,21 @@ # Contract — P13-S27: the reduction version gets an outside witness -**Status:** DRAFT — **BLOCKED on P13-S28** (the format-epoch rung). Pins 1 and -3–10 are settled, internally consistent, and ratifiable as a plan. **Pin 2a is -an open question this contract states rather than answers**, and it is not -answerable inside this rung: the provenance carrier it needs is a format-epoch -design, which P13-S28 owns. +**Status:** DRAFT — **BLOCKED on the format-epoch rung** +(`spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md`), which is **ratified and in +implementation** but has not yet landed. Pins 1 and 3–10 are settled, internally +consistent, and ratifiable as a plan. -**This is bounded analysis, not an abandoned implementation plan.** It may not be -dispatched. Pin 2a MUST NOT be treated as a sub-pin that drifts into this rung's -scope — see its own prohibition. +**Pin 2a is RESOLVED as of 2026-08-07** — from outside this rung, by that +contract's pin 8, exactly as its prohibition required. Legacy bases are refused +by container epoch, never by version arithmetic; see the resolution block under +pin 2a. This contract additionally **inherits three obligations** from that rung +(the two interim refusals it must convert to validation, M8's deferred laundering +demonstration, and pin 3c's two suspended conformance assertions) — recorded in +the same place. + +**It becomes dispatchable when the format rung lands, not before.** Until then +this remains bounded analysis. Pin 2a's original prohibition stands for the +record: it was never amended into a disposition from inside this contract. **Rung type:** **capability + API change.** No wire bytes move and no schema major or minor changes — `BundleError` has no discriminant and no encoder @@ -16,10 +23,10 @@ major or minor changes — `BundleError` has no discriminant and no encoder change is `Bundle::open`'s **and `Bundle::create`'s** signatures, at 57 and 32 call sites. -**Does NOT by itself unblock P13-S16.** This rung installs the authority and -validates both read and write paths. S16 additionally requires the **legacy-base -disposition of pin 2a**, which is an open question this contract states rather -than answers. +**Now DOES unblock P13-S16, once it lands.** This rung installs the authority and +validates both read and write paths; S16's remaining precondition was pin 2a's +legacy-base disposition, and that is resolved. The chain is therefore +format-epoch rung → **P13-S27** → **P13-S16**, with no open question left in it. **Rulings already made (2026-07-31), not re-opened here:** @@ -246,6 +253,74 @@ dispatchable merely because S27 lands.** This pin deliberately remains an open question. It is not to be amended into a disposition without its own ratification round. +### Pin 2a — **RESOLVED 2026-08-07 by the format-epoch rung.** + +The ratification round this pin demanded is the one the format-epoch contract +had: `spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md`, ratified after four adversarial +review rounds, whose **pin 8** exists to resolve this pin from outside it. The +open analysis above is retained verbatim as the reasoning that produced the +answer, not superseded prose. + +**The disposition is none of (i), (ii) or (iii): it is the container epoch.** + +> **Reduction-version authority is meaningful only in major-1 containers. +> Legacy bases are refused by container epoch, never by version arithmetic.** + +That is why the collision this pin identified never has to be adjudicated. A +pre-S27 base carrying `1` and a legitimately rebuilt S16 base carrying `1` are +indeed indistinguishable **as numbers** — and they never meet, because the +pre-S27 base can only exist in a major-0 container, which is refused at the +epoch boundary before any version is compared. The `u32` never has to carry +provenance, because the container already does. + +Each rejected option, and why the epoch beats it: **(i)** normalizing the corpus +would have to find every artifact, and a missed one is silently wrong forever; +**(ii)** a non-colliding epoch value is a convention a hand-authored document can +simply declare — `parse.rs:591` accepts an unbounded `u32`; **(iii)** widening +the type makes the wire meaning richer and buys nothing the container property +does not already give. All three try to make a number carry provenance. The +epoch makes the *file* carry it. + +**S27's own baseline stays `0`** (pin 2 is unchanged). What changes is that the +question "what about a base older than the authority?" is no longer S27's to +answer. + +### Inherited from the format-epoch rung — obligations S27 must discharge + +The format rung lands **before** this one and closes two things temporarily, +naming S27 as what reopens them. Both are owed work here, not optional: + +1. **The interim refusals become real validation.** The format rung's pin 3a + temporarily refuses **both** major-1 base boundaries — opening a major-1 + container that already carries a base, and committing a base into one — + through a third, temporary error (`ReductionAuthorityUnavailable`) that is + distinct from its two legacy/repack errors. S27 replaces **both** branches + with capability validation. Replacing only one leaves a hole exactly where + the format rung's own review found one. + +2. **The deferred laundering demonstration** (format-rung M8). That rung could + not demonstrate the text-import laundering path end to end, because pin 3a + refuses every major-1 base commit categorically, so the "a base-bearing text + document really does serialize into a major-1 container" observation is + unreachable there. Under S27 a base commit succeeds or fails **on its + version**, so the demonstration becomes performable and is owed: with pin 3b's + text refusal removed, show that a base-bearing document whose raw version + happens to match the current authority serializes into a major-1 container + indistinguishable from a validated one. That is the false provenance the text + refusal exists to prevent, and it has never been observed — only reasoned + about. + +3. **Two conformance assertions come back** (format-rung pin 3c). Criterion 4's + bookkeeping-projection counterpart, `assert_reduction_serialization_stable` + (`testkit/src/roundtrip.rs:241`), keeps its serialize → load → decode → + reserialize cycle through the interval but loses exactly two + canonical-base-specific assertions: `verify_canonical_chunks`'s base branch + (`bundle.rs:613`–`:621`, including the `base.hash != base.root.hash` + cross-check), and the reopened manifest actually carrying the base + (`roundtrip.rs:293`–`:297`). S27 restores both, since a base-bearing container + becomes constructible again the moment validation replaces refusal. The + harness carries a marker naming this contract at the suspension point. + **Pin 3 — `BundleCapabilities`, required at both constructors, carried on the `Bundle`.** @@ -404,10 +479,27 @@ Named, permanent, in `epiphany-bundle`: intact. A writer check that corrupts the document while refusing is worse than no check. +**Added 2026-08-07 with pin 2a's resolution — the inherited obligations, stated +as tests so they cannot be discharged by prose:** + +6. **`a_major_1_bundle_carrying_a_base_opens_when_the_authority_matches`** — the + read-side half of the format rung's pin 3a, converted from temporary refusal + to real validation. Its sibling is test 2, which is the same path when the + authority *disagrees*. Both branches must exist; the format rung's own review + found a draft that closed only one. +7. **The two restored conformance assertions** (format-rung pin 3c), in + `assert_reduction_serialization_stable` (`testkit/src/roundtrip.rs:241`): + `verify_canonical_chunks` covering the base again, and the reopened manifest + carrying it. **Restoring them means deleting the suspension marker** that + names this contract — if the marker is still in the tree when this rung + reports, the restoration did not happen. + Tests 2 and 3 must be **paired in review**: each asserts the other's error is *not* produced. A test that only checks its own variant cannot show the two paths are distinguishable, which is the whole point of pin 6. +Tests 6 and 2 stand in the same relation to each other. + --- ## §4. Mutation plan diff --git a/spec/PASS13_CANDIDATES.md b/spec/PASS13_CANDIDATES.md index a4d8baa..c59cca3 100644 --- a/spec/PASS13_CANDIDATES.md +++ b/spec/PASS13_CANDIDATES.md @@ -122,5 +122,5 @@ evidence in isolation. | P13-S23 | **No filed candidate owns "place any anchor pair on a common timeline and measure musical distance along it" — P13-S18 previously mis-cited a narrower capability as its gate.** Two disjoint deficiencies, both owned by this candidate. (1) **No ordering.** The pair is not comparable under any of `measure20_comparable_order`'s five shapes c1-c5 (`invariants.rs:2457`) at all — whether the failure is in the **referent** (distinct `Event` ids; distinct `Measure` ids outside c3's `Start`+`Zero` restriction), the **variant or selector** (`Event` against `Measure`, `Measure` against `Region`, differing `pos`/`edge`), or the **clock** (`Musical` against `WallClock`, including inside `measure20_offset_order`, `:2419`) — this is what invariant 20's A4 and B4 are made of. (2) **Ordering without a usable delta.** The pair IS comparable and still yields no musical distance: c3 supplies a vector index (an order, never a distance), and c5 compares two `WallClock`s, and `measure20_musical_delta` (`:2522`) never returns a `WallClock` delta (`:2527`) — this is what invariant 20's B5 is made of. Scoping this as merely "anchors of differing shapes" or "not directly comparable under c1-c5" would exclude B5 entirely — S5 (distinct-id `Measure` `Start`/`Zero`) is c3-comparable and S1 (`WallClock` measures, `WallClock` meter changes) is c5-comparable, and both still reach B5 — an earlier draft of this filing made exactly that narrower mistake. **Explicitly broader than P11-C5**: P11-C5 (`PASS11_WORKLIST.md:159`) is a re-anchoring proximity metric that resolves "when the graph-mutation phase tracks resolved positions", and covers narrowly the two-distinct-`Event`s case (`CONTRACT_GENESIS_G3B_MEASURE.md:223`, `effect.rs:139`-`:142`'s `PositionOutsideRegion` Reserved note); P13-S23 is the timeline itself, whatever positions get placed on it. Names its dependents: invariant 20's A4, B4 and B5, and `PositionOutsideRegion`'s Reserved status | `spec/CONTRACT_P13S18_MATRIX.md` pin 10 (filed 2026-07-31 during the same rung that corrected P13-S18's over-narrow P11-C5 citation) | **open.** No code owed by this rung. Closing it needs the deferred common-timeline/duration machinery — once a `Measure` end, a distinct-id `Measure`/`Event` referent, or an `Event` position on a wall-clock-placed region can be placed on a common timeline with a musical distance, invariant 20's A4/B4/B5 residue and `PositionOutsideRegion`'s Reserved status shrink together | | P13-S25 | **The committed decode corpus's numbered tag rows lock byte→byte, not variant→byte — one row already has the property the other thirty-nine lack.** `ops/src/vectors.rs:206`–`:209` emits one row per tag as `format!("tag_{:02}", tag.discriminant())` carrying `[discriminant]`: **both the name and the payload derive from the value alone**, so `tag_32` asserts that `0x20` round-trips and never that `SetCanvasLayoutDefaults` is 32. The `Registered` row (`:210`–`:217`) is different — its name is the hard-coded string `"registered"` while its bytes are computed from the variant, so the frozen literal at `spec/vectors/decode_vectors.txt:80` binds the association. **Disposition B of P13-S22:** give the numbered rows the same property. It **does** catch the coordinated permutation — by exactly the `Registered` mechanism, with the committed text serving as the independent statement — and it propagates the property to every implementation that reads the cross-impl corpus, which an in-crate Rust test cannot do | `spec/CONTRACT_P13S22_TAGLOCK.md` (disposition B, considered and deferred during the 2026-07-31 ruling; filed rather than left as a closing remark, per the same discipline that moved P13-S22 out of P13-S15's resolved row) | **open. Complementary to P13-S22, not a replacement for it, and not a re-litigation of it.** P13-S22 landed disposition A (`tag_wire_discriminants_are_golden`, `payload.rs:2730`), which fails **by variant name inside the crate**. B cannot supply that: its failure is still *"spec/vectors/decode_vectors.txt is stale. Regenerate: …"* (`testkit/src/vectors.rs:224`) — the misleading diagnosis P13-S22 was filed about — even though the diff text would now name variants. **What B buys is cross-implementation reach; what it costs is churn in a committed artifact other implementations pin.** Both are wanted; neither substitutes for the other. Sequencing note: run B's own signing mutation as the coordinated permutation (literals *and* declaration lines), since the literal-only form is caught today by row ordering and proves nothing | | P13-S26 | **A doc comment in shipped code claims a specification repair that never landed, and the claim is guarded on the code side and nowhere on the specification side.** `crates/epiphany-core/src/invariants.rs:69`–`:71` enumerates invariant 10's four reference classes and states that *“genesis tranche G3a repairs this prose to name what the check body already enforced”*. **It did not.** `core_spec.tex:6570`–`:6572`, the normative enumeration item 10, still reads only *“Every cross-cutting structure's references resolve to extant objects in the graph, except where explicit re-anchoring rules permit transient dangling states during edits”* — naming neither a staff's declared instrument, a staff's group, a staff group's members, a part's staves, a view's active layers, nor any of the meter/time-signature references the Rust doc lists and the check body enforces. The repair landed in the Rust doc comment only. **The asymmetry is the defect's sharp edge:** the Rust doc block is protected by a grep-assert, `t12_invariant_10_doc_comment_names_the_four_reference_classes` (`invariants.rs:4554`, needles at `:4562`–`:4566`), so the side that is *wrong about the other* is the side that is **locked**, while the side that is actually stale is unguarded | this file (found 2026-07-31 during P13-S16 reconnaissance, while verifying that row's invariant-10 citations; no ledger entry covered it) | **open.** **Not a live incorrectness** — the check body is correct and enforces every class; only the normative prose under-describes it, and only the doc comment lies about that. **A P13-S9 instance**, and filed deliberately as one: the loud form (a dangling citation) is caught by `requirement_labels.rs`, and this quiet form — a *true-sounding claim about another document's state* — is caught by nothing. **`invariants.rs:69`–`:71` MUST NOT be “corrected” on its own.** It is currently the only artifact in the tree pointing at the `core_spec.tex` gap; softening the Rust claim in isolation would make the specification defect invisible and convert a caught defect into an uncaught one — which is P13-S9's stated failure mode verbatim. **Repair both sides in one rung**, and consider whether the LaTeX enumeration deserves the grep-assert its Rust mirror already has | -| P13-S27 | **The reduction-algorithm-version machinery is self-referential, so the one check that would detect a canonical-semantics change necessarily passes.** `core_spec.tex:11614`–`:11617` is normative — *"Snapshots produced under an earlier algorithm version cannot be used as canonical bases under a later one without rebuilding"* — and `:14369`–`:14372` states that replicas at differing versions *"may produce different canonical states from the same operation set."* The machinery to enforce it appears to exist: `ReductionAlgorithmVersion` (`bundle/src/ids.rs:291`) is a superblock wire field (bytes `68..72`, `superblock.rs:20`); `reduction_version_for` (`bundle.rs:989`) sets a new superblock's value; and `open` (`bundle.rs:396`–`:399`) rejects a mismatch. **But the writer sources the value from the canonical base's own self-report** (mapping the base's `reduction_algorithm_version` through `unwrap_or_default()`), **and the reader compares it only against the superblock that value seeded.** Nothing compares either against the semantics the running implementation actually implements. **The check is not vacuous** — it catches a corrupt or tampered base whose version disagrees with its superblock — but it **necessarily passes for a conformingly propagated stale base**, which is precisely the case the requirement exists to prevent. Supporting: **no constant or accessor anywhere names the implementation's current reduction semantics**, and `ids.rs:288`–`:289` states that *"the algorithm catalog itself lives in `epiphany-ops`"* while nothing of the kind exists in that crate — **a second instance of P13-S26's pattern**, a doc comment asserting a false fact about another module | `spec/CONTRACT_P13S16_PROJECTION.md` pin 0 (found 2026-07-31 while scoping P13-S16, which is a canonical reduction-semantics change and therefore the first rung to need this guarantee; filed in the same ledger edit as the row it blocks) | **open, BLOCKED on P13-S28, and blocking P13-S16.** **Scoped 2026-07-31 as `spec/CONTRACT_P13S27_REDUCTION_AUTHORITY.md` (DRAFT, not dispatchable).** Rulings taken: a typed `BundleCapabilities` required at both `Bundle::open` and `Bundle::create` and carried on the `Bundle` — no default, so every caller states the semantics it implements — and outright rejection on mismatch via a new `CanonicalBaseRequiresRebuild` error, not read-only and not an integrity anomaly. Storing the capability keeps all 57 `commit` sites unchanged; only `open` (57 sites) and `create` (32) move. **The scoping also falsified this row's first reading that the writer path was test-only:** `epiphany-textproj`'s `serialize_document` (`serialize.rs:119`) and `project.rs:936` are production paths that copy a base's `reduction_algorithm_version` verbatim into a fresh `SnapshotRef`, which `commit_versioned` then stamps into the superblock (`bundle.rs:798`) — so production mints self-consistent stale documents **without ever calling `open`**, and the capability must govern writers too. **What blocks it:** contract pin 2a. Baseline authority `0` does not preserve the corpus (`serialize.rs:327` stamps `1` and round-trips it; `vectors.rs:353`/`:363` likewise), and once P13-S16 moves the authority to `1`, a pre-S27 base that happens to carry `1` is **indistinguishable from a legitimately rebuilt one** — a raw `u32` carries no provenance. Four dispositions are recorded there; `FORMAT_MINOR` as a provenance carrier was proposed and **rejected** (the header never changes after creation, `core_spec.tex:10799`, so a legacy bundle committing a freshly validated base keeps its old minor forever; and a minor change may only append append-safe discriminants, `:12258`, not alter acceptance semantics). The surviving requirement — provenance must ride a container property **old readers cannot silently accept** and **a later commit cannot inherit unchanged** — is a format-epoch design, filed as **P13-S28**. **Scope of the claim, deliberately narrow:** this establishes that the **current implementation** has no detection mechanism. It does **not** establish that no reduction-semantics change in the project's history was ever detectable — that needs a history audit not yet done, and the stronger sentence is deliberately not written here. **What closing it requires:** an authority naming the semantics this build implements, and a rejection-or-rebuild path when a base disagrees with it. Until then any rung changing canonical reduction semantics can record its break in prose but cannot make stale bases unusable — which is why P13-S16's contract is complete, ratifiable as a plan, and **not dispatchable**. **Method note:** an earlier draft of S16's pin 0 claimed no writer path existed at all. That was false, and the way it was false is the point — the search behind it looked for `ReductionAlgorithmVersion(` constructor calls, which cannot find a path that propagates an existing value without constructing one. The instrument could not observe the thing it was used to rule out | -| P13-S28 | **No container property distinguishes a document produced under a validated reduction authority from one produced before any authority existed — and the two candidates that look like they would, cannot.** P13-S27 installs an authority and validates it at read and write time, but cannot state what to do with a canonical base that predates the authority: a raw `ReductionAlgorithmVersion` is a bare `u32` (`bundle/src/ids.rs:291`) carrying no provenance, and the text-projection parser accepts an unbounded one from a document (`textproj/src/parse.rs:591`), so no numeric convention — including a deliberately high epoch — is safe from a hand-authored or third-party document declaring it. **`FORMAT_MINOR` does not work either, for two independent reasons:** the header *"never changes after the file is created"* (`core_spec.tex:10799`–`:10800`) and `commit_versioned` publishes only a superblock (`bundle.rs:791`), so a legacy bundle that commits a base S27 just validated keeps its old minor **permanently** — rejecting minor-≤1 bases would then reject a base the authority itself accepted, and accepting them leaves S16's `1` ambiguous; and `core_spec.tex:12258`–`:12262` limits a minor change to appending append-safe discriminants and calls it backward-compatible, whereas making a previously-valid base newly rejectable is a **semantic acceptance change**, with current readers ignoring minor entirely (`header.rs:119` gates on major alone) so the boundary would bind only readers that already comply. **The requirement that survives:** provenance MUST ride a container property that **old readers cannot silently accept** and that **a later commit cannot inherit unchanged** | `spec/CONTRACT_P13S27_REDUCTION_AUTHORITY.md` pin 2a (filed 2026-07-31; the disposition S27 cannot make from inside itself) | **open. The critical path — P13-S27 and P13-S16 are both blocked on it.** **This rung must own all five, and none may be deferred into S27:** (1) an **old-reader rejection boundary** — pre-boundary readers must fail closed rather than silently open a document whose safety check they do not run; (2) **provenance that survives commits correctly**, i.e. is not inherited unchanged by a later generation and is not lost by one; (3) **legacy-base rebuild/repack behaviour**, stated for real artifacts rather than assumed away; (4) **every writer path, including text projection** — `serialize_document`, `project.rs`, and the committed `.txt` vectors, since a text document can declare any version; (5) **the exact format-version and compatibility consequences**, most plausibly a **major**-version boundary or a generation-scoped attestation paired with an incompatibility boundary. **Not a sub-pin of S27 and must not drift into it** — S27's pin 2a carries an explicit prohibition against being amended into a disposition without its own ratification round. **Scoped and RATIFIED 2026-07-31 as `spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md`** after four adversarial review rounds — 11 pins, 11 tests, 11 mutations, 15 touch rows, 7 gate items. **This row is now a dependency record only; the work lives there and P13-S28 does not execute as a Pass 13 rung.** Rulings taken: the carrier is the **format major** (`FORMAT_MAJOR` 0 → 1, `FORMAT_MINOR` 1 → 0), decoded three ways through a named `FormatEpoch` rather than a bool, with **no** generation-scoped attestation in this epoch; legacy resolves to **hard rejection, not read-only**; and an eight-row epoch matrix in which a major-0 bundle with no base may open, one carrying a base is rejected, and one attempting to *add* a base is rejected and told to repack — **the non-inheritance rule that `FORMAT_MINOR` could not express**. All five things this row required the rung to own are pinned: old-reader boundary (pin 2), commit-surviving provenance (pin 3), legacy repack (pins 4, 5), every writer path including text projection (pins 3b, 6), and the exact format/compatibility consequences (pins 1, 7). **Three findings from the review rounds that changed the rung's shape**, none of them visible at filing: (1) **it cannot stamp major 1 before S27's writer enforcement exists**, so pin 3a temporarily refuses *both* boundaries — opening a major-1 bundle already carrying a base, and committing one into it — through a third, temporary `ReductionAuthorityUnavailable` error that must name P13-S27 and must **not** name repack; (2) **text projection launders provenance straight through the boundary** (`serialize_document` stages a carried base into a fresh bundle and `build_manifest` writes it), resolved as **symmetric document-level refusal** — projection, parsing and a new dedicated `SerializeError` variant, none of which existed to be "retained" — which forces `COMPANION_VERSION` 0.13.0 → **0.14.0** and rebuilds the committed corpus to **20 vectors, ten rejection classes, `canonical_bases` reach 2 → 0**, a real and stated capability loss; (3) **corruption precedence binds in both epochs** — a corrupt major-1 base must still fail as malformed, never as the *temporary* authority error a user would reasonably retry. **S27 and S16 remain blocked** until this rung is implemented; S27's contract is a mandatory touch of it (pin 8 resolves S27's open pin 2a: legacy bases are refused by container epoch, never by version arithmetic) | +| P13-S27 | **The reduction-algorithm-version machinery is self-referential, so the one check that would detect a canonical-semantics change necessarily passes.** `core_spec.tex:11614`–`:11617` is normative — *"Snapshots produced under an earlier algorithm version cannot be used as canonical bases under a later one without rebuilding"* — and `:14369`–`:14372` states that replicas at differing versions *"may produce different canonical states from the same operation set."* The machinery to enforce it appears to exist: `ReductionAlgorithmVersion` (`bundle/src/ids.rs:291`) is a superblock wire field (bytes `68..72`, `superblock.rs:20`); `reduction_version_for` (`bundle.rs:989`) sets a new superblock's value; and `open` (`bundle.rs:396`–`:399`) rejects a mismatch. **But the writer sources the value from the canonical base's own self-report** (mapping the base's `reduction_algorithm_version` through `unwrap_or_default()`), **and the reader compares it only against the superblock that value seeded.** Nothing compares either against the semantics the running implementation actually implements. **The check is not vacuous** — it catches a corrupt or tampered base whose version disagrees with its superblock — but it **necessarily passes for a conformingly propagated stale base**, which is precisely the case the requirement exists to prevent. Supporting: **no constant or accessor anywhere names the implementation's current reduction semantics**, and `ids.rs:288`–`:289` states that *"the algorithm catalog itself lives in `epiphany-ops`"* while nothing of the kind exists in that crate — **a second instance of P13-S26's pattern**, a doc comment asserting a false fact about another module | `spec/CONTRACT_P13S16_PROJECTION.md` pin 0 (found 2026-07-31 while scoping P13-S16, which is a canonical reduction-semantics change and therefore the first rung to need this guarantee; filed in the same ledger edit as the row it blocks) | **open, BLOCKED on P13-S28, and blocking P13-S16.** **Scoped 2026-07-31 as `spec/CONTRACT_P13S27_REDUCTION_AUTHORITY.md` (DRAFT, not dispatchable).** Rulings taken: a typed `BundleCapabilities` required at both `Bundle::open` and `Bundle::create` and carried on the `Bundle` — no default, so every caller states the semantics it implements — and outright rejection on mismatch via a new `CanonicalBaseRequiresRebuild` error, not read-only and not an integrity anomaly. Storing the capability keeps all 57 `commit` sites unchanged; only `open` (57 sites) and `create` (32) move. **The scoping also falsified this row's first reading that the writer path was test-only:** `epiphany-textproj`'s `serialize_document` (`serialize.rs:119`) and `project.rs:936` are production paths that copy a base's `reduction_algorithm_version` verbatim into a fresh `SnapshotRef`, which `commit_versioned` then stamps into the superblock (`bundle.rs:798`) — so production mints self-consistent stale documents **without ever calling `open`**, and the capability must govern writers too. **What blocks it:** contract pin 2a. Baseline authority `0` does not preserve the corpus (`serialize.rs:327` stamps `1` and round-trips it; `vectors.rs:353`/`:363` likewise), and once P13-S16 moves the authority to `1`, a pre-S27 base that happens to carry `1` is **indistinguishable from a legitimately rebuilt one** — a raw `u32` carries no provenance. Four dispositions are recorded there; `FORMAT_MINOR` as a provenance carrier was proposed and **rejected** (the header never changes after creation, `core_spec.tex:10799`, so a legacy bundle committing a freshly validated base keeps its old minor forever; and a minor change may only append append-safe discriminants, `:12258`, not alter acceptance semantics). The surviving requirement — provenance must ride a container property **old readers cannot silently accept** and **a later commit cannot inherit unchanged** — is a format-epoch design, filed as **P13-S28**. **Scope of the claim, deliberately narrow:** this establishes that the **current implementation** has no detection mechanism. It does **not** establish that no reduction-semantics change in the project's history was ever detectable — that needs a history audit not yet done, and the stronger sentence is deliberately not written here. **What closing it requires:** an authority naming the semantics this build implements, and a rejection-or-rebuild path when a base disagrees with it. Until then any rung changing canonical reduction semantics can record its break in prose but cannot make stale bases unusable — which is why P13-S16's contract is complete, ratifiable as a plan, and **not dispatchable**. **Method note:** an earlier draft of S16's pin 0 claimed no writer path existed at all. That was false, and the way it was false is the point — the search behind it looked for `ReductionAlgorithmVersion(` constructor calls, which cannot find a path that propagates an existing value without constructing one. The instrument could not observe the thing it was used to rule out. **UNBLOCKED 2026-08-07:** the format-epoch rung landed and its pin 8 **resolves pin 2a** — reduction-version authority is meaningful only in major-1 containers, so legacy bases are refused by container epoch and never by version arithmetic. The collision pin 2a identified never has to be adjudicated: a pre-S27 base carrying `1` and a rebuilt S16 base carrying `1` are indistinguishable as numbers but can never meet, because the former exists only in a major-0 container, refused at the epoch boundary before any version is compared. The `u32` never has to carry provenance because the container does. **S27 now additionally owes three inherited items** (both interim refusals converted to validation, M8's deferred laundering demonstration, pin 3c's two suspended conformance assertions), recorded in its contract as required tests | +| P13-S28 | **No container property distinguishes a document produced under a validated reduction authority from one produced before any authority existed — and the two candidates that look like they would, cannot.** P13-S27 installs an authority and validates it at read and write time, but cannot state what to do with a canonical base that predates the authority: a raw `ReductionAlgorithmVersion` is a bare `u32` (`bundle/src/ids.rs:291`) carrying no provenance, and the text-projection parser accepts an unbounded one from a document (`textproj/src/parse.rs:591`), so no numeric convention — including a deliberately high epoch — is safe from a hand-authored or third-party document declaring it. **`FORMAT_MINOR` does not work either, for two independent reasons:** the header *"never changes after the file is created"* (`core_spec.tex:10799`–`:10800`) and `commit_versioned` publishes only a superblock (`bundle.rs:791`), so a legacy bundle that commits a base S27 just validated keeps its old minor **permanently** — rejecting minor-≤1 bases would then reject a base the authority itself accepted, and accepting them leaves S16's `1` ambiguous; and `core_spec.tex:12258`–`:12262` limits a minor change to appending append-safe discriminants and calls it backward-compatible, whereas making a previously-valid base newly rejectable is a **semantic acceptance change**, with current readers ignoring minor entirely (`header.rs:119` gates on major alone) so the boundary would bind only readers that already comply. **The requirement that survives:** provenance MUST ride a container property that **old readers cannot silently accept** and that **a later commit cannot inherit unchanged** | `spec/CONTRACT_P13S27_REDUCTION_AUTHORITY.md` pin 2a (filed 2026-07-31; the disposition S27 cannot make from inside itself) | **open. The critical path — P13-S27 and P13-S16 are both blocked on it.** **This rung must own all five, and none may be deferred into S27:** (1) an **old-reader rejection boundary** — pre-boundary readers must fail closed rather than silently open a document whose safety check they do not run; (2) **provenance that survives commits correctly**, i.e. is not inherited unchanged by a later generation and is not lost by one; (3) **legacy-base rebuild/repack behaviour**, stated for real artifacts rather than assumed away; (4) **every writer path, including text projection** — `serialize_document`, `project.rs`, and the committed `.txt` vectors, since a text document can declare any version; (5) **the exact format-version and compatibility consequences**, most plausibly a **major**-version boundary or a generation-scoped attestation paired with an incompatibility boundary. **Not a sub-pin of S27 and must not drift into it** — S27's pin 2a carries an explicit prohibition against being amended into a disposition without its own ratification round. **Scoped and RATIFIED 2026-07-31 as `spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md`** after four adversarial review rounds — 11 pins, 11 tests, 11 mutations, 15 touch rows, 7 gate items. **This row is now a dependency record only; the work lives there and P13-S28 does not execute as a Pass 13 rung.** Rulings taken: the carrier is the **format major** (`FORMAT_MAJOR` 0 → 1, `FORMAT_MINOR` 1 → 0), decoded three ways through a named `FormatEpoch` rather than a bool, with **no** generation-scoped attestation in this epoch; legacy resolves to **hard rejection, not read-only**; and an eight-row epoch matrix in which a major-0 bundle with no base may open, one carrying a base is rejected, and one attempting to *add* a base is rejected and told to repack — **the non-inheritance rule that `FORMAT_MINOR` could not express**. All five things this row required the rung to own are pinned: old-reader boundary (pin 2), commit-surviving provenance (pin 3), legacy repack (pins 4, 5), every writer path including text projection (pins 3b, 6), and the exact format/compatibility consequences (pins 1, 7). **Three findings from the review rounds that changed the rung's shape**, none of them visible at filing: (1) **it cannot stamp major 1 before S27's writer enforcement exists**, so pin 3a temporarily refuses *both* boundaries — opening a major-1 bundle already carrying a base, and committing one into it — through a third, temporary `ReductionAuthorityUnavailable` error that must name P13-S27 and must **not** name repack; (2) **text projection launders provenance straight through the boundary** (`serialize_document` stages a carried base into a fresh bundle and `build_manifest` writes it), resolved as **symmetric document-level refusal** — projection, parsing and a new dedicated `SerializeError` variant, none of which existed to be "retained" — which forces `COMPANION_VERSION` 0.13.0 → **0.14.0** and rebuilds the committed corpus to **20 vectors, ten rejection classes, `canonical_bases` reach 2 → 0**, a real and stated capability loss; (3) **corruption precedence binds in both epochs** — a corrupt major-1 base must still fail as malformed, never as the *temporary* authority error a user would reasonably retry. **IMPLEMENTED 2026-08-07** — amended once before dispatch (pin 3c, touch rows 10/11, gate 8) after reconnaissance found pin 3a's refusals reaching a conformance criterion through a file the touch table did not carry. All 11 tests landed under their contract names, all 11 mutations run and observed, workspace green at 1569. **P13-S27 is unblocked and P13-S16 remains blocked on S27** — pin 8 resolved S27's open pin 2a (legacy bases are refused by container epoch, never by version arithmetic), and S27 additionally inherits three obligations recorded in its own contract: converting **both** interim refusals to validation, M8's deferred laundering demonstration, and pin 3c's two suspended conformance assertions. **Two touch-table gaps found during execution, both of the same shape** — a `.tex` requirement addition moves hardcoded counts in `testkit/tests/requirement_labels.rs`, and a companion-version bump moves a second normative version literal spelled `version~0.13.0` rather than `(0 13 0)`; neither file was in any touch table, and the second was caught only because `requirements_name_only_this_companion_version` exists | diff --git a/spec/binary_format.pdf b/spec/binary_format.pdf index 9b10fdf..7a05fe0 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 5224bea..adf4ceb 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.16.0 --- The genesis operation tranche closes: \texttt{CreateMeasure} (G3b)}\\[4pt] + {\normalsize\color{epiphanyink}Version 0.17.0 --- The container format major becomes 1: the header carries an epoch}\\[4pt] {\small\color{epiphanyslate}Normative for the byte layouts it defines} \vfill \end{titlepage} @@ -1805,10 +1805,18 @@ format major version. All integers little-endian. \endhead \tablenums{0..8} & magic & \texttt{MUSCBND\textbackslash0} (8 bytes, ASCII, trailing NUL) \\ - \tablenums{8..10} & \texttt{format\_major} (\texttt{u16}) & \tablenums{0} - in this version; readers reject other values \\ - \tablenums{10..12} & \texttt{format\_minor} (\texttt{u16}) & \tablenums{1} - written in this version \\ + \tablenums{8..10} & \texttt{format\_major} (\texttt{u16}) & \tablenums{1} + written in this version. The container \emph{epoch}: a major-1 + container is one whose every base-bearing commit was validated + against a supplied reduction authority. \tablenums{0} is + \emph{legacy} and \MUST{} be decoded deliberately as such, not + rejected; any other value is rejected. A legacy container \MAY{} + be opened only while it carries no canonical base, and \MUSTNOT{} + acquire one --- the epoch is fixed at creation and cannot be + inherited by a later commit (Core Specification, + \sectionsc{The Container Epoch}) \\ + \tablenums{10..12} & \texttt{format\_minor} (\texttt{u16}) & \tablenums{0} + written in this version; minor numbering restarts at a new major \\ \tablenums{12..16} & \texttt{header\_length} (\texttt{u32}) & \tablenums{64}; readers reject other values \\ \tablenums{16..24} & \texttt{superblock\_a\_offset} (\texttt{u64}) & @@ -3715,6 +3723,28 @@ only}: implementations need not agree on an error taxonomy. ladder: G1 $\rightarrow$ G2a $\rightarrow$ G-minor $\rightarrow$ G2b $\rightarrow$ G3a $\rightarrow$ G3b. Semantics: Operation Catalog \sectionsc{CreateMeasure}, 0.13.0. \\ + + \today & The Fixed Header & 0.17.0 --- The container format major becomes + \tablenums{1}, and \texttt{format\_minor} restarts at \tablenums{0} + (\texttt{spec/CONTRACT\_FORMAT\_EPOCH\_MAJOR1.md}). This is the first + \emph{container} major boundary, and it is not a change of wire layout: no + field moves, no offset changes, and every byte range in the header table is + what it was. What changes is what the major \emph{means}. A major-1 container + is one whose every base-bearing commit was validated against a supplied + reduction authority; major \tablenums{0} is legacy and \MUST{} be decoded + deliberately as such rather than rejected. + + The major is the carrier precisely because the header never changes after + creation. A superblock field could not do this job: a legacy container that + later commits a validated base would keep carrying whatever its creation + stamped, so provenance would be inheritable by a document that never earned + it. A minor bump could not do it either --- readers ignore minor, and this + document's own rules limit a minor change to appending append-safe + discriminants, whereas making a previously-valid canonical base newly + rejectable is a semantic acceptance change. The full epoch matrix, including + the rule that a legacy container may open but \MUSTNOT{} acquire a canonical + base, is normative in the Core Specification, + \sectionsc{The Container Epoch}. \\ \bottomrule \end{longtable} diff --git a/spec/core_spec.pdf b/spec/core_spec.pdf index bf3ceaa..b1ea413 100644 Binary files a/spec/core_spec.pdf and b/spec/core_spec.pdf differ diff --git a/spec/core_spec.tex b/spec/core_spec.tex index 292b813..28fd1ab 100644 --- a/spec/core_spec.tex +++ b/spec/core_spec.tex @@ -10868,6 +10868,65 @@ pub struct FixedHeader { \texttt{file\_uuid}. \end{requirement} +\subsubsection{The Container Epoch} +\label{sec:format:container-epoch} + +The \texttt{format\_major} field is not only a wire-layout +discriminator. It is the container's \emph{epoch}: the statement of +which guarantees held at the moment the file was created. Because the +header never changes after creation, the epoch is a property of the +file's creation and cannot drift as the document is committed to --- +which is precisely what makes it able to carry a provenance guarantee +that a later generation must not be able to inherit. + +Major \tablenums{1} is the first epoch to use that capacity. A major-1 +container is one whose every base-bearing commit was validated against +a supplied reduction authority; a major-0 container carries no such +guarantee, and no sequence of commits can promote one into the other. + +\begin{requirement} + \label{req:format:container-epoch} + A reader \MUST{} classify \texttt{format\_major} three ways rather + than testing it for equality with the version it writes: + \tablenums{0} is \emph{legacy} and \MUST{} be decoded deliberately as + such; \tablenums{1} is \emph{current}; any other value \MUST{} be + rejected as an unsupported format version. The classification + \MUST{} be carried as a value the reader can act on, not re-derived + by comparison at each use site. + + Behaviour is then determined by the container's epoch together with + the presence of a canonical base: + + \begin{itemize} + \item \emph{Legacy, no canonical base} --- the bundle \MAY{} be + opened. Nothing whose validity cannot be established is exposed. + \item \emph{Legacy, canonical base present} --- the bundle + \MUST{} be refused. A base predating any reduction authority is + not safe materialized state, and \MUSTNOT{} be served in a + read-only or otherwise restricted mode: a restricted view of + unverifiable canonical state is still unverifiable canonical + state, presented confidently. + \item \emph{Legacy, a commit attempts to add or replace a + canonical base} --- the commit \MUST{} be refused, and the + implementation \MUST{} direct the writer to repack into a fresh + major-1 container. This is the rule that makes the epoch + non-inheritable, and it is the reason a header field can carry + this guarantee where a superblock field could not. + \item \emph{Current, no canonical base} --- the bundle \MAY{} be + opened, and \MAY{} be committed to. + \item \emph{Current, canonical base present or introduced} --- + the base \MUST{} be validated against the reduction authority + the opening implementation supplies. + \end{itemize} + + Where a container's canonical base disagrees with its active + superblock, that disagreement is \emph{corruption} and \MUST{} be + reported as a malformed bundle \emph{before} any epoch rule above is + applied, in either epoch. Ordering the epoch rules first would + collapse tampering into staleness and erase the distinction the + reduction authority depends on. +\end{requirement} + \subsection{The Superblock Slots} Each superblock slot is exactly 256 bytes. The bundle has two slots @@ -12229,6 +12288,18 @@ pub struct SchemaVersion { Schema versioning rules: \begin{itemize} + \item These rules govern \emph{schema} versions --- the manifest's + \texttt{manifest\_schema\_version} and each chunk's + \texttt{schema\_version}. The container's + \texttt{format\_major} is a separate axis, and a change to it + now means more than a change of wire layout: it declares a new + container \emph{epoch}, with guarantees that a reader may rely + on and that no later commit can confer on a container created + before them (Section~\ref{sec:format:container-epoch}). A + container format major boundary is therefore not satisfied by + re-encoding bytes; a document produced under an older epoch + acquires the new epoch's guarantees only by being repacked into + a newly created container. \item Major version changes are non-backward-compatible. Readers that do not support the chunk's major schema version \MUST{} handle it according to the chunk's role: @@ -16720,6 +16791,35 @@ layouts they own versus inherit: metadata: it enters no content hash and no canonical encoding, so no wire form and no companion version moves. \\ + \midrule + \today & The Fixed Header (container epoch) & + Chapter~8 gains \sectionsc{The Container Epoch} and + \ref{req:format:container-epoch} + (\texttt{spec/CONTRACT\_FORMAT\_EPOCH\_MAJOR1.md}). + \texttt{format\_major} becomes \tablenums{1} and \texttt{format\_minor} + restarts at \tablenums{0}. A reader classifies the major three ways --- + legacy \tablenums{0}, current \tablenums{1}, unsupported otherwise --- + and carries the classification as a value rather than re-deriving it by + comparison, so a legacy container is decoded deliberately instead of + refused. + + The epoch exists because a canonical base is only meaningful when it has + been validated against a reduction authority, and nothing in a document + could previously attest that: the reduction-algorithm version is a bare + integer that a writer copies from the base's own self-report, so the one + check that would detect a stale base necessarily passes for a base that + was propagated conformingly. The header can attest it because the header + never changes after creation --- which makes the epoch a property of the + file's creation that no later commit can confer. A legacy container may + therefore open while it carries no base, \MUSTNOT{} acquire one, and is + refused outright if it already has one; a repack into a freshly created + major-1 container is the only path forward, and read-only degradation is + explicitly not offered, since a restricted view of unverifiable canonical + state is still unverifiable canonical state. Corruption keeps precedence + over every epoch rule in both epochs, so tampering is never reported as + staleness. \sectionsc{Schema Versioning} gains the corresponding + distinction between the schema axis and the container axis. + \\ \bottomrule \end{longtable} diff --git a/spec/text_projection.pdf b/spec/text_projection.pdf index 574ebe2..2739771 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 dc6c0ef..407d476 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.13.0 --- The genesis ladder closes: \texttt{create-measure} reaches the grammar}\\[4pt] + {\normalsize\color{epiphanyink}Version 0.14.0 --- Canonical bases leave the companion: the container epoch carries what text cannot}\\[4pt] {\small\color{epiphanyslate}Normative for the text form it defines} \vfill \end{titlepage} @@ -483,7 +483,7 @@ A projection is, in order: \begin{requirement} \label{req:textproj:header-version} A parser implementing this companion \MUST{} accept exactly one header - version: \texttt{(0 13 0)}, the version of the companion it implements. It + version: \texttt{(0 14 0)}, the version of the companion it implements. It \MUST{} reject any other version at line one. Multi-version acceptance and text migrate-on-read are deferred in the same @@ -534,7 +534,7 @@ projection introduces no ordering of its own. A parser \MUST{} reject every \texttt{(blob ...)} line whose blob is unreferenced by canonical state (Requirement~\ref{req:textproj:canonical-blobs}). At companion - version~0.13.0, neither a canonical operation nor canonical reduced state can + version~0.14.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} @@ -906,6 +906,31 @@ are ratified. whose canonical document semantics are identical to the original's. The bundle's physical layout, chunking, and compression \MAY{} differ. + \textbf{Documents carrying a canonical base are outside this requirement's + domain, and are refused rather than round-tripped.} The exclusion is stated + here, in the requirement's own terms, because the equations below quantify + universally and a reader checking them against an implementation would + otherwise find a conforming implementation failing them. The companion + \MUST{} refuse a base-bearing document at every one of its three boundaries: + projecting a base-bearing bundle to text, parsing text that declares a + canonical base, and serializing a directly constructed document that carries + one. + + The reason is that this format cannot carry the guarantee the base needs. A + canonical base is only meaningful when it has been validated against a + reduction authority, and that validation is attested by the \emph{container} + epoch (Core Specification, \sectionsc{The Container Epoch}), which a text + document has no way to hold: every field a text format defines can be typed + by hand, so any provenance marker it carried would reduce to trusting its + author. Serializing a base-bearing text document into a freshly created + container would therefore mint a container asserting a validation that never + occurred. A canonical base enters a container only through an explicit + rebuild or repack flow, never through text import. + + This is a real loss of capability and is stated as one: a base-bearing bundle + does not round-trip through text. The quantifiers below range over documents + that carry no canonical base. + Equivalently, and more usefully to an implementer: for every bundle $B$, \[ \textrm{semantics}(\textrm{parse}(\textrm{project}(B))) = @@ -1140,13 +1165,12 @@ an example that cannot be parsed teaches the wrong lesson. The conformance vecto that pin \emph{real} bytes are a deliverable of the implementation. A document of one operation --- a transposition of two pitches up a perfect -fifth, over a compacted base --- projects to five lines: +fifth --- projects to four lines: \begin{lstlisting} -(text-projection (0 13 0)) +(text-projection (0 14 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)))) \end{lstlisting} @@ -1155,6 +1179,19 @@ The envelope's targets are a \emph{set}: strictly increasing, no duplicates $\mathrm{seq}^{\Uparrow}$). A parser \MUST{} reject a duplicate rather than absorb it, exactly as the binary decoder does. +Earlier revisions of this companion showed the same document over a compacted +base, as five lines. The fifth line is retained here as the spelling a parser +now \emph{refuses}: + +\begin{lstlisting} +(canonical-base #x1f8b0000000000000000000000000000 #x 1 full (schema 0 1) #x0000) +\end{lstlisting} + +The grammar still defines the section --- it is what a refusal is defined +against, and what a future rebuild or repack flow will emit --- but no document +containing it parses, projects, or serializes +(\texttt{req:textproj:roundtrip}). + % =========================================================================== \chapter{Revision History} \label{ch:history} @@ -1417,6 +1454,27 @@ absorb it, exactly as the binary decoder does. grammars both claiming \texttt{(0 12 0)}. Cached projections at \texttt{(0 12 0)} do not migrate; a stale \texttt{TextProjection} chunk is regenerated, not converted. \\ + + \today & Chapters 4, 5 & 0.14.0 --- Canonical bases leave the companion + (\texttt{spec/CONTRACT\_FORMAT\_EPOCH\_MAJOR1.md}, pin 3b). The container + format major becomes an \emph{epoch} attesting that every base-bearing commit + was validated against a reduction authority (Core Specification, + \sectionsc{The Container Epoch}); a text document cannot hold that attestation, + because every field this format defines can be typed by hand. Serializing a + base-bearing document into a freshly created container would therefore mint a + container asserting a validation that never happened. + \texttt{req:textproj:roundtrip} accordingly excludes base-bearing documents in + its own terms, and all three boundaries refuse them: projection, parsing, and + serialization. + + The first two of those refusals are \emph{new}; serialization had none to + retain. This is a real capability loss and is recorded as one --- a + base-bearing bundle does not round-trip through text until a rebuild or repack + flow exists. The forcing reason for the version bump is the usual one in + reverse: refusing a document the companion previously serialized is a semantic + change, and holding the version would leave two mutually incompatible readings + of \texttt{(0 13 0)}. The \texttt{canonical-base} production is retained in the + grammar --- a refusal is defined against it, and a repack flow will emit it. \\ \bottomrule \end{longtable} diff --git a/spec/vectors/textproj_document_vectors.txt b/spec/vectors/textproj_document_vectors.txt index dd36420..fd82980 100644 --- a/spec/vectors/textproj_document_vectors.txt +++ b/spec/vectors/textproj_document_vectors.txt @@ -22,22 +22,23 @@ # document bytes are normative. `` is lowercase with no separators. # textproj.document -textproj.document accept - minimal 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a -textproj.document accept - set_tuning_context 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830353035303530353035303530353035303530353035303530353035303530352028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303037202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203730302030202378303030303030303030303030303030313030303030303030303030303030303729202863617573616c2028292028292920282920287072696d697469766520287365742d74756e696e672d636f6e74657874202874756e696e672d636f6e746578742d73657474696e67732022636d6e2d31322220227465742d31322220287265666572656e63652d70697463682028636d6e2061203020342920237830303030303030303030663037623430292028736d75666c2d76657273696f6e2d726571756972656d656e742028736d75666c2d76657273696f6e2031203430292028736d75666c2d76657273696f6e20312034302929202829292929290a -textproj.document accept - lineage_custom_profile 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a -textproj.document accept - extension_base_two_envelopes 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a -textproj.document accept - rich_document 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a -textproj.document accept - create_staff_group 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830363036303630363036303630363036303630363036303630363036303630362028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303038202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203830302030202378303030303030303030303030303030313030303030303030303030303030303829202863617573616c2028292028292920282920287072696d697469766520286372656174652d73746166662d67726f7570202873746166662d67726f757020237830303030303030303030303030303031303030303030303030303030303030312028736f6d65202273746166662d67726f75702d312229206772616e642d737461666620282378303030303030303030303030303030313030303030303030303030303030303129292929290a -textproj.document accept - create_part_definition 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830373037303730373037303730373037303730373037303730373037303730372028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303039202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203930302030202378303030303030303030303030303030313030303030303030303030303030303929202863617573616c2028292028292920282920287072696d697469766520286372656174652d706172742d646566696e6974696f6e2028706172742d646566696e6974696f6e20237830303030303030303030303030303031303030303030303030303030303030312022706172742d312220282378303030303030303030303030303030313030303030303030303030303030303129292929290a -textproj.document accept - create_analysis_layer 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830383038303830383038303830383038303830383038303830383038303830382028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303061202378303030303030303030303030303030303030303030303030303030303030616220287374616d7020313030302030202378303030303030303030303030303030313030303030303030303030303030306129202863617573616c2028292028292920282920287072696d697469766520286372656174652d616e616c797369732d6c617965722028616e616c797369732d6c61796572202378303030303030303030303030303030313030303030303030303030303030303120226c617965722d3122292929290a -textproj.document accept - create_view 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830393039303930393039303930393039303930393039303930393039303930392028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303062202378303030303030303030303030303030303030303030303030303030303030616220287374616d7020313130302030202378303030303030303030303030303030313030303030303030303030303030306229202863617573616c2028292028292920282920287072696d697469766520286372656174652d766965772028766965772d646566696e6974696f6e20237830303030303030303030303030303031303030303030303030303030303030312022766965772d312220282378303030303030303030303030303030313030303030303030303030303030303129292929290a -textproj.document accept - create_measure 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830613061306130613061306130613061306130613061306130613061306130612028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303063202378303030303030303030303030303030303030303030303030303030303030616220287374616d7020313230302030202378303030303030303030303030303030313030303030303030303030303030306329202863617573616c2028292028292920282920287072696d697469766520286372656174652d6d656173757265202378303030303030303030303030303030313030303030303030303030303030303120286d6561737572652023783030303030303030303030303030303130303030303030303030303030303031202877616c6c2d636c6f636b2031303030303030303030292028736f6d652023783030303030303030303030303030303130303030303030303030303030303031292028736f6d65203129206175746f292929290a -textproj.document reject wrong-header-version superseded_companion_version 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a -textproj.document reject blob-line unreferenced_blob 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28626c6f6220226170706c69636174696f6e2f6f637465742d73747265616d222028292023783031290a -textproj.document reject out-of-order-sections canonical_base_before_extension 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a -textproj.document reject repeated-singular-section lineage_repeated 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a -textproj.document reject operation-envelope-order envelopes_reversed 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a -textproj.document reject profile-declaration-order profiles_reversed 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a -textproj.document reject extension-declaration-order extensions_reversed 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a -textproj.document reject extension-chunk-order extension_chunks_reversed 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a -textproj.document reject missing-trailing-lf final_lf_missing 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e20312028292074727565292929 +textproj.document accept - minimal 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a +textproj.document accept - set_tuning_context 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830353035303530353035303530353035303530353035303530353035303530352028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303037202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203730302030202378303030303030303030303030303030313030303030303030303030303030303729202863617573616c2028292028292920282920287072696d697469766520287365742d74756e696e672d636f6e74657874202874756e696e672d636f6e746578742d73657474696e67732022636d6e2d31322220227465742d31322220287265666572656e63652d70697463682028636d6e2061203020342920237830303030303030303030663037623430292028736d75666c2d76657273696f6e2d726571756972656d656e742028736d75666c2d76657273696f6e2031203430292028736d75666c2d76657273696f6e20312034302929202829292929290a +textproj.document accept - lineage_custom_profile 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a +textproj.document accept - extension_base_two_envelopes 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a +textproj.document accept - rich_document 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a +textproj.document accept - create_staff_group 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830363036303630363036303630363036303630363036303630363036303630362028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303038202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203830302030202378303030303030303030303030303030313030303030303030303030303030303829202863617573616c2028292028292920282920287072696d697469766520286372656174652d73746166662d67726f7570202873746166662d67726f757020237830303030303030303030303030303031303030303030303030303030303030312028736f6d65202273746166662d67726f75702d312229206772616e642d737461666620282378303030303030303030303030303030313030303030303030303030303030303129292929290a +textproj.document accept - create_part_definition 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830373037303730373037303730373037303730373037303730373037303730372028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303039202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203930302030202378303030303030303030303030303030313030303030303030303030303030303929202863617573616c2028292028292920282920287072696d697469766520286372656174652d706172742d646566696e6974696f6e2028706172742d646566696e6974696f6e20237830303030303030303030303030303031303030303030303030303030303030312022706172742d312220282378303030303030303030303030303030313030303030303030303030303030303129292929290a +textproj.document accept - create_analysis_layer 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830383038303830383038303830383038303830383038303830383038303830382028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303061202378303030303030303030303030303030303030303030303030303030303030616220287374616d7020313030302030202378303030303030303030303030303030313030303030303030303030303030306129202863617573616c2028292028292920282920287072696d697469766520286372656174652d616e616c797369732d6c617965722028616e616c797369732d6c61796572202378303030303030303030303030303030313030303030303030303030303030303120226c617965722d3122292929290a +textproj.document accept - create_view 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830393039303930393039303930393039303930393039303930393039303930392028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303062202378303030303030303030303030303030303030303030303030303030303030616220287374616d7020313130302030202378303030303030303030303030303030313030303030303030303030303030306229202863617573616c2028292028292920282920287072696d697469766520286372656174652d766965772028766965772d646566696e6974696f6e20237830303030303030303030303030303031303030303030303030303030303030312022766965772d312220282378303030303030303030303030303030313030303030303030303030303030303129292929290a +textproj.document accept - create_measure 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830613061306130613061306130613061306130613061306130613061306130612028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303063202378303030303030303030303030303030303030303030303030303030303030616220287374616d7020313230302030202378303030303030303030303030303030313030303030303030303030303030306329202863617573616c2028292028292920282920287072696d697469766520286372656174652d6d656173757265202378303030303030303030303030303030313030303030303030303030303030303120286d6561737572652023783030303030303030303030303030303130303030303030303030303030303031202877616c6c2d636c6f636b2031303030303030303030292028736f6d652023783030303030303030303030303030303130303030303030303030303030303031292028736f6d65203129206175746f292929290a +textproj.document reject wrong-header-version superseded_companion_version 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a +textproj.document reject blob-line unreferenced_blob 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28626c6f6220226170706c69636174696f6e2f6f637465742d73747265616d222028292023783031290a +textproj.document reject out-of-order-sections canonical_base_before_extension 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a +textproj.document reject repeated-singular-section lineage_repeated 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a +textproj.document reject operation-envelope-order envelopes_reversed 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a +textproj.document reject profile-declaration-order profiles_reversed 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a +textproj.document reject extension-declaration-order extensions_reversed 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a +textproj.document reject extension-chunk-order extension_chunks_reversed 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312929202378303120237830313032290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a +textproj.document reject missing-trailing-lf final_lf_missing 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e20312028292074727565292929 +textproj.document reject canonical-base-unsupported canonical_base_present 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830623062306230623062306230623062306230623062306230623062306230622028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2863616e6f6e6963616c2d62617365202378306230623062306230623062306230623062306230623062306230623062306220237830313032306220312066756c6c2028736368656d61203020312920237861303062290a