P13-S27: accept reduction authority implementation
This commit is contained in:
parent
83df0a01be
commit
4df8e257f3
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(_)) => {}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue