P13-S27: accept reduction authority implementation

This commit is contained in:
Levi Neuwirth 2026-08-09 15:05:58 +02:00
parent 83df0a01be
commit 4df8e257f3
No known key found for this signature in database
22 changed files with 1323 additions and 251 deletions

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,7 @@
//! bundle, not as errors. //! bundle, not as errors.
use crate::codec::DecodeError; use crate::codec::DecodeError;
use crate::ids::SchemaVersion; use crate::ids::{ReductionAlgorithmVersion, SchemaVersion};
use epiphany_determinism::ContentHash; use epiphany_determinism::ContentHash;
/// A hard bundle failure: the file is unopenable, or a canonical chunk is /// A hard bundle failure: the file is unopenable, or a canonical chunk is
@ -139,17 +139,42 @@ pub enum BundleError {
/// error MUST NOT degrade to a read-only open. /// error MUST NOT degrade to a read-only open.
LegacyBaseIntroductionRejected, LegacyBaseIntroductionRejected,
/// A major-1 container's canonical base cannot yet be validated: matrix /// A canonical base was produced under reduction semantics this build does
/// rows 5i/6i, opening a container that already carries a base, or /// not implement, so the materialized state it carries is **the wrong
/// committing one into it. The container is the **right** epoch — this is /// materialization** and must be rebuilt before use.
/// not a request to repack — but no reduction-authority capability exists ///
/// yet to validate the base against. **Temporary**: `P13-S27` supplies /// Raised on both boundaries a base can cross: `Bundle::open`, when a
/// that capability and replaces both branches with real validation, not /// container's base disagrees with the caller's
/// this categorical refusal. Never mentions repack (repacking a /// [`BundleCapabilities::current_reduction_version`], and
/// already-correct-epoch container would be wrong advice), and MUST NOT /// `commit`/`commit_versioned`, when a **newly emitted or replaced** base
/// degrade to a read-only open — a pre-authority base is not a /// does. Replaces the format rung's temporary
/// restricted-but-correct view. /// `ReductionAuthorityUnavailable`, which refused both boundaries
ReductionAuthorityUnavailable, /// categorically because no authority existed to validate against; P13-S27
/// supplies that authority, so refusal becomes validation.
///
/// # Not read-only, and not an integrity anomaly
///
/// A stale base is **not a restricted-but-correct view** — it is state
/// computed under different rules. Exposing it read-only would serve
/// incorrect canonical state confidently, which is worse than refusing.
///
/// # Why `open` cannot recover
///
/// **`open` cannot rebuild.** Drop-and-replay is unsound once pruning
/// exists (`core_spec.tex:12207`, `:14701` — specified, **not
/// implemented**; no `prune` appears anywhere in this crate), because the
/// operations needed to rebuild may no longer be present. A higher-level
/// rebuild path may be authorized later **only where full pre-base history
/// is demonstrably available**; this rung authorizes none.
///
/// [`BundleCapabilities::current_reduction_version`]:
/// crate::bundle::BundleCapabilities::current_reduction_version
CanonicalBaseRequiresRebuild {
/// The version the canonical base reports for itself.
base: ReductionAlgorithmVersion,
/// The version the caller stated this build implements.
current: ReductionAlgorithmVersion,
},
} }
impl core::fmt::Display for BundleError { impl core::fmt::Display for BundleError {
@ -229,9 +254,11 @@ impl core::fmt::Display for BundleError {
"cannot add or replace a canonical base in a legacy (format major 0) bundle; \ "cannot add or replace a canonical base in a legacy (format major 0) bundle; \
repack into a fresh major-1 bundle", repack into a fresh major-1 bundle",
), ),
BundleError::ReductionAuthorityUnavailable => f.write_str( BundleError::CanonicalBaseRequiresRebuild { base, current } => write!(
"this major-1 container's canonical base cannot yet be validated: no \ f,
reduction-authority capability exists until P13-S27 lands", "canonical base was produced under reduction algorithm version {} but this \
build implements {}; the base must be rebuilt before use",
base.0, current.0
), ),
} }
} }

View File

@ -35,7 +35,7 @@
//! bundle images by hand and asserts the Chapter 8 §"Superblock Selection" rule //! bundle images by hand and asserts the Chapter 8 §"Superblock Selection" rule
//! across every corruption scenario the QUICKSTART enumerates. //! across every corruption scenario the QUICKSTART enumerates.
use crate::bundle::{Bundle, CommitContext, StagedChunk, BODY_START}; use crate::bundle::{Bundle, BundleCapabilities, CommitContext, StagedChunk, BODY_START};
use crate::chunk::{ChunkKind, ChunkRef, CompressionAlgorithm}; use crate::chunk::{ChunkKind, ChunkRef, CompressionAlgorithm};
use crate::error::IntegrityAnomaly; use crate::error::IntegrityAnomaly;
use crate::header::FixedHeader; use crate::header::FixedHeader;
@ -101,14 +101,21 @@ fn staged_blocks(envelope_payloads: &[Vec<u8>]) -> Vec<StagedChunk> {
fn check_recovery(base_image: &[u8], base_gen: u64, chunks: &[StagedChunk], crash: CrashPoint) { fn check_recovery(base_image: &[u8], base_gen: u64, chunks: &[StagedChunk], crash: CrashPoint) {
// Open the bundle over a fault store; `open` only reads, so it never // Open the bundle over a fault store; `open` only reads, so it never
// consumes crash budget and always succeeds on a valid base image. // consumes crash budget and always succeeds on a valid base image.
let mut bundle = Bundle::open(FaultStore::new(base_image.to_vec(), crash)) let mut bundle = Bundle::open(
.expect("base image must open before the commit"); FaultStore::new(base_image.to_vec(), crash),
BundleCapabilities::synthetic_for_fixture(0),
)
.expect("base image must open before the commit");
let committed = bundle.commit(chunks, append_roots).is_ok(); let committed = bundle.commit(chunks, append_roots).is_ok();
let store = bundle.into_store(); let store = bundle.into_store();
// Recover: reopen from exactly the bytes that survived the crash. // Recover: reopen from exactly the bytes that survived the crash.
let durable = store.durable_image(); let durable = store.durable_image();
let recovered = Bundle::open(MemStore::from_bytes(durable.clone())).unwrap_or_else(|e| { let recovered = Bundle::open(
MemStore::from_bytes(durable.clone()),
BundleCapabilities::synthetic_for_fixture(0),
)
.unwrap_or_else(|e| {
panic!( panic!(
"crash at {crash:?} left an UNOPENABLE bundle (base gen {base_gen}): {e}\n\ "crash at {crash:?} left an UNOPENABLE bundle (base gen {base_gen}): {e}\n\
durable image length {}", durable image length {}",
@ -164,7 +171,13 @@ fn check_recovery(base_image: &[u8], base_gen: u64, chunks: &[StagedChunk], cras
fn build_base(rng: &mut SplitMix64, commits: u64) -> (Vec<u8>, u64) { fn build_base(rng: &mut SplitMix64, commits: u64) -> (Vec<u8>, u64) {
let doc = DocumentId([(rng.next_u64() & 0xff) as u8; 16]); let doc = DocumentId([(rng.next_u64() & 0xff) as u8; 16]);
let uuid = FileUuid([(rng.next_u64() & 0xff) as u8; 16]); let uuid = FileUuid([(rng.next_u64() & 0xff) as u8; 16]);
let mut bundle = Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).unwrap(); let mut bundle = Bundle::create(
MemStore::new(),
uuid,
Manifest::empty(doc),
BundleCapabilities::synthetic_for_fixture(0),
)
.unwrap();
for _ in 0..commits { for _ in 0..commits {
let n = rng.below(3) as usize; // 0..2 envelopes let n = rng.below(3) as usize; // 0..2 envelopes
let envelopes: Vec<Vec<u8>> = (0..n) let envelopes: Vec<Vec<u8>> = (0..n)
@ -289,7 +302,11 @@ pub fn run_wire_decode_fuzz(iters: u64, seed: u64) -> WireFuzzCoverage {
let mut manifests: Vec<Vec<u8>> = Vec::new(); let mut manifests: Vec<Vec<u8>> = Vec::new();
for commits in 0..4u64 { for commits in 0..4u64 {
let (image, _) = build_base(&mut rng, commits); let (image, _) = build_base(&mut rng, commits);
let bundle = Bundle::open(MemStore::from_bytes(image.clone())).expect("valid image opens"); let bundle = Bundle::open(
MemStore::from_bytes(image.clone()),
BundleCapabilities::synthetic_for_fixture(0),
)
.expect("valid image opens");
manifests.push(bundle.manifest().encode()); manifests.push(bundle.manifest().encode());
images.push(image); images.push(image);
} }
@ -347,7 +364,10 @@ pub fn run_wire_decode_fuzz(iters: u64, seed: u64) -> WireFuzzCoverage {
// 1. Whole-image open. Must never panic; an Ok manifest must re-encode. // 1. Whole-image open. Must never panic; an Ok manifest must re-encode.
let pick = (rng.next_u64() as usize) % images.len(); let pick = (rng.next_u64() as usize) % images.len();
let image = mutate_image(&mut rng, &images[pick]); let image = mutate_image(&mut rng, &images[pick]);
match Bundle::open(MemStore::from_bytes(image)) { match Bundle::open(
MemStore::from_bytes(image),
BundleCapabilities::synthetic_for_fixture(0),
) {
Ok(bundle) => { Ok(bundle) => {
cov.opens_ok += 1; cov.opens_ok += 1;
let encoded = bundle.manifest().encode(); let encoded = bundle.manifest().encode();
@ -532,10 +552,18 @@ pub fn exhaustive_crash_check(base_image: &[u8], base_gen: u64, envelope_payload
// Learn the commit's total syscall count (and confirm the clean commit // Learn the commit's total syscall count (and confirm the clean commit
// recovers to G+1) via a no-fault run. // recovers to G+1) via a no-fault run.
let total = { let total = {
let mut bundle = Bundle::open(FaultStore::no_fault(base_image.to_vec())).unwrap(); let mut bundle = Bundle::open(
FaultStore::no_fault(base_image.to_vec()),
BundleCapabilities::synthetic_for_fixture(0),
)
.unwrap();
bundle.commit(&chunks, append_roots).unwrap(); bundle.commit(&chunks, append_roots).unwrap();
let store = bundle.into_store(); let store = bundle.into_store();
let recovered = Bundle::open(store.recover()).unwrap(); let recovered = Bundle::open(
store.recover(),
BundleCapabilities::synthetic_for_fixture(0),
)
.unwrap();
assert_eq!(recovered.generation(), base_gen + 1); assert_eq!(recovered.generation(), base_gen + 1);
store.syscalls_issued() store.syscalls_issued()
}; };
@ -646,7 +674,8 @@ pub fn run_manifest_selection_harness() {
b.set_slot(Slot::A, &sb_a); b.set_slot(Slot::A, &sb_a);
b.set_slot(Slot::B, &sb_b); b.set_slot(Slot::B, &sb_b);
b.corrupt_slot(Slot::A); b.corrupt_slot(Slot::A);
let bundle = Bundle::open(b.store()).expect("slot B is valid; bundle must open"); let bundle = Bundle::open(b.store(), BundleCapabilities::synthetic_for_fixture(0))
.expect("slot B is valid; bundle must open");
assert_eq!(bundle.active_slot(), Slot::B); assert_eq!(bundle.active_slot(), Slot::B);
assert_eq!(bundle.generation(), 1); assert_eq!(bundle.generation(), 1);
assert!(bundle.anomalies().is_empty()); assert!(bundle.anomalies().is_empty());
@ -661,7 +690,8 @@ pub fn run_manifest_selection_harness() {
b.set_slot(Slot::A, &sb_a); b.set_slot(Slot::A, &sb_a);
b.set_slot(Slot::B, &sb_b); b.set_slot(Slot::B, &sb_b);
b.corrupt_slot(Slot::B); b.corrupt_slot(Slot::B);
let bundle = Bundle::open(b.store()).expect("slot A is valid; bundle must open"); let bundle = Bundle::open(b.store(), BundleCapabilities::synthetic_for_fixture(0))
.expect("slot A is valid; bundle must open");
assert_eq!(bundle.active_slot(), Slot::A); assert_eq!(bundle.active_slot(), Slot::A);
assert_eq!(bundle.generation(), 7); assert_eq!(bundle.generation(), 7);
assert!(bundle.anomalies().is_empty()); assert!(bundle.anomalies().is_empty());
@ -675,7 +705,7 @@ pub fn run_manifest_selection_harness() {
let sb_b = b.add_manifest(5, &m); let sb_b = b.add_manifest(5, &m);
b.set_slot(Slot::A, &sb_a); b.set_slot(Slot::A, &sb_a);
b.set_slot(Slot::B, &sb_b); b.set_slot(Slot::B, &sb_b);
let bundle = Bundle::open(b.store()).unwrap(); let bundle = Bundle::open(b.store(), BundleCapabilities::synthetic_for_fixture(0)).unwrap();
assert_eq!(bundle.generation(), 5); assert_eq!(bundle.generation(), 5);
assert_eq!(bundle.active_slot(), Slot::B); assert_eq!(bundle.active_slot(), Slot::B);
assert!(bundle.anomalies().is_empty()); assert!(bundle.anomalies().is_empty());
@ -688,7 +718,7 @@ pub fn run_manifest_selection_harness() {
let sb = b.add_manifest(9, &m); let sb = b.add_manifest(9, &m);
b.set_slot(Slot::A, &sb); b.set_slot(Slot::A, &sb);
b.set_slot(Slot::B, &sb); b.set_slot(Slot::B, &sb);
let bundle = Bundle::open(b.store()).unwrap(); let bundle = Bundle::open(b.store(), BundleCapabilities::synthetic_for_fixture(0)).unwrap();
assert_eq!(bundle.active_slot(), Slot::A); assert_eq!(bundle.active_slot(), Slot::A);
assert_eq!(bundle.generation(), 9); assert_eq!(bundle.generation(), 9);
assert!(bundle.anomalies().is_empty()); assert!(bundle.anomalies().is_empty());
@ -707,7 +737,7 @@ pub fn run_manifest_selection_harness() {
sb_b.generation = 9; sb_b.generation = 9;
b.set_slot(Slot::A, &sb_a); b.set_slot(Slot::A, &sb_a);
b.set_slot(Slot::B, &sb_b); b.set_slot(Slot::B, &sb_b);
let bundle = Bundle::open(b.store()).unwrap(); let bundle = Bundle::open(b.store(), BundleCapabilities::synthetic_for_fixture(0)).unwrap();
assert_eq!( assert_eq!(
bundle.anomalies(), bundle.anomalies(),
&[IntegrityAnomaly::DivergentSameGeneration { generation: 9 }] &[IntegrityAnomaly::DivergentSameGeneration { generation: 9 }]
@ -726,7 +756,7 @@ pub fn run_manifest_selection_harness() {
sb_b.generation = 9; sb_b.generation = 9;
b.set_slot(Slot::A, &sb_a); b.set_slot(Slot::A, &sb_a);
b.set_slot(Slot::B, &sb_b); b.set_slot(Slot::B, &sb_b);
let bundle = Bundle::open(b.store()).unwrap(); let bundle = Bundle::open(b.store(), BundleCapabilities::synthetic_for_fixture(0)).unwrap();
assert_eq!(bundle.generation(), 9); assert_eq!(bundle.generation(), 9);
assert_eq!( assert_eq!(
bundle.anomalies(), bundle.anomalies(),
@ -749,7 +779,7 @@ pub fn run_manifest_selection_harness() {
b.corrupt_slot(Slot::A); b.corrupt_slot(Slot::A);
b.corrupt_slot(Slot::B); b.corrupt_slot(Slot::B);
assert!(matches!( assert!(matches!(
Bundle::open(b.store()), Bundle::open(b.store(), BundleCapabilities::synthetic_for_fixture(0)),
Err(crate::BundleError::NoValidSuperblock) Err(crate::BundleError::NoValidSuperblock)
)); ));
} }
@ -764,7 +794,7 @@ pub fn run_manifest_selection_harness() {
sb_b.commit_state = CommitState::Reserved(1); sb_b.commit_state = CommitState::Reserved(1);
b.set_slot(Slot::A, &sb_a); b.set_slot(Slot::A, &sb_a);
b.set_slot(Slot::B, &sb_b); b.set_slot(Slot::B, &sb_b);
let bundle = Bundle::open(b.store()).unwrap(); let bundle = Bundle::open(b.store(), BundleCapabilities::synthetic_for_fixture(0)).unwrap();
// Slot B is not committed -> excluded; A (gen 3) is selected. The // Slot B is not committed -> excluded; A (gen 3) is selected. The
// non-committed slot is surfaced as an anomaly, but this is ordinary // non-committed slot is surfaced as an anomaly, but this is ordinary
// fallback (the next commit overwrites the bad slot), so the bundle is // fallback (the next commit overwrites the bad slot), so the bundle is
@ -787,7 +817,8 @@ pub fn run_manifest_selection_harness() {
sb_b.manifest_hash = manifest_chunk_hash(b"not the manifest"); sb_b.manifest_hash = manifest_chunk_hash(b"not the manifest");
b.set_slot(Slot::A, &sb_a); b.set_slot(Slot::A, &sb_a);
b.set_slot(Slot::B, &sb_b); b.set_slot(Slot::B, &sb_b);
let bundle = Bundle::open(b.store()).expect("slot A is valid"); let bundle = Bundle::open(b.store(), BundleCapabilities::synthetic_for_fixture(0))
.expect("slot A is valid");
assert_eq!(bundle.active_slot(), Slot::A); assert_eq!(bundle.active_slot(), Slot::A);
assert_eq!(bundle.generation(), 5); assert_eq!(bundle.generation(), 5);
} }

View File

@ -285,8 +285,22 @@ impl SchemaVersion {
/// The reduction-algorithm version that produced a canonical-base snapshot /// The reduction-algorithm version that produced a canonical-base snapshot
/// (Chapter 8): a snapshot may serve as a canonical base only if this matches /// (Chapter 8): a snapshot may serve as a canonical base only if this matches
/// the active superblock's value. Modeled as an opaque monotonically-versioned /// **the semantics the running build implements**, which the caller states via
/// `u32` (the algorithm catalog itself lives in `epiphany-ops`). /// `BundleCapabilities` at `open` and `create`.
///
/// Modeled as an opaque monotonically-versioned `u32`. **The authoritative
/// number lives in `epiphany-ops` as
/// `epiphany_ops::CURRENT_REDUCTION_ALGORITHM_VERSION`, a plain `u32`; this
/// wrapper type is constructed at the composition boundary** by whichever crate
/// depends on both. `epiphany-bundle` deliberately does **not** depend on
/// `epiphany-ops` — its only workspace dependency is `epiphany-determinism` —
/// so it cannot read that constant itself, which is exactly why the capability
/// is injected rather than looked up.
///
/// This doc comment previously claimed "the algorithm catalog itself lives in
/// `epiphany-ops`" while nothing of the kind existed there — a doc asserting a
/// false fact about another module, and as written **unimplementable from where
/// the check must run**. P13-S27 pin 8 is the rung that made it true.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub struct ReductionAlgorithmVersion(pub u32); pub struct ReductionAlgorithmVersion(pub u32);

View File

@ -67,8 +67,9 @@ pub use block::{
MAX_BLOCK_DEFAULT, MAX_BLOCK_DEFAULT,
}; };
pub use bundle::{ pub use bundle::{
manifest_chunk_hash, manifest_chunk_hash_versioned, Bundle, CommitContext, StagedChunk, manifest_chunk_hash, manifest_chunk_hash_versioned, Bundle, BundleCapabilities, CommitContext,
BODY_START, MAX_BLOB_BYTES, MAX_CHUNK_BYTES, MAX_MANIFEST_BYTES, SUPPORTED_SCHEMA_MAJOR, StagedChunk, BODY_START, MAX_BLOB_BYTES, MAX_CHUNK_BYTES, MAX_MANIFEST_BYTES,
SUPPORTED_SCHEMA_MAJOR,
}; };
pub use chunk::{ pub use chunk::{
chunk_content_hash, chunk_id, content_hash_for, ChunkKind, ChunkRef, CompressionAlgorithm, chunk_content_hash, chunk_id, content_hash_for, ChunkKind, ChunkRef, CompressionAlgorithm,

View File

@ -10,6 +10,7 @@
//! against an actual durable-flush primitive, not only the simulator. //! against an actual durable-flush primitive, not only the simulator.
use epiphany_bundle::fuzz::{exhaustive_crash_check, run_crash_recovery_fuzz, SplitMix64}; use epiphany_bundle::fuzz::{exhaustive_crash_check, run_crash_recovery_fuzz, SplitMix64};
use epiphany_bundle::BundleCapabilities;
use epiphany_bundle::{Bundle, DocumentId, FileUuid, Manifest, MemStore, StagedChunk}; use epiphany_bundle::{Bundle, DocumentId, FileUuid, Manifest, MemStore, StagedChunk};
/// The headline gate: 10,000 randomized crash scenarios. Every one must recover /// The headline gate: 10,000 randomized crash scenarios. Every one must recover
@ -51,8 +52,13 @@ fn exhaustive_sweep_across_base_states_and_commit_shapes() {
/// its image and generation. (Mirrors the fuzzer's own base builder.) /// its image and generation. (Mirrors the fuzzer's own base builder.)
fn build_base(rng: &mut SplitMix64, commits: u64) -> (Vec<u8>, u64) { fn build_base(rng: &mut SplitMix64, commits: u64) -> (Vec<u8>, u64) {
let doc = DocumentId([(rng.next_u64() & 0xff) as u8; 16]); let doc = DocumentId([(rng.next_u64() & 0xff) as u8; 16]);
let mut bundle = let mut bundle = Bundle::create(
Bundle::create(MemStore::new(), FileUuid([7; 16]), Manifest::empty(doc)).unwrap(); MemStore::new(),
FileUuid([7; 16]),
Manifest::empty(doc),
BundleCapabilities::synthetic_for_fixture(0),
)
.unwrap();
for i in 0..commits { for i in 0..commits {
let payload = epiphany_bundle::encode_block(&[vec![i as u8; 16]]); let payload = epiphany_bundle::encode_block(&[vec![i as u8; 16]]);
bundle bundle
@ -83,6 +89,7 @@ fn file_store_real_fsync_round_trip() {
store, store,
FileUuid([0xAB; 16]), FileUuid([0xAB; 16]),
Manifest::empty(DocumentId([1; 16])), Manifest::empty(DocumentId([1; 16])),
BundleCapabilities::synthetic_for_fixture(0),
) )
.unwrap(); .unwrap();
for i in 1..=2u64 { for i in 1..=2u64 {
@ -99,7 +106,11 @@ fn file_store_real_fsync_round_trip() {
} }
// Reopen from disk in a fresh handle: the committed state is durable. // Reopen from disk in a fresh handle: the committed state is durable.
let reopened = Bundle::open(FileStore::open(&path).unwrap()).unwrap(); let reopened = Bundle::open(
FileStore::open(&path).unwrap(),
BundleCapabilities::synthetic_for_fixture(0),
)
.unwrap();
assert_eq!(reopened.generation(), 2); assert_eq!(reopened.generation(), 2);
assert_eq!(reopened.manifest().operation_roots.len(), 2); assert_eq!(reopened.manifest().operation_roots.len(), 2);
reopened.verify_canonical_chunks().unwrap(); reopened.verify_canonical_chunks().unwrap();

View File

@ -9,6 +9,7 @@
//! real commit path. //! real commit path.
use epiphany_bundle::fuzz::run_manifest_selection_harness; use epiphany_bundle::fuzz::run_manifest_selection_harness;
use epiphany_bundle::BundleCapabilities;
use epiphany_bundle::{Bundle, DocumentId, FileUuid, Manifest, MemStore, Slot, StagedChunk}; use epiphany_bundle::{Bundle, DocumentId, FileUuid, Manifest, MemStore, Slot, StagedChunk};
#[test] #[test]
@ -25,6 +26,7 @@ fn commit_then_corrupt_active_slot_falls_back() {
MemStore::new(), MemStore::new(),
FileUuid([3; 16]), FileUuid([3; 16]),
Manifest::empty(DocumentId([4; 16])), Manifest::empty(DocumentId([4; 16])),
BundleCapabilities::synthetic_for_fixture(0),
) )
.unwrap(); .unwrap();
// Commit once: slot A holds gen 0, slot B holds gen 1 (active). // Commit once: slot A holds gen 0, slot B holds gen 1 (active).
@ -44,7 +46,11 @@ fn commit_then_corrupt_active_slot_falls_back() {
image[320 + 80] ^= 0xFF; image[320 + 80] ^= 0xFF;
// Recovery falls back to slot A (the previous generation), cleanly. // Recovery falls back to slot A (the previous generation), cleanly.
let recovered = Bundle::open(MemStore::from_bytes(image)).unwrap(); let recovered = Bundle::open(
MemStore::from_bytes(image),
BundleCapabilities::synthetic_for_fixture(0),
)
.unwrap();
assert_eq!(recovered.active_slot(), Slot::A); assert_eq!(recovered.active_slot(), Slot::A);
assert_eq!(recovered.generation(), 0); assert_eq!(recovered.generation(), 0);
assert!(recovered.anomalies().is_empty()); assert!(recovered.anomalies().is_empty());

View File

@ -104,6 +104,53 @@ pub mod valuegen;
pub mod fuzz; pub mod fuzz;
pub mod vectors; pub mod vectors;
/// The reduction semantics **this build implements**, as a bare number.
///
/// `core_spec.tex` §"Canonical Document Identity" is normative: *snapshots
/// produced under an earlier algorithm version cannot be used as canonical
/// bases under a later one without rebuilding*. Enforcing that needs a value
/// naming what the running implementation actually does — and before P13-S27
/// no such value existed anywhere. `ReductionAlgorithmVersion`
/// (`epiphany-bundle`) was a wire field whose reader compared it only against
/// the superblock that same value had seeded, so the check was a tautology for
/// every conformingly-written document.
///
/// # The bump discipline — this is the whole guarantee
///
/// **Any change to a canonical reduction verdict, or to canonical reduced
/// state, MUST bump this constant and record the change in the list below.**
///
/// **No mechanism can detect a semantics change.** A golden test over reduction
/// outputs can *prompt* the question — outputs moved, did semantics? — but it
/// can never answer it: a deliberate semantics change and an accidental
/// regression look identical from outside. **The discipline is the guarantee;
/// there is no backstop.**
///
/// # Why `0`, and why that is a decision
///
/// Bundles written to date carry `0` when they have no canonical base, and
/// bases self-report whatever they were stamped with. Starting anywhere but `0`
/// would make every existing base-bearing document fail to open **without any
/// semantics having changed** — the check would manufacture the breakage it
/// exists to detect. `0` is therefore a decision, **not "unset"**.
///
/// # Bumps
///
/// * `0` — the baseline. The semantics `canonical_reduction_order` and
/// `reduce_onto` implement as of P13-S27 (2026-08-08). No earlier version
/// exists; nothing predates this constant.
///
/// The first real bump belongs to **P13-S16**, which changes
/// `CreateStaffGroup`'s reduction verdict and must move this to `1`.
///
/// # Layering
///
/// This is a plain `u32`, and `epiphany-ops` **MUST NOT** gain a dependency on
/// `epiphany-bundle` in order to use that crate's `ReductionAlgorithmVersion`
/// wrapper. The wrapper is constructed at the composition boundary by whoever
/// depends on both (P13-S27 pin 1, §0.3).
pub const CURRENT_REDUCTION_ALGORITHM_VERSION: u32 = 0;
pub use anomaly::{ pub use anomaly::{
AnomalousReplicaSegment, IntegrityAnomaly, IntegrityAnomalyKind, ReplicaAnomalyReason, AnomalousReplicaSegment, IntegrityAnomaly, IntegrityAnomalyKind, ReplicaAnomalyReason,
}; };

View File

@ -124,8 +124,13 @@ fn build_fixture(dir: &Path) -> Fixture {
let uuid = FileUuid(rng.array16()); let uuid = FileUuid(rng.array16());
let doc = DocumentId(rng.array16()); let doc = DocumentId(rng.array16());
let mut bundle = let mut bundle = Bundle::create(
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create base bundle"); MemStore::new(),
uuid,
Manifest::empty(doc),
epiphany_testkit::production_caps(),
)
.expect("create base bundle");
bundle bundle
.commit(&staged_blocks(&envelopes), append_roots) .commit(&staged_blocks(&envelopes), append_roots)
.expect("commit base operation blocks"); .expect("commit base operation blocks");
@ -160,7 +165,11 @@ fn build_fixture(dir: &Path) -> Fixture {
/// Un-timed setup for the commit row: restore the base image and open it. /// Un-timed setup for the commit row: restore the base image and open it.
fn restore_and_open(path: &Path, image: &[u8]) -> Bundle<FileStore> { fn restore_and_open(path: &Path, image: &[u8]) -> Bundle<FileStore> {
fs::write(path, image).expect("restore base image"); fs::write(path, image).expect("restore base image");
Bundle::open(FileStore::open(path).expect("open store")).expect("open bundle") Bundle::open(
FileStore::open(path).expect("open store"),
epiphany_testkit::production_caps(),
)
.expect("open bundle")
} }
/// The timed commit: block append + manifest rewrite + superblock flip, fsync'd. /// The timed commit: block append + manifest rewrite + superblock flip, fsync'd.
@ -176,7 +185,11 @@ fn typical_edit_commit(mut bundle: Bundle<FileStore>, edit: &[StagedChunk]) -> u
/// `ChunkRef`; see `Fixture::base_root`'s doc comment for why it is not /// `ChunkRef`; see `Fixture::base_root`'s doc comment for why it is not
/// `manifest.canonical_base` right now) and every operation block. /// `manifest.canonical_base` right now) and every operation block.
fn open_bootstrap_read(path: &Path, base_root: &ChunkRef) -> usize { fn open_bootstrap_read(path: &Path, base_root: &ChunkRef) -> usize {
let bundle = Bundle::open(FileStore::open(path).expect("open store")).expect("open bundle"); let bundle = Bundle::open(
FileStore::open(path).expect("open store"),
epiphany_testkit::production_caps(),
)
.expect("open bundle");
let manifest = bundle.manifest(); let manifest = bundle.manifest();
let mut bytes = 0usize; let mut bytes = 0usize;
bytes += bundle bytes += bundle

View File

@ -81,8 +81,13 @@ fn staged(payloads: &[Vec<u8>]) -> Vec<StagedChunk> {
pub fn build_base(rng: &mut Rng, commits: u64) -> (Vec<u8>, u64) { pub fn build_base(rng: &mut Rng, commits: u64) -> (Vec<u8>, u64) {
let uuid = FileUuid(rng.array16()); let uuid = FileUuid(rng.array16());
let doc = DocumentId(rng.array16()); let doc = DocumentId(rng.array16());
let mut bundle = let mut bundle = Bundle::create(
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle"); MemStore::new(),
uuid,
Manifest::empty(doc),
crate::production_caps(),
)
.expect("create bundle");
for _ in 0..commits { for _ in 0..commits {
let n = rng.range_usize(0, 2); let n = rng.range_usize(0, 2);
let payloads: Vec<Vec<u8>> = (0..n).map(|_| rng.byte_vec(1, 40)).collect(); let payloads: Vec<Vec<u8>> = (0..n).map(|_| rng.byte_vec(1, 40)).collect();
@ -104,16 +109,20 @@ pub fn assert_recovers(
crash: CrashPoint, crash: CrashPoint,
) { ) {
let blocks = staged(envelope_payloads); let blocks = staged(envelope_payloads);
let mut bundle = Bundle::open(FaultStore::new(base_image.to_vec(), crash)) let mut bundle = Bundle::open(
.expect("the base image must open before the commit"); FaultStore::new(base_image.to_vec(), crash),
crate::production_caps(),
)
.expect("the base image must open before the commit");
let committed = bundle.commit(&blocks, append_roots).is_ok(); let committed = bundle.commit(&blocks, append_roots).is_ok();
let store = bundle.into_store(); let store = bundle.into_store();
// Recover from exactly the bytes that survived the crash. // Recover from exactly the bytes that survived the crash.
let durable = store.durable_image(); let durable = store.durable_image();
let recovered = Bundle::open(MemStore::from_bytes(durable)).unwrap_or_else(|e| { let recovered = Bundle::open(MemStore::from_bytes(durable), crate::production_caps())
panic!("crash at {crash:?} left an UNOPENABLE bundle (base gen {base_gen}): {e}") .unwrap_or_else(|e| {
}); panic!("crash at {crash:?} left an UNOPENABLE bundle (base gen {base_gen}): {e}")
});
let g = recovered.generation(); let g = recovered.generation();
assert!( assert!(
@ -159,10 +168,14 @@ pub fn exhaustive_crash_sweep(base_image: &[u8], base_gen: u64, envelope_payload
// Learn the commit's total syscall count via a no-fault run (and confirm the // Learn the commit's total syscall count via a no-fault run (and confirm the
// clean commit reaches G+1). // clean commit reaches G+1).
let total = { let total = {
let mut bundle = Bundle::open(FaultStore::no_fault(base_image.to_vec())).unwrap(); let mut bundle = Bundle::open(
FaultStore::no_fault(base_image.to_vec()),
crate::production_caps(),
)
.unwrap();
bundle.commit(&blocks, append_roots).unwrap(); bundle.commit(&blocks, append_roots).unwrap();
let store = bundle.into_store(); let store = bundle.into_store();
let recovered = Bundle::open(store.recover()).unwrap(); let recovered = Bundle::open(store.recover(), crate::production_caps()).unwrap();
assert_eq!(recovered.generation(), base_gen + 1); assert_eq!(recovered.generation(), base_gen + 1);
store.syscalls_issued() store.syscalls_issued()
}; };
@ -232,8 +245,13 @@ pub fn assert_selection_through_commits(seed: u64) {
let mut rng = Rng::new(seed); let mut rng = Rng::new(seed);
let uuid = FileUuid(rng.array16()); let uuid = FileUuid(rng.array16());
let doc = DocumentId(rng.array16()); let doc = DocumentId(rng.array16());
let mut bundle = let mut bundle = Bundle::create(
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle"); MemStore::new(),
uuid,
Manifest::empty(doc),
crate::production_caps(),
)
.expect("create bundle");
// Generation 0 lives in slot A; each commit flips the active slot. // Generation 0 lives in slot A; each commit flips the active slot.
assert_eq!(bundle.active_slot(), Slot::A); assert_eq!(bundle.active_slot(), Slot::A);
let commits = 5u64; let commits = 5u64;
@ -249,7 +267,8 @@ pub fn assert_selection_through_commits(seed: u64) {
let image = bundle.into_store().into_bytes(); let image = bundle.into_store().into_bytes();
// Reopen: selection picks the highest committed generation, no anomaly. // Reopen: selection picks the highest committed generation, no anomaly.
let reopened = Bundle::open(MemStore::from_bytes(image)).expect("reopen"); let reopened =
Bundle::open(MemStore::from_bytes(image), crate::production_caps()).expect("reopen");
assert_eq!(reopened.generation(), commits); assert_eq!(reopened.generation(), commits);
assert!(reopened.anomalies().is_empty()); assert!(reopened.anomalies().is_empty());
assert!(!reopened.is_read_only()); assert!(!reopened.is_read_only());
@ -373,8 +392,13 @@ pub fn assert_operation_index_end_to_end(seed: u64) {
let envelopes = generators::operation_envelopes(&mut rng, 36, 3, 8, 8); let envelopes = generators::operation_envelopes(&mut rng, 36, 3, 8, 8);
let uuid = FileUuid(rng.array16()); let uuid = FileUuid(rng.array16());
let doc = DocumentId(rng.array16()); let doc = DocumentId(rng.array16());
let mut bundle = let mut bundle = Bundle::create(
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle"); MemStore::new(),
uuid,
Manifest::empty(doc),
crate::production_caps(),
)
.expect("create bundle");
bundle bundle
.commit(&staged_envelope_blocks(&envelopes, 12), append_roots) .commit(&staged_envelope_blocks(&envelopes, 12), append_roots)
.expect("commit operation blocks"); .expect("commit operation blocks");
@ -392,7 +416,8 @@ pub fn assert_operation_index_end_to_end(seed: u64) {
// Reader side: reopen from the durable image; the fresh index is usable // Reader side: reopen from the durable image; the fresh index is usable
// and locates every operation. // and locates every operation.
let image = bundle.into_store().into_bytes(); let image = bundle.into_store().into_bytes();
let reopened = Bundle::open(MemStore::from_bytes(image)).expect("reopen"); let reopened =
Bundle::open(MemStore::from_bytes(image), crate::production_caps()).expect("reopen");
let usable = reopened let usable = reopened
.usable_operation_index() .usable_operation_index()
.expect("a fresh, covering index is usable"); .expect("a fresh, covering index is usable");
@ -413,8 +438,13 @@ pub fn assert_stale_operation_index_rejected_and_rebuilt(seed: u64) {
let uuid = FileUuid(rng.array16()); let uuid = FileUuid(rng.array16());
let doc = DocumentId(rng.array16()); let doc = DocumentId(rng.array16());
let mut bundle = let mut bundle = Bundle::create(
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle"); MemStore::new(),
uuid,
Manifest::empty(doc),
crate::production_caps(),
)
.expect("create bundle");
bundle bundle
.commit(&staged_envelope_blocks(first, 12), append_roots) .commit(&staged_envelope_blocks(first, 12), append_roots)
.expect("commit first blocks"); .expect("commit first blocks");
@ -444,7 +474,8 @@ pub fn assert_stale_operation_index_rejected_and_rebuilt(seed: u64) {
// The same verdict from a cold reopen; then rebuild from blocks. // The same verdict from a cold reopen; then rebuild from blocks.
let image = bundle.into_store().into_bytes(); let image = bundle.into_store().into_bytes();
let mut reopened = Bundle::open(MemStore::from_bytes(image)).expect("reopen"); let mut reopened =
Bundle::open(MemStore::from_bytes(image), crate::production_caps()).expect("reopen");
assert!(reopened.usable_operation_index().is_none()); assert!(reopened.usable_operation_index().is_none());
let rebuilt = scan_rebuild_operation_index(&reopened); let rebuilt = scan_rebuild_operation_index(&reopened);
commit_index(&mut reopened, &rebuilt); commit_index(&mut reopened, &rebuilt);
@ -466,8 +497,13 @@ pub fn assert_corrupt_operation_index_is_not_bundle_corruption(seed: u64) {
let envelopes = generators::operation_envelopes(&mut rng, 24, 3, 8, 8); let envelopes = generators::operation_envelopes(&mut rng, 24, 3, 8, 8);
let uuid = FileUuid(rng.array16()); let uuid = FileUuid(rng.array16());
let doc = DocumentId(rng.array16()); let doc = DocumentId(rng.array16());
let mut bundle = let mut bundle = Bundle::create(
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle"); MemStore::new(),
uuid,
Manifest::empty(doc),
crate::production_caps(),
)
.expect("create bundle");
bundle bundle
.commit(&staged_envelope_blocks(&envelopes, 8), append_roots) .commit(&staged_envelope_blocks(&envelopes, 8), append_roots)
.expect("commit operation blocks"); .expect("commit operation blocks");
@ -486,7 +522,7 @@ pub fn assert_corrupt_operation_index_is_not_bundle_corruption(seed: u64) {
) )
.expect("commit garbage index chunk"); .expect("commit garbage index chunk");
let image = bundle.into_store().into_bytes(); let image = bundle.into_store().into_bytes();
let mut reopened = Bundle::open(MemStore::from_bytes(image)) let mut reopened = Bundle::open(MemStore::from_bytes(image), crate::production_caps())
.expect("a defective index must not prevent opening"); .expect("a defective index must not prevent opening");
assert!(reopened.anomalies().is_empty()); assert!(reopened.anomalies().is_empty());
assert!(!reopened.is_read_only()); assert!(!reopened.is_read_only());
@ -512,14 +548,18 @@ pub fn assert_corrupt_operation_index_is_not_bundle_corruption(seed: u64) {
// (b) On-disk corruption of the now-valid index chunk's payload region. // (b) On-disk corruption of the now-valid index chunk's payload region.
let valid_image = reopened.into_store().into_bytes(); let valid_image = reopened.into_store().into_bytes();
let probe = Bundle::open(MemStore::from_bytes(valid_image.clone())).expect("reopen"); let probe = Bundle::open(
MemStore::from_bytes(valid_image.clone()),
crate::production_caps(),
)
.expect("reopen");
let root = probe let root = probe
.manifest() .manifest()
.operation_index_root .operation_index_root
.expect("the index is referenced"); .expect("the index is referenced");
let mut corrupt = valid_image; let mut corrupt = valid_image;
corrupt[(root.offset + 3) as usize] ^= 0xFF; corrupt[(root.offset + 3) as usize] ^= 0xFF;
let reopened = Bundle::open(MemStore::from_bytes(corrupt)) let reopened = Bundle::open(MemStore::from_bytes(corrupt), crate::production_caps())
.expect("index-region corruption must not prevent opening"); .expect("index-region corruption must not prevent opening");
assert!(reopened.anomalies().is_empty()); assert!(reopened.anomalies().is_empty());
reopened reopened
@ -595,8 +635,13 @@ pub fn run_barrier_declaration_roundtrip(seed: u64) {
// Commit a manifest carrying the declaration; reopen from the raw image. // Commit a manifest carrying the declaration; reopen from the raw image.
let uuid = FileUuid(rng.array16()); let uuid = FileUuid(rng.array16());
let doc = DocumentId(rng.array16()); let doc = DocumentId(rng.array16());
let mut bundle = let mut bundle = Bundle::create(
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle"); MemStore::new(),
uuid,
Manifest::empty(doc),
crate::production_caps(),
)
.expect("create bundle");
bundle bundle
.commit(&staged(&[rng.byte_vec(1, 40)]), |ctx| { .commit(&staged(&[rng.byte_vec(1, 40)]), |ctx| {
let mut m = append_roots(ctx); let mut m = append_roots(ctx);
@ -605,7 +650,8 @@ pub fn run_barrier_declaration_roundtrip(seed: u64) {
}) })
.expect("commit the declaration"); .expect("commit the declaration");
let image = bundle.into_store().into_bytes(); let image = bundle.into_store().into_bytes();
let reopened = Bundle::open(MemStore::from_bytes(image)).expect("reopen"); let reopened =
Bundle::open(MemStore::from_bytes(image), crate::production_caps()).expect("reopen");
// The bundle preserved the opaque blobs verbatim ... // The bundle preserved the opaque blobs verbatim ...
let decl = reopened let decl = reopened

View File

@ -87,6 +87,7 @@ fn build_bundle(
FileUuid([seed; 16]), FileUuid([seed; 16]),
manifest, manifest,
stamped_version, stamped_version,
crate::production_caps(),
) )
.expect("fixture manifest is emittable") .expect("fixture manifest is emittable")
} }

View File

@ -130,4 +130,24 @@ pub mod layout_stub;
// re-layout → re-render → re-resolve selection, across the ops/layout/render seams. // re-layout → re-render → re-resolve selection, across the ops/layout/render seams.
pub mod editloop; pub mod editloop;
use epiphany_bundle::{BundleCapabilities, ReductionAlgorithmVersion};
pub use rng::Rng; pub use rng::Rng;
/// The capabilities a **production composition path** supplies: the reduction
/// semantics this build actually implements, wrapped at the composition
/// boundary.
///
/// This is the "real authority" side of pin 3b's split. Fixtures deliberately
/// exercising arbitrary wire values use
/// [`BundleCapabilities::synthetic_for_fixture`] instead, so that a fixture
/// asserting behaviour at version `7` keeps asserting it when the authority
/// moves. **Never use `synthetic_for_fixture` on a production path.**
#[must_use]
pub fn production_caps() -> BundleCapabilities {
BundleCapabilities {
current_reduction_version: ReductionAlgorithmVersion(
epiphany_ops::CURRENT_REDUCTION_ALGORITHM_VERSION,
),
}
}

View File

@ -195,8 +195,13 @@ pub fn committed_manifest(seed: u64) -> Manifest {
let mut rng = Rng::new(seed); let mut rng = Rng::new(seed);
let uuid = FileUuid(rng.array16()); let uuid = FileUuid(rng.array16());
let doc = DocumentId(rng.array16()); let doc = DocumentId(rng.array16());
let mut bundle = let mut bundle = Bundle::create(
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle"); MemStore::new(),
uuid,
Manifest::empty(doc),
crate::production_caps(),
)
.expect("create bundle");
for _ in 0..3 { for _ in 0..3 {
let n = rng.range_usize(1, 3); let n = rng.range_usize(1, 3);
let payloads: Vec<Vec<u8>> = (0..n).map(|_| rng.byte_vec(1, 80)).collect(); let payloads: Vec<Vec<u8>> = (0..n).map(|_| rng.byte_vec(1, 80)).collect();
@ -228,25 +233,21 @@ fn canonical_score_bytes(envelopes: &[OperationEnvelope]) -> Vec<u8> {
/// stored as a `Snapshot` chunk, hash-verified on reopen and read back /// stored as a `Snapshot` chunk, hash-verified on reopen and read back
/// byte-identically. /// byte-identically.
/// ///
/// **Canonical-base wiring suspended** (`CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin /// **Canonical-base wiring RESTORED by P13-S27** (test 7 / inherited obligation
/// 3c, amended 2026-08-07). The snapshot's correct semantic home is the /// 3). The format rung's pin 3c suspended it for the S28 → P13-S27 interval,
/// manifest's `canonical_base` — that has not changed — but pin 3a's /// because pin 3a refused every base introduction and every base-bearing open
/// format-epoch matrix refuses every base introduction (`Bundle::commit`) and /// while no reduction authority existed to validate against. P13-S27 supplies
/// every base-bearing open (`Bundle::open`) during the S28 → P13-S27 /// that authority, so a base-bearing container is constructible again and the
/// interval, in both format epochs. So this harness cannot wire the snapshot /// snapshot is wired to its correct semantic home: the manifest's
/// there right now: it stages the snapshot as an ordinary chunk instead and /// `canonical_base`.
/// carries the resulting `ChunkRef` across the reopen out of band, reading it ///
/// back directly via [`Bundle::read_chunk`] (which hash-verifies any /// The two assertions that lapsed are back, and they are the point of the
/// `ChunkRef`, not only a canonical one). The /// restoration: **`verify_canonical_chunks` covers the base branch again**
/// serialize → load → decode → reserialize cycle below is unchanged; only the /// (including its `base.hash != base.root.hash` cross-check), and **the
/// snapshot's manifest placement is suspended. **Not** re-homed to /// reopened manifest actually carries the base**. It was never re-homed to
/// `acceleration_snapshots` — that field is verified nowhere in /// `acceleration_snapshots` — that field is verified nowhere in
/// `epiphany-bundle` (not in `open`, not in `verify_canonical_chunks`), so a /// `epiphany-bundle`, so a reference there would have verified nothing while
/// reference there would verify nothing while looking like preserved /// looking like preserved coverage.
/// 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 /// After reopen, the snapshot payload is decoded through
/// [`MaterializedState::decode_canonical`], compared structurally with the /// [`MaterializedState::decode_canonical`], compared structurally with the
@ -262,18 +263,21 @@ pub fn assert_reduction_serialization_stable(envelopes: &[OperationEnvelope], se
"re-reduction changed the canonical score bytes" "re-reduction changed the canonical score bytes"
); );
// serialize: stage the canonical state as a real **Snapshot** chunk. // serialize: stage the canonical state as a real **Snapshot** chunk and
// P13-S27 / CONTRACT_FORMAT_EPOCH_MAJOR1.md pin 3c: the manifest's // wire it to its correct semantic home, the manifest's `canonical_base`.
// `canonical_base` is the snapshot's correct semantic home, but that // Restored by P13-S27 (test 7): the base's version is the authority this
// wiring is suspended for the S28 -> P13-S27 interval (see the doc // session states, so pin 3a validates the introduction rather than
// comment above) — the chunk is staged but not referenced from any // refusing it.
// manifest root, and its `ChunkRef` is captured directly from the commit
// closure instead.
let mut rng = Rng::new(seed); let mut rng = Rng::new(seed);
let uuid = FileUuid(rng.array16()); let uuid = FileUuid(rng.array16());
let doc = DocumentId(rng.array16()); let doc = DocumentId(rng.array16());
let mut bundle = let mut bundle = Bundle::create(
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle"); MemStore::new(),
uuid,
Manifest::empty(doc),
crate::production_caps(),
)
.expect("create bundle");
let snapshot = StagedChunk { let snapshot = StagedChunk {
kind: ChunkKind::Snapshot, kind: ChunkKind::Snapshot,
schema_version: SchemaVersion::V0, schema_version: SchemaVersion::V0,
@ -282,21 +286,45 @@ pub fn assert_reduction_serialization_stable(envelopes: &[OperationEnvelope], se
let mut snapshot_root = None; let mut snapshot_root = None;
bundle bundle
.commit(&[snapshot], |ctx| { .commit(&[snapshot], |ctx| {
snapshot_root = Some(ctx.new_chunks[0]); let root = ctx.new_chunks[0];
ctx.previous_manifest.clone() snapshot_root = Some(root);
let mut m = ctx.previous_manifest.clone();
m.canonical_base = Some(SnapshotRef {
snapshot_id: SnapshotId(rng.array16()),
covers_causal_frontier: FrontierBytes::empty(),
reduction_algorithm_version: ReductionAlgorithmVersion(
epiphany_ops::CURRENT_REDUCTION_ALGORITHM_VERSION,
),
profile_id: ProfileId::Full,
hash: root.hash,
root,
});
m
}) })
.expect("commit snapshot"); .expect("commit snapshot as the canonical base");
let snapshot_root = snapshot_root.expect("commit ran the build closure"); let snapshot_root = snapshot_root.expect("commit ran the build closure");
let image = bundle.into_store().into_bytes(); let image = bundle.into_store().into_bytes();
// load: reopen from exactly those bytes; the snapshot chunk is read back // load: reopen from exactly those bytes. `verify_canonical_chunks` now
// directly by its `ChunkRef` (not through `manifest.canonical_base`, // covers the **base branch** again — RESTORED ASSERTION 1 of 2 — including
// suspended above) — `read_chunk` hash-verifies any `ChunkRef`, so the // its `base.hash != base.root.hash` cross-check.
// cycle's integrity guarantee is unchanged. let reopened =
let reopened = Bundle::open(MemStore::from_bytes(image)).expect("reopen bundle"); Bundle::open(MemStore::from_bytes(image), crate::production_caps()).expect("reopen bundle");
reopened reopened
.verify_canonical_chunks() .verify_canonical_chunks()
.expect("canonical chunks intact"); .expect("canonical chunks intact");
// RESTORED ASSERTION 2 of 2: the reopened manifest actually carries the
// base. Without this the harness passes on a base-free bundle and the
// restoration would be cosmetic.
let reopened_base = reopened
.manifest()
.canonical_base
.as_ref()
.expect("the reopened manifest must carry the canonical base");
assert_eq!(
reopened_base.root, snapshot_root,
"the reopened base must point at the snapshot chunk that was staged"
);
let loaded = reopened let loaded = reopened
.read_chunk(&snapshot_root) .read_chunk(&snapshot_root)
.expect("read snapshot chunk back"); .expect("read snapshot chunk back");
@ -347,8 +375,13 @@ pub fn assert_score_serialization_stable(score: &Score, frontier: &[u8], seed: u
let mut rng = Rng::new(seed); let mut rng = Rng::new(seed);
let uuid = FileUuid(rng.array16()); let uuid = FileUuid(rng.array16());
let doc = DocumentId(rng.array16()); let doc = DocumentId(rng.array16());
let mut bundle = let mut bundle = Bundle::create(
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle"); MemStore::new(),
uuid,
Manifest::empty(doc),
crate::production_caps(),
)
.expect("create bundle");
let snapshot = StagedChunk { let snapshot = StagedChunk {
kind: ChunkKind::Snapshot, kind: ChunkKind::Snapshot,
schema_version: SchemaVersion::for_major(3), schema_version: SchemaVersion::for_major(3),
@ -377,7 +410,8 @@ pub fn assert_score_serialization_stable(score: &Score, frontier: &[u8], seed: u
// load: reopen (read-write — an acceleration snapshot at the current // load: reopen (read-write — an acceleration snapshot at the current
// major is within the snapshot role's accept-set), hash-verify, read the // major is within the snapshot role's accept-set), hash-verify, read the
// referenced chunk back byte-identically. // referenced chunk back byte-identically.
let reopened = Bundle::open(MemStore::from_bytes(image)).expect("reopen bundle"); let reopened =
Bundle::open(MemStore::from_bytes(image), crate::production_caps()).expect("reopen bundle");
assert!( assert!(
!reopened.is_read_only(), !reopened.is_read_only(),
"a current-major acceleration snapshot must not force read-only" "a current-major acceleration snapshot must not force read-only"
@ -525,8 +559,13 @@ pub fn assert_operation_block_summary_survives_storage(envelopes: &[OperationEnv
let mut rng = Rng::new(seed); let mut rng = Rng::new(seed);
let uuid = FileUuid(rng.array16()); let uuid = FileUuid(rng.array16());
let doc = DocumentId(rng.array16()); let doc = DocumentId(rng.array16());
let mut bundle = let mut bundle = Bundle::create(
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle"); MemStore::new(),
uuid,
Manifest::empty(doc),
crate::production_caps(),
)
.expect("create bundle");
// A real operation block (opaque payload bytes) carrying the summary. // A real operation block (opaque payload bytes) carrying the summary.
let blocks: Vec<StagedChunk> = pack_operation_blocks(&[rng.byte_vec(4, 64)]) let blocks: Vec<StagedChunk> = pack_operation_blocks(&[rng.byte_vec(4, 64)])
.into_iter() .into_iter()
@ -544,7 +583,8 @@ pub fn assert_operation_block_summary_survives_storage(envelopes: &[OperationEnv
// Reopen and select the summary by block id — no block payload is decoded. // Reopen and select the summary by block id — no block payload is decoded.
let image = bundle.into_store().into_bytes(); let image = bundle.into_store().into_bytes();
let reopened = Bundle::open(MemStore::from_bytes(image)).expect("reopen bundle"); let reopened =
Bundle::open(MemStore::from_bytes(image), crate::production_caps()).expect("reopen bundle");
let root_id = reopened.manifest().operation_roots[0].id; let root_id = reopened.manifest().operation_roots[0].id;
assert_eq!( assert_eq!(
reopened.manifest().operation_block_summary(root_id), reopened.manifest().operation_block_summary(root_id),
@ -599,6 +639,7 @@ mod tests {
MemStore::new(), MemStore::new(),
FileUuid(rng.array16()), FileUuid(rng.array16()),
Manifest::empty(DocumentId(rng.array16())), Manifest::empty(DocumentId(rng.array16())),
crate::production_caps(),
) )
.expect("create bundle"); .expect("create bundle");
bundle bundle
@ -609,7 +650,7 @@ mod tests {
}) })
.expect("commit op block"); .expect("commit op block");
let image = bundle.into_store().into_bytes(); let image = bundle.into_store().into_bytes();
Bundle::open(MemStore::from_bytes(image)).expect("reopen bundle") Bundle::open(MemStore::from_bytes(image), crate::production_caps()).expect("reopen bundle")
} }
#[test] #[test]
@ -788,8 +829,13 @@ mod tests {
// A real committed superblock from a live bundle. // A real committed superblock from a live bundle.
let uuid = FileUuid(rng.array16()); let uuid = FileUuid(rng.array16());
let doc = DocumentId(rng.array16()); let doc = DocumentId(rng.array16());
let mut bundle = let mut bundle = Bundle::create(
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create"); MemStore::new(),
uuid,
Manifest::empty(doc),
crate::production_caps(),
)
.expect("create");
bundle bundle
.commit( .commit(
&[StagedChunk::operation_block(encode_block(&[vec![1u8; 8]]))], &[StagedChunk::operation_block(encode_block(&[vec![1u8; 8]]))],
@ -816,4 +862,95 @@ mod tests {
// Strong sensitivity: same identities, changed content → different bytes. // Strong sensitivity: same identities, changed content → different bytes.
assert_content_mutation_changes_serialization(); assert_content_mutation_changes_serialization();
} }
/// P13-S27 test 10b — the test M5b breaks, and **the only place in the rung
/// where the real authority meets a canonical base**. In `epiphany-testkit`,
/// which may reach the real constant.
///
/// # Two provably independent operands
///
/// The fixture is built with `synthetic_for_fixture(0)` and commits a base
/// carrying the **literal** `ReductionAlgorithmVersion(0)`; the reopen then
/// supplies `production_caps()`, which wraps the real constant. One operand
/// is a literal written into a fixture, the other is the authority read at
/// the reopen — neither derived from the other.
///
/// **Round 3 caught the alternative**: if both the supplied capability and
/// the base version descended from `CURRENT_REDUCTION_ALGORITHM_VERSION`,
/// both would move together under M5b's mutation and the comparison would
/// pass for every value — §0.1's own tautology, reproduced inside the
/// mutation built to detect it. **Do not tidy either literal into the
/// constant** (§7 item 4b).
///
/// # Both `Result` arms are written deliberately
///
/// Round 5 pinned this: "assert it opens" was not enough, because under M5b
/// the reopen returns `Err` and **a `#[test]` returning `Err` asserts
/// nothing about that error's fields**. The `Err` arm below runs only under
/// mutation, and it is what makes M5b's required two-field observation a
/// *verified* one rather than a stack trace. The third arm exists so a
/// *different* error under mutation is reported rather than read as success.
#[test]
fn a_base_bearing_bundle_reopened_under_the_real_authority_validates() {
use epiphany_bundle::{BundleCapabilities, BundleError};
let mut bundle = Bundle::create(
MemStore::new(),
FileUuid([0x5E; 16]),
Manifest::empty(DocumentId([0x5E; 16])),
BundleCapabilities::synthetic_for_fixture(0),
)
.expect("fixture bundle creates");
let staged = StagedChunk {
kind: ChunkKind::Snapshot,
schema_version: SchemaVersion::V0,
payload: vec![5u8, 5, 5],
};
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([0x5E; 16]),
covers_causal_frontier: FrontierBytes::empty(),
// A deliberate LITERAL — not the constant. See above.
reduction_algorithm_version: ReductionAlgorithmVersion(0),
profile_id: ProfileId::Full,
hash: root.hash,
root,
});
m
})
.expect("committing the base under the matching synthetic capability succeeds");
let image = bundle.into_store().into_bytes();
match Bundle::open(MemStore::from_bytes(image), crate::production_caps()) {
Ok(reopened) => {
assert!(
reopened.manifest().canonical_base.is_some(),
"the base must survive the reopen, or this asserts nothing"
);
assert_eq!(
reopened
.manifest()
.canonical_base
.as_ref()
.unwrap()
.reduction_algorithm_version,
ReductionAlgorithmVersion(0)
);
}
Err(BundleError::CanonicalBaseRequiresRebuild { base, current }) => {
// Reached only under M5b. Assert both fields, then fail loudly
// quoting them — that is the mutation's required observation.
assert_eq!(base, ReductionAlgorithmVersion(0));
panic!(
"M5b observation: base={} current={} — the authority is load-bearing here",
base.0, current.0
);
}
Err(other) => panic!("unexpected error, not the authority verdict: {other:?}"),
}
}
} }

View File

@ -48,13 +48,18 @@ fn a_bundle_round_trips_through_bytes_back_into_a_reduced_score() {
MemStore::new(), MemStore::new(),
FileUuid([3; 16]), FileUuid([3; 16]),
Manifest::empty(DocumentId([9; 16])), Manifest::empty(DocumentId([9; 16])),
epiphany_testkit::production_caps(),
) )
.expect("create"); .expect("create");
bundle.commit(&staged, append_roots).expect("commit"); bundle.commit(&staged, append_roots).expect("commit");
let image = bundle.into_store().into_bytes(); let image = bundle.into_store().into_bytes();
// Close it, reopen it from nothing but the bytes. // Close it, reopen it from nothing but the bytes.
let reopened = Bundle::open(MemStore::from_bytes(image)).expect("reopen"); let reopened = Bundle::open(
MemStore::from_bytes(image),
epiphany_testkit::production_caps(),
)
.expect("reopen");
let mut recovered = Vec::new(); let mut recovered = Vec::new();
for chunk in reopened.manifest().operation_roots.clone() { for chunk in reopened.manifest().operation_roots.clone() {

View File

@ -12,11 +12,11 @@ use std::path::{Path, PathBuf};
// +1 for req:format:container-epoch (the format-epoch rung, pin 7: // +1 for req:format:container-epoch (the format-epoch rung, pin 7:
// spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md) — the container major becomes an epoch, // spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md) — the container major becomes an epoch,
// and Chapter 8 states the classification and the epoch matrix normatively. // and Chapter 8 states the classification and the epoch matrix normatively.
const CORE_REQUIREMENT_COUNT: usize = 213; const CORE_REQUIREMENT_COUNT: usize = 214;
// +1 for req:textproj:manifest-schema-carried (G-minor, pins 8/11: // +1 for req:textproj:manifest-schema-carried (G-minor, pins 8/11:
// spec/PLAN_GMINOR_SCHEMA_MINOR.md); +1 for req:format:container-epoch (above). // spec/PLAN_GMINOR_SCHEMA_MINOR.md); +1 for req:format:container-epoch (above).
const SUITE_REQUIREMENT_COUNT: usize = 284; const SUITE_REQUIREMENT_COUNT: usize = 285;
const SUITE_LABEL_COUNT: usize = 284; const SUITE_LABEL_COUNT: usize = 285;
/// The normative chapter-to-area assignment. Keeping this as data makes adding a /// The normative chapter-to-area assignment. Keeping this as data makes adding a
/// requirement under the wrong chapter fail without encoding chapter names in /// requirement under the wrong chapter fail without encoding chapter names in

View File

@ -9,6 +9,8 @@ pub mod project;
pub mod serialize; pub mod serialize;
pub mod vectors; pub mod vectors;
use epiphany_bundle::BundleCapabilities;
use epiphany_bundle::{ use epiphany_bundle::{
ChunkKind, DocumentId, ExtensionId, FrontierBytes, LineageId, ProfileDeclaration, ProfileId, ChunkKind, DocumentId, ExtensionId, FrontierBytes, LineageId, ProfileDeclaration, ProfileId,
ReductionAlgorithmVersion, SchemaVersion, SemVer, SnapshotId, ReductionAlgorithmVersion, SchemaVersion, SemVer, SnapshotId,
@ -172,3 +174,21 @@ pub struct TextBlob {
/// Uncompressed blob payload carried inline. /// Uncompressed blob payload carried inline.
pub payload: Vec<u8>, pub payload: Vec<u8>,
} }
/// The capabilities a **production composition path** supplies: the reduction
/// semantics this build actually implements, wrapped at the composition
/// boundary.
///
/// This is the "real authority" side of pin 3b's split. Fixtures deliberately
/// exercising arbitrary wire values use
/// [`BundleCapabilities::synthetic_for_fixture`] instead, so that a fixture
/// asserting behaviour at version `7` keeps asserting it when the authority
/// moves. **Never use `synthetic_for_fixture` on a production path.**
#[must_use]
pub(crate) fn production_caps() -> BundleCapabilities {
BundleCapabilities {
current_reduction_version: ReductionAlgorithmVersion(
epiphany_ops::CURRENT_REDUCTION_ALGORITHM_VERSION,
),
}
}

View File

@ -980,8 +980,13 @@ mod tests {
let mut initial = Manifest::empty(DocumentId([3; 16])); let mut initial = Manifest::empty(DocumentId([3; 16]));
initial.lineage_id = Some(LineageId([4; 16])); initial.lineage_id = Some(LineageId([4; 16]));
let mut bundle = Bundle::create(MemStore::new(), FileUuid([1; 16]), initial) let mut bundle = Bundle::create(
.expect("a freshly created bundle with no canonical roots is valid"); MemStore::new(),
FileUuid([1; 16]),
initial,
crate::production_caps(),
)
.expect("a freshly created bundle with no canonical roots is valid");
bundle bundle
.commit( .commit(
@ -1119,7 +1124,13 @@ mod tests {
fn a_corrupt_operation_envelope_is_a_typed_error_not_a_panic() { fn a_corrupt_operation_envelope_is_a_typed_error_not_a_panic() {
let mut initial = Manifest::empty(DocumentId([5; 16])); let mut initial = Manifest::empty(DocumentId([5; 16]));
initial.lineage_id = None; initial.lineage_id = None;
let mut bundle = Bundle::create(MemStore::new(), FileUuid([2; 16]), initial).unwrap(); let mut bundle = Bundle::create(
MemStore::new(),
FileUuid([2; 16]),
initial,
crate::production_caps(),
)
.unwrap();
let garbage_block = StagedChunk::operation_block(encode_block(&[vec![0xFF; 4]])); let garbage_block = StagedChunk::operation_block(encode_block(&[vec![0xFF; 4]]));
bundle bundle
.commit(&[garbage_block], |ctx| { .commit(&[garbage_block], |ctx| {
@ -1144,7 +1155,7 @@ mod tests {
let corrupt_at = extension_root.offset as usize; let corrupt_at = extension_root.offset as usize;
image[corrupt_at] ^= 0xFF; image[corrupt_at] ^= 0xFF;
let corrupted = Bundle::open(MemStore::from_bytes(image)) let corrupted = Bundle::open(MemStore::from_bytes(image), crate::production_caps())
.expect("corrupting a non-canonical chunk's payload does not stop the bundle opening"); .expect("corrupting a non-canonical chunk's payload does not stop the bundle opening");
match document_from_bundle(&corrupted) { match document_from_bundle(&corrupted) {
Err(ProjectError::Bundle(_)) => {} Err(ProjectError::Bundle(_)) => {}

View File

@ -152,7 +152,12 @@ pub fn serialize_document<S: BlockStore>(
return Err(SerializeError::CanonicalBaseUnsupported); return Err(SerializeError::CanonicalBaseUnsupported);
} }
let mut bundle = Bundle::create(store, file_uuid, empty_manifest(document))?; let mut bundle = Bundle::create(
store,
file_uuid,
empty_manifest(document),
crate::production_caps(),
)?;
let mut staged = Vec::new(); let mut staged = Vec::new();
if let Some(base) = &document.canonical_base { if let Some(base) = &document.canonical_base {
@ -380,7 +385,8 @@ mod tests {
let bundle = serialize_document(document, MemStore::new(), FileUuid([1; 16])) let bundle = serialize_document(document, MemStore::new(), FileUuid([1; 16]))
.expect("a well-formed document serializes"); .expect("a well-formed document serializes");
let image = bundle.into_store().into_bytes(); let image = bundle.into_store().into_bytes();
Bundle::open(MemStore::from_bytes(image)).expect("the serialized bundle reopens") Bundle::open(MemStore::from_bytes(image), crate::production_caps())
.expect("the serialized bundle reopens")
} }
#[test] #[test]
@ -632,4 +638,32 @@ mod tests {
assert_eq!(reopened.manifest().document_id, document.document_id); assert_eq!(reopened.manifest().document_id, document.document_id);
} }
} }
/// P13-S27 test 10a — the test M5a breaks. **In `epiphany-textproj`**,
/// because `epiphany-bundle` must not depend on `epiphany-ops` (pin 1, §0.3)
/// and so no test there can reach the real authority.
///
/// # The `0` is a deliberate LITERAL, and that is load-bearing
///
/// Comparing against `CURRENT_REDUCTION_ALGORITHM_VERSION` would compare the
/// constant with itself laundered through one function call: mutate the
/// constant and **both sides move**, so the assertion would hold for every
/// value and M5a could not break it. **Do not "tidy" this into the
/// constant** — doing so makes M5a vacuous while leaving every test green,
/// a failure invisible to the suite (contract §7 item 4b exists to catch it).
///
/// **This test is expected to fail when P13-S16 bumps the authority**, and
/// that is correct: the literal is a tripwire on the production wiring, and
/// S16 updating it is S16 stating that the authority moved.
#[test]
fn serialize_document_supplies_the_real_reduction_authority() {
let document = minimal_document(42);
let bundle = serialize_document(&document, MemStore::new(), FileUuid([1; 16]))
.expect("a base-free document serializes");
assert_eq!(
bundle.capabilities().current_reduction_version,
ReductionAlgorithmVersion(0),
"the production writer must supply the real authority, not a literal of its own"
);
}
} }

View File

@ -10,8 +10,9 @@ them, having gone stale in two consecutive rounds by doing so.
execution is **reported, not patched in place** — if it needs a pin change, that execution is **reported, not patched in place** — if it needs a pin change, that
is its own amendment with its own review round. is its own amendment with its own review round.
**IMPLEMENTED 2026-08-09, STAGED, and NOT YET ACCEPTED.** The implementation remains **IMPLEMENTED AND ACCEPTED 2026-08-09, by the repository owner after execution review
staged and uncommitted; only the amendments are committed. 8.** The implementation was staged for the required independent review and is committed
with this acceptance record.
**How many post-ratification reviews have closed, which amendment each produced, and **How many post-ratification reviews have closed, which amendment each produced, and
what each found are THE HISTORY TABLE'S ROWS. This block does not restate them — what each found are THE HISTORY TABLE'S ROWS. This block does not restate them —
@ -1046,9 +1047,10 @@ history table, and it shows that every independent round before this one found s
- **Every finding since execution has been in this contract, not in the 21 staged - **Every finding since execution has been in this contract, not in the 21 staged
files** — across all eight reviews. files** — across all eight reviews.
**What remains is the owner's acceptance decision.** This document does not make it. **The repository owner accepted the implementation on 2026-08-09, after execution
Round 1's ratification was claimed by the author after a single round and withdrawn; review 8.** That decision is recorded here; it does not recast the evidence as proof of
**that precedent is why this block records the evidence and stops.** correctness. Round 1's author-claimed ratification was withdrawn, which is why the
evidence and the owner's decision remain distinct.
**Review round 19 — 2026-08-08, independent, against the round-17/18 working **Review round 19 — 2026-08-08, independent, against the round-17/18 working
tree. ZERO FINDINGS. The first clean round in nineteen.** tree. ZERO FINDINGS. The first clean round in nineteen.**
@ -3059,7 +3061,8 @@ begin it. No `create_staff`, `create_staff_group`, or invariant change.
**Do not bump `CURRENT_REDUCTION_ALGORITHM_VERSION` past 0** — pin 2. The bump **Do not bump `CURRENT_REDUCTION_ALGORITHM_VERSION` past 0** — pin 2. The bump
to 1 belongs to S16. to 1 belongs to S16.
**The executing agent MUST NOT commit.** Leave the work staged. **Execution boundary (SATISFIED):** the executing agent MUST NOT commit and left the
work staged for independent review.
**Execution is AUTHORISED as of ratification, 2026-08-08.** *(This read "no **Execution is AUTHORISED as of ratification, 2026-08-08.** *(This read "no
execution work may begin at all until this contract is ratified"; ratification has execution work may begin at all until this contract is ratified"; ratification has
@ -3067,9 +3070,10 @@ happened.)* The boundaries above are unchanged and remain binding — **stage on
§2's files by explicit path, never `git add -A`, re-check `HEAD` before staging §2's files by explicit path, never `git add -A`, re-check `HEAD` before staging
and before committing, and never `git reset`/`restore`/`checkout`/`stash`.** and before committing, and never `git reset`/`restore`/`checkout`/`stash`.**
**Leave the work STAGED. Do not commit.** The execution report is then subject to **The executing agent left the work STAGED and did not commit.** The execution report
**independent review before completion is accepted**, covering in particular was then subject to **independent review before completion was accepted**, covering in
**M7's three observations and its control**. particular **M7's three observations and its control**. The repository owner accepted
the reviewed staged implementation on 2026-08-09.
--- ---

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -11733,6 +11733,61 @@ pub struct SnapshotRef {
ignored or rebuilt if they disagree with the canonical document. ignored or rebuilt if they disagree with the canonical document.
\end{requirement} \end{requirement}
\begin{requirement}
\label{req:format:reduction-authority}
An implementation \MUST{} name the reduction semantics it
implements, and \MUST{} validate a canonical base against that
value rather than against a value the base itself supplied.
Comparing a base's \texttt{reduction\_algorithm\_version} only
against the active superblock's, as
Requirement~\ref{req:format:canonical-document-reduction}
requires, is necessary but \emph{not} sufficient: a writer that
seeds the superblock from the base's own self-report makes that
comparison an identity, so it holds for every conformingly
written document and detects only tampering. The semantics the
running implementation actually implements is a third value, and
it \MUST{} participate.
Accordingly:
\begin{itemize}
\item An implementation \MUST{} expose the reduction-algorithm
version it implements, and every reader and writer of a
canonical base \MUST{} state which semantics it implements
rather than infer it from the document.
\item A reader \MUST{} refuse to open a document whose
canonical base's \texttt{reduction\_algorithm\_version}
differs from the semantics the reader implements. A document
with no canonical base \MUST{} open regardless, since it
carries no reduced state that could be stale.
\item A writer \MUST{} refuse to emit or replace a canonical
base whose \texttt{reduction\_algorithm\_version} differs
from the semantics the writer implements. A commit that does
not emit or replace the base \MUSTNOT{} be refused on this
ground.
\item The refusal \MUSTNOT{} degrade to a read-only open and
\MUSTNOT{} be reported as an integrity anomaly. A stale base
is not a restricted-but-correct view of the document; it is
state computed under different rules, and serving it
read-only would present incorrect canonical state as
authoritative.
\item A refusal on this ground \MUST{} remain distinguishable
from a malformed-document failure. A base whose version
disagrees with its own superblock is corrupt, and corruption
\MUST{} be reported as such: the two conditions detect
different faults, and collapsing them loses the distinction
this requirement rests on.
\end{itemize}
Rebuilding is out of scope for opening. An implementation
\MUSTNOT{} silently rebuild a stale base during open, because
the envelopes required to rebuild may have been pruned
(Requirement~\ref{req:format:pruning-state-preservation}); a rebuild
path is sound only
where the full pre-base history is demonstrably present.
\end{requirement}
\subsection{Pruning} \subsection{Pruning}
\begin{requirement} \begin{requirement}
@ -16820,6 +16875,28 @@ layouts they own versus inherit:
staleness. \sectionsc{Schema Versioning} gains the corresponding staleness. \sectionsc{Schema Versioning} gains the corresponding
distinction between the schema axis and the container axis. distinction between the schema axis and the container axis.
\\ \\
\today & Ch.~\ref{ch:format} & \sectionsc{The Canonical Document Identity}
gains Requirement~\ref{req:format:reduction-authority}: an implementation
\MUST{} name the reduction semantics it implements and validate a canonical
base against \emph{that}, not against a value the base itself supplied.
Comparing the base's \texttt{reduction\_algorithm\_version} only against
the active superblock's is necessary but not sufficient, because a writer
that seeds the superblock from the base's own self-report makes the
comparison an identity --- so it holds for every conformingly written
document and detects only tampering. The requirement therefore introduces
the implementation's own semantics as a third participant, required at both
boundaries: a reader refuses a base that disagrees with the semantics it
implements, and a writer refuses to emit or replace one. A document with no
canonical base opens regardless, carrying no reduced state that could be
stale, and a commit that does not touch the base is not refused on this
ground. The refusal may not degrade to a read-only open nor be reported as
an integrity anomaly --- a stale base is not a restricted-but-correct view
but state computed under different rules --- and it stays distinguishable
from the malformed-document failure a base disagreeing with its own
superblock produces, since the two detect different faults. Rebuilding
during open is prohibited outright, because the envelopes a rebuild would
need may have been pruned.
\\
\bottomrule \bottomrule
\end{longtable} \end{longtable}