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.
|
||||
|
||||
use crate::codec::DecodeError;
|
||||
use crate::ids::SchemaVersion;
|
||||
use crate::ids::{ReductionAlgorithmVersion, SchemaVersion};
|
||||
use epiphany_determinism::ContentHash;
|
||||
|
||||
/// 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.
|
||||
LegacyBaseIntroductionRejected,
|
||||
|
||||
/// A major-1 container's canonical base cannot yet be validated: matrix
|
||||
/// rows 5i/6i, opening a container that already carries a base, or
|
||||
/// committing one into it. The container is the **right** epoch — this is
|
||||
/// not a request to repack — but no reduction-authority capability exists
|
||||
/// yet to validate the base against. **Temporary**: `P13-S27` supplies
|
||||
/// that capability and replaces both branches with real validation, not
|
||||
/// this categorical refusal. Never mentions repack (repacking a
|
||||
/// already-correct-epoch container would be wrong advice), and MUST NOT
|
||||
/// degrade to a read-only open — a pre-authority base is not a
|
||||
/// restricted-but-correct view.
|
||||
ReductionAuthorityUnavailable,
|
||||
/// A canonical base was produced under reduction semantics this build does
|
||||
/// not implement, so the materialized state it carries is **the wrong
|
||||
/// materialization** and must be rebuilt before use.
|
||||
///
|
||||
/// Raised on both boundaries a base can cross: `Bundle::open`, when a
|
||||
/// container's base disagrees with the caller's
|
||||
/// [`BundleCapabilities::current_reduction_version`], and
|
||||
/// `commit`/`commit_versioned`, when a **newly emitted or replaced** base
|
||||
/// does. Replaces the format rung's temporary
|
||||
/// `ReductionAuthorityUnavailable`, which refused both boundaries
|
||||
/// 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 {
|
||||
|
|
@ -229,9 +254,11 @@ impl core::fmt::Display for BundleError {
|
|||
"cannot add or replace a canonical base in a legacy (format major 0) bundle; \
|
||||
repack into a fresh major-1 bundle",
|
||||
),
|
||||
BundleError::ReductionAuthorityUnavailable => f.write_str(
|
||||
"this major-1 container's canonical base cannot yet be validated: no \
|
||||
reduction-authority capability exists until P13-S27 lands",
|
||||
BundleError::CanonicalBaseRequiresRebuild { base, current } => write!(
|
||||
f,
|
||||
"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
|
||||
//! 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::error::IntegrityAnomaly;
|
||||
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) {
|
||||
// Open the bundle over a fault store; `open` only reads, so it never
|
||||
// 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(
|
||||
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 store = bundle.into_store();
|
||||
|
||||
// Recover: reopen from exactly the bytes that survived the crash.
|
||||
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!(
|
||||
"crash at {crash:?} left an UNOPENABLE bundle (base gen {base_gen}): {e}\n\
|
||||
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) {
|
||||
let doc = DocumentId([(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 {
|
||||
let n = rng.below(3) as usize; // 0..2 envelopes
|
||||
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();
|
||||
for commits in 0..4u64 {
|
||||
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());
|
||||
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.
|
||||
let pick = (rng.next_u64() as usize) % images.len();
|
||||
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) => {
|
||||
cov.opens_ok += 1;
|
||||
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
|
||||
// recovers to G+1) via a no-fault run.
|
||||
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();
|
||||
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);
|
||||
store.syscalls_issued()
|
||||
};
|
||||
|
|
@ -646,7 +674,8 @@ pub fn run_manifest_selection_harness() {
|
|||
b.set_slot(Slot::A, &sb_a);
|
||||
b.set_slot(Slot::B, &sb_b);
|
||||
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.generation(), 1);
|
||||
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::B, &sb_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.generation(), 7);
|
||||
assert!(bundle.anomalies().is_empty());
|
||||
|
|
@ -675,7 +705,7 @@ pub fn run_manifest_selection_harness() {
|
|||
let sb_b = b.add_manifest(5, &m);
|
||||
b.set_slot(Slot::A, &sb_a);
|
||||
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.active_slot(), Slot::B);
|
||||
assert!(bundle.anomalies().is_empty());
|
||||
|
|
@ -688,7 +718,7 @@ pub fn run_manifest_selection_harness() {
|
|||
let sb = b.add_manifest(9, &m);
|
||||
b.set_slot(Slot::A, &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.generation(), 9);
|
||||
assert!(bundle.anomalies().is_empty());
|
||||
|
|
@ -707,7 +737,7 @@ pub fn run_manifest_selection_harness() {
|
|||
sb_b.generation = 9;
|
||||
b.set_slot(Slot::A, &sb_a);
|
||||
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.anomalies(),
|
||||
&[IntegrityAnomaly::DivergentSameGeneration { generation: 9 }]
|
||||
|
|
@ -726,7 +756,7 @@ pub fn run_manifest_selection_harness() {
|
|||
sb_b.generation = 9;
|
||||
b.set_slot(Slot::A, &sb_a);
|
||||
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.anomalies(),
|
||||
|
|
@ -749,7 +779,7 @@ pub fn run_manifest_selection_harness() {
|
|||
b.corrupt_slot(Slot::A);
|
||||
b.corrupt_slot(Slot::B);
|
||||
assert!(matches!(
|
||||
Bundle::open(b.store()),
|
||||
Bundle::open(b.store(), BundleCapabilities::synthetic_for_fixture(0)),
|
||||
Err(crate::BundleError::NoValidSuperblock)
|
||||
));
|
||||
}
|
||||
|
|
@ -764,7 +794,7 @@ pub fn run_manifest_selection_harness() {
|
|||
sb_b.commit_state = CommitState::Reserved(1);
|
||||
b.set_slot(Slot::A, &sb_a);
|
||||
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
|
||||
// non-committed slot is surfaced as an anomaly, but this is ordinary
|
||||
// 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");
|
||||
b.set_slot(Slot::A, &sb_a);
|
||||
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.generation(), 5);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -285,8 +285,22 @@ impl SchemaVersion {
|
|||
|
||||
/// The reduction-algorithm version that produced a canonical-base snapshot
|
||||
/// (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
|
||||
/// `u32` (the algorithm catalog itself lives in `epiphany-ops`).
|
||||
/// **the semantics the running build implements**, which the caller states via
|
||||
/// `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)]
|
||||
pub struct ReductionAlgorithmVersion(pub u32);
|
||||
|
||||
|
|
|
|||
|
|
@ -67,8 +67,9 @@ pub use block::{
|
|||
MAX_BLOCK_DEFAULT,
|
||||
};
|
||||
pub use bundle::{
|
||||
manifest_chunk_hash, manifest_chunk_hash_versioned, Bundle, CommitContext, StagedChunk,
|
||||
BODY_START, MAX_BLOB_BYTES, MAX_CHUNK_BYTES, MAX_MANIFEST_BYTES, SUPPORTED_SCHEMA_MAJOR,
|
||||
manifest_chunk_hash, manifest_chunk_hash_versioned, Bundle, BundleCapabilities, CommitContext,
|
||||
StagedChunk, BODY_START, MAX_BLOB_BYTES, MAX_CHUNK_BYTES, MAX_MANIFEST_BYTES,
|
||||
SUPPORTED_SCHEMA_MAJOR,
|
||||
};
|
||||
pub use chunk::{
|
||||
chunk_content_hash, chunk_id, content_hash_for, ChunkKind, ChunkRef, CompressionAlgorithm,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
//! 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::BundleCapabilities;
|
||||
use epiphany_bundle::{Bundle, DocumentId, FileUuid, Manifest, MemStore, StagedChunk};
|
||||
|
||||
/// 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.)
|
||||
fn build_base(rng: &mut SplitMix64, commits: u64) -> (Vec<u8>, u64) {
|
||||
let doc = DocumentId([(rng.next_u64() & 0xff) as u8; 16]);
|
||||
let mut bundle =
|
||||
Bundle::create(MemStore::new(), FileUuid([7; 16]), Manifest::empty(doc)).unwrap();
|
||||
let mut bundle = Bundle::create(
|
||||
MemStore::new(),
|
||||
FileUuid([7; 16]),
|
||||
Manifest::empty(doc),
|
||||
BundleCapabilities::synthetic_for_fixture(0),
|
||||
)
|
||||
.unwrap();
|
||||
for i in 0..commits {
|
||||
let payload = epiphany_bundle::encode_block(&[vec![i as u8; 16]]);
|
||||
bundle
|
||||
|
|
@ -83,6 +89,7 @@ fn file_store_real_fsync_round_trip() {
|
|||
store,
|
||||
FileUuid([0xAB; 16]),
|
||||
Manifest::empty(DocumentId([1; 16])),
|
||||
BundleCapabilities::synthetic_for_fixture(0),
|
||||
)
|
||||
.unwrap();
|
||||
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.
|
||||
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.manifest().operation_roots.len(), 2);
|
||||
reopened.verify_canonical_chunks().unwrap();
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
//! real commit path.
|
||||
|
||||
use epiphany_bundle::fuzz::run_manifest_selection_harness;
|
||||
use epiphany_bundle::BundleCapabilities;
|
||||
use epiphany_bundle::{Bundle, DocumentId, FileUuid, Manifest, MemStore, Slot, StagedChunk};
|
||||
|
||||
#[test]
|
||||
|
|
@ -25,6 +26,7 @@ fn commit_then_corrupt_active_slot_falls_back() {
|
|||
MemStore::new(),
|
||||
FileUuid([3; 16]),
|
||||
Manifest::empty(DocumentId([4; 16])),
|
||||
BundleCapabilities::synthetic_for_fixture(0),
|
||||
)
|
||||
.unwrap();
|
||||
// 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;
|
||||
|
||||
// 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.generation(), 0);
|
||||
assert!(recovered.anomalies().is_empty());
|
||||
|
|
|
|||
|
|
@ -104,6 +104,53 @@ pub mod valuegen;
|
|||
pub mod fuzz;
|
||||
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::{
|
||||
AnomalousReplicaSegment, IntegrityAnomaly, IntegrityAnomalyKind, ReplicaAnomalyReason,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -124,8 +124,13 @@ fn build_fixture(dir: &Path) -> Fixture {
|
|||
|
||||
let uuid = FileUuid(rng.array16());
|
||||
let doc = DocumentId(rng.array16());
|
||||
let mut bundle =
|
||||
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create base bundle");
|
||||
let mut bundle = Bundle::create(
|
||||
MemStore::new(),
|
||||
uuid,
|
||||
Manifest::empty(doc),
|
||||
epiphany_testkit::production_caps(),
|
||||
)
|
||||
.expect("create base bundle");
|
||||
bundle
|
||||
.commit(&staged_blocks(&envelopes), append_roots)
|
||||
.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.
|
||||
fn restore_and_open(path: &Path, image: &[u8]) -> Bundle<FileStore> {
|
||||
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.
|
||||
|
|
@ -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
|
||||
/// `manifest.canonical_base` right now) and every operation block.
|
||||
fn open_bootstrap_read(path: &Path, base_root: &ChunkRef) -> usize {
|
||||
let bundle = Bundle::open(FileStore::open(path).expect("open store")).expect("open bundle");
|
||||
let bundle = Bundle::open(
|
||||
FileStore::open(path).expect("open store"),
|
||||
epiphany_testkit::production_caps(),
|
||||
)
|
||||
.expect("open bundle");
|
||||
let manifest = bundle.manifest();
|
||||
let mut bytes = 0usize;
|
||||
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) {
|
||||
let uuid = FileUuid(rng.array16());
|
||||
let doc = DocumentId(rng.array16());
|
||||
let mut bundle =
|
||||
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle");
|
||||
let mut bundle = Bundle::create(
|
||||
MemStore::new(),
|
||||
uuid,
|
||||
Manifest::empty(doc),
|
||||
crate::production_caps(),
|
||||
)
|
||||
.expect("create bundle");
|
||||
for _ in 0..commits {
|
||||
let n = rng.range_usize(0, 2);
|
||||
let payloads: Vec<Vec<u8>> = (0..n).map(|_| rng.byte_vec(1, 40)).collect();
|
||||
|
|
@ -104,14 +109,18 @@ pub fn assert_recovers(
|
|||
crash: CrashPoint,
|
||||
) {
|
||||
let blocks = staged(envelope_payloads);
|
||||
let mut bundle = Bundle::open(FaultStore::new(base_image.to_vec(), crash))
|
||||
let mut bundle = Bundle::open(
|
||||
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 store = bundle.into_store();
|
||||
|
||||
// Recover from exactly the bytes that survived the crash.
|
||||
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())
|
||||
.unwrap_or_else(|e| {
|
||||
panic!("crash at {crash:?} left an UNOPENABLE bundle (base gen {base_gen}): {e}")
|
||||
});
|
||||
|
||||
|
|
@ -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
|
||||
// clean commit reaches G+1).
|
||||
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();
|
||||
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);
|
||||
store.syscalls_issued()
|
||||
};
|
||||
|
|
@ -232,8 +245,13 @@ pub fn assert_selection_through_commits(seed: u64) {
|
|||
let mut rng = Rng::new(seed);
|
||||
let uuid = FileUuid(rng.array16());
|
||||
let doc = DocumentId(rng.array16());
|
||||
let mut bundle =
|
||||
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle");
|
||||
let mut bundle = Bundle::create(
|
||||
MemStore::new(),
|
||||
uuid,
|
||||
Manifest::empty(doc),
|
||||
crate::production_caps(),
|
||||
)
|
||||
.expect("create bundle");
|
||||
// Generation 0 lives in slot A; each commit flips the active slot.
|
||||
assert_eq!(bundle.active_slot(), Slot::A);
|
||||
let commits = 5u64;
|
||||
|
|
@ -249,7 +267,8 @@ pub fn assert_selection_through_commits(seed: u64) {
|
|||
let image = bundle.into_store().into_bytes();
|
||||
|
||||
// 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!(reopened.anomalies().is_empty());
|
||||
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 uuid = FileUuid(rng.array16());
|
||||
let doc = DocumentId(rng.array16());
|
||||
let mut bundle =
|
||||
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle");
|
||||
let mut bundle = Bundle::create(
|
||||
MemStore::new(),
|
||||
uuid,
|
||||
Manifest::empty(doc),
|
||||
crate::production_caps(),
|
||||
)
|
||||
.expect("create bundle");
|
||||
bundle
|
||||
.commit(&staged_envelope_blocks(&envelopes, 12), append_roots)
|
||||
.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
|
||||
// and locates every operation.
|
||||
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
|
||||
.usable_operation_index()
|
||||
.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 doc = DocumentId(rng.array16());
|
||||
let mut bundle =
|
||||
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle");
|
||||
let mut bundle = Bundle::create(
|
||||
MemStore::new(),
|
||||
uuid,
|
||||
Manifest::empty(doc),
|
||||
crate::production_caps(),
|
||||
)
|
||||
.expect("create bundle");
|
||||
bundle
|
||||
.commit(&staged_envelope_blocks(first, 12), append_roots)
|
||||
.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.
|
||||
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());
|
||||
let rebuilt = scan_rebuild_operation_index(&reopened);
|
||||
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 uuid = FileUuid(rng.array16());
|
||||
let doc = DocumentId(rng.array16());
|
||||
let mut bundle =
|
||||
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle");
|
||||
let mut bundle = Bundle::create(
|
||||
MemStore::new(),
|
||||
uuid,
|
||||
Manifest::empty(doc),
|
||||
crate::production_caps(),
|
||||
)
|
||||
.expect("create bundle");
|
||||
bundle
|
||||
.commit(&staged_envelope_blocks(&envelopes, 8), append_roots)
|
||||
.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");
|
||||
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");
|
||||
assert!(reopened.anomalies().is_empty());
|
||||
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.
|
||||
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
|
||||
.manifest()
|
||||
.operation_index_root
|
||||
.expect("the index is referenced");
|
||||
let mut corrupt = valid_image;
|
||||
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");
|
||||
assert!(reopened.anomalies().is_empty());
|
||||
reopened
|
||||
|
|
@ -595,8 +635,13 @@ pub fn run_barrier_declaration_roundtrip(seed: u64) {
|
|||
// Commit a manifest carrying the declaration; reopen from the raw image.
|
||||
let uuid = FileUuid(rng.array16());
|
||||
let doc = DocumentId(rng.array16());
|
||||
let mut bundle =
|
||||
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle");
|
||||
let mut bundle = Bundle::create(
|
||||
MemStore::new(),
|
||||
uuid,
|
||||
Manifest::empty(doc),
|
||||
crate::production_caps(),
|
||||
)
|
||||
.expect("create bundle");
|
||||
bundle
|
||||
.commit(&staged(&[rng.byte_vec(1, 40)]), |ctx| {
|
||||
let mut m = append_roots(ctx);
|
||||
|
|
@ -605,7 +650,8 @@ pub fn run_barrier_declaration_roundtrip(seed: u64) {
|
|||
})
|
||||
.expect("commit the declaration");
|
||||
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 ...
|
||||
let decl = reopened
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ fn build_bundle(
|
|||
FileUuid([seed; 16]),
|
||||
manifest,
|
||||
stamped_version,
|
||||
crate::production_caps(),
|
||||
)
|
||||
.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.
|
||||
pub mod editloop;
|
||||
|
||||
use epiphany_bundle::{BundleCapabilities, ReductionAlgorithmVersion};
|
||||
|
||||
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 uuid = FileUuid(rng.array16());
|
||||
let doc = DocumentId(rng.array16());
|
||||
let mut bundle =
|
||||
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle");
|
||||
let mut bundle = Bundle::create(
|
||||
MemStore::new(),
|
||||
uuid,
|
||||
Manifest::empty(doc),
|
||||
crate::production_caps(),
|
||||
)
|
||||
.expect("create bundle");
|
||||
for _ in 0..3 {
|
||||
let n = rng.range_usize(1, 3);
|
||||
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
|
||||
/// byte-identically.
|
||||
///
|
||||
/// **Canonical-base wiring suspended** (`CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin
|
||||
/// 3c, amended 2026-08-07). The snapshot's correct semantic home is the
|
||||
/// manifest's `canonical_base` — that has not changed — but pin 3a's
|
||||
/// format-epoch matrix refuses every base introduction (`Bundle::commit`) and
|
||||
/// every base-bearing open (`Bundle::open`) during the S28 → P13-S27
|
||||
/// interval, in both format epochs. So this harness cannot wire the snapshot
|
||||
/// there right now: it stages the snapshot as an ordinary chunk instead and
|
||||
/// carries the resulting `ChunkRef` across the reopen out of band, reading it
|
||||
/// back directly via [`Bundle::read_chunk`] (which hash-verifies any
|
||||
/// `ChunkRef`, not only a canonical one). The
|
||||
/// serialize → load → decode → reserialize cycle below is unchanged; only the
|
||||
/// snapshot's manifest placement is suspended. **Not** re-homed to
|
||||
/// **Canonical-base wiring RESTORED by P13-S27** (test 7 / inherited obligation
|
||||
/// 3). The format rung's pin 3c suspended it for the S28 → P13-S27 interval,
|
||||
/// because pin 3a refused every base introduction and every base-bearing open
|
||||
/// while no reduction authority existed to validate against. P13-S27 supplies
|
||||
/// that authority, so a base-bearing container is constructible again and the
|
||||
/// snapshot is wired to its correct semantic home: the manifest's
|
||||
/// `canonical_base`.
|
||||
///
|
||||
/// The two assertions that lapsed are back, and they are the point of the
|
||||
/// restoration: **`verify_canonical_chunks` covers the base branch again**
|
||||
/// (including its `base.hash != base.root.hash` cross-check), and **the
|
||||
/// reopened manifest actually carries the base**. It was never re-homed to
|
||||
/// `acceleration_snapshots` — that field is verified nowhere in
|
||||
/// `epiphany-bundle` (not in `open`, not in `verify_canonical_chunks`), so a
|
||||
/// reference there would verify nothing while looking like preserved
|
||||
/// coverage. Two assertions lapse until P13-S27 restores the wiring:
|
||||
/// `verify_canonical_chunks`'s base branch (its `base.hash != base.root.hash`
|
||||
/// cross-check), and the reopened manifest actually carrying the base. Both
|
||||
/// are owed back in `spec/CONTRACT_P13S27_REDUCTION_AUTHORITY.md`.
|
||||
/// `epiphany-bundle`, so a reference there would have verified nothing while
|
||||
/// looking like preserved coverage.
|
||||
///
|
||||
/// After reopen, the snapshot payload is decoded through
|
||||
/// [`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"
|
||||
);
|
||||
|
||||
// serialize: stage the canonical state as a real **Snapshot** chunk.
|
||||
// P13-S27 / CONTRACT_FORMAT_EPOCH_MAJOR1.md pin 3c: the manifest's
|
||||
// `canonical_base` is the snapshot's correct semantic home, but that
|
||||
// wiring is suspended for the S28 -> P13-S27 interval (see the doc
|
||||
// comment above) — the chunk is staged but not referenced from any
|
||||
// manifest root, and its `ChunkRef` is captured directly from the commit
|
||||
// closure instead.
|
||||
// serialize: stage the canonical state as a real **Snapshot** chunk and
|
||||
// wire it to its correct semantic home, the manifest's `canonical_base`.
|
||||
// Restored by P13-S27 (test 7): the base's version is the authority this
|
||||
// session states, so pin 3a validates the introduction rather than
|
||||
// refusing it.
|
||||
let mut rng = Rng::new(seed);
|
||||
let uuid = FileUuid(rng.array16());
|
||||
let doc = DocumentId(rng.array16());
|
||||
let mut bundle =
|
||||
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle");
|
||||
let mut bundle = Bundle::create(
|
||||
MemStore::new(),
|
||||
uuid,
|
||||
Manifest::empty(doc),
|
||||
crate::production_caps(),
|
||||
)
|
||||
.expect("create bundle");
|
||||
let snapshot = StagedChunk {
|
||||
kind: ChunkKind::Snapshot,
|
||||
schema_version: SchemaVersion::V0,
|
||||
|
|
@ -282,21 +286,45 @@ pub fn assert_reduction_serialization_stable(envelopes: &[OperationEnvelope], se
|
|||
let mut snapshot_root = None;
|
||||
bundle
|
||||
.commit(&[snapshot], |ctx| {
|
||||
snapshot_root = Some(ctx.new_chunks[0]);
|
||||
ctx.previous_manifest.clone()
|
||||
let root = ctx.new_chunks[0];
|
||||
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 image = bundle.into_store().into_bytes();
|
||||
|
||||
// load: reopen from exactly those bytes; the snapshot chunk is read back
|
||||
// directly by its `ChunkRef` (not through `manifest.canonical_base`,
|
||||
// suspended above) — `read_chunk` hash-verifies any `ChunkRef`, so the
|
||||
// cycle's integrity guarantee is unchanged.
|
||||
let reopened = Bundle::open(MemStore::from_bytes(image)).expect("reopen bundle");
|
||||
// load: reopen from exactly those bytes. `verify_canonical_chunks` now
|
||||
// covers the **base branch** again — RESTORED ASSERTION 1 of 2 — including
|
||||
// its `base.hash != base.root.hash` cross-check.
|
||||
let reopened =
|
||||
Bundle::open(MemStore::from_bytes(image), crate::production_caps()).expect("reopen bundle");
|
||||
reopened
|
||||
.verify_canonical_chunks()
|
||||
.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
|
||||
.read_chunk(&snapshot_root)
|
||||
.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 uuid = FileUuid(rng.array16());
|
||||
let doc = DocumentId(rng.array16());
|
||||
let mut bundle =
|
||||
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle");
|
||||
let mut bundle = Bundle::create(
|
||||
MemStore::new(),
|
||||
uuid,
|
||||
Manifest::empty(doc),
|
||||
crate::production_caps(),
|
||||
)
|
||||
.expect("create bundle");
|
||||
let snapshot = StagedChunk {
|
||||
kind: ChunkKind::Snapshot,
|
||||
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
|
||||
// major is within the snapshot role's accept-set), hash-verify, read the
|
||||
// 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!(
|
||||
!reopened.is_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 uuid = FileUuid(rng.array16());
|
||||
let doc = DocumentId(rng.array16());
|
||||
let mut bundle =
|
||||
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create bundle");
|
||||
let mut bundle = Bundle::create(
|
||||
MemStore::new(),
|
||||
uuid,
|
||||
Manifest::empty(doc),
|
||||
crate::production_caps(),
|
||||
)
|
||||
.expect("create bundle");
|
||||
// A real operation block (opaque payload bytes) carrying the summary.
|
||||
let blocks: Vec<StagedChunk> = pack_operation_blocks(&[rng.byte_vec(4, 64)])
|
||||
.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.
|
||||
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;
|
||||
assert_eq!(
|
||||
reopened.manifest().operation_block_summary(root_id),
|
||||
|
|
@ -599,6 +639,7 @@ mod tests {
|
|||
MemStore::new(),
|
||||
FileUuid(rng.array16()),
|
||||
Manifest::empty(DocumentId(rng.array16())),
|
||||
crate::production_caps(),
|
||||
)
|
||||
.expect("create bundle");
|
||||
bundle
|
||||
|
|
@ -609,7 +650,7 @@ mod tests {
|
|||
})
|
||||
.expect("commit op block");
|
||||
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]
|
||||
|
|
@ -788,8 +829,13 @@ mod tests {
|
|||
// A real committed superblock from a live bundle.
|
||||
let uuid = FileUuid(rng.array16());
|
||||
let doc = DocumentId(rng.array16());
|
||||
let mut bundle =
|
||||
Bundle::create(MemStore::new(), uuid, Manifest::empty(doc)).expect("create");
|
||||
let mut bundle = Bundle::create(
|
||||
MemStore::new(),
|
||||
uuid,
|
||||
Manifest::empty(doc),
|
||||
crate::production_caps(),
|
||||
)
|
||||
.expect("create");
|
||||
bundle
|
||||
.commit(
|
||||
&[StagedChunk::operation_block(encode_block(&[vec![1u8; 8]]))],
|
||||
|
|
@ -816,4 +862,95 @@ mod tests {
|
|||
// Strong sensitivity: same identities, changed content → different bytes.
|
||||
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(),
|
||||
FileUuid([3; 16]),
|
||||
Manifest::empty(DocumentId([9; 16])),
|
||||
epiphany_testkit::production_caps(),
|
||||
)
|
||||
.expect("create");
|
||||
bundle.commit(&staged, append_roots).expect("commit");
|
||||
let image = bundle.into_store().into_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();
|
||||
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:
|
||||
// spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md) — the container major becomes an epoch,
|
||||
// and Chapter 8 states the classification and the epoch matrix normatively.
|
||||
const CORE_REQUIREMENT_COUNT: usize = 213;
|
||||
const CORE_REQUIREMENT_COUNT: usize = 214;
|
||||
// +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).
|
||||
const SUITE_REQUIREMENT_COUNT: usize = 284;
|
||||
const SUITE_LABEL_COUNT: usize = 284;
|
||||
const SUITE_REQUIREMENT_COUNT: usize = 285;
|
||||
const SUITE_LABEL_COUNT: usize = 285;
|
||||
|
||||
/// The normative chapter-to-area assignment. Keeping this as data makes adding a
|
||||
/// requirement under the wrong chapter fail without encoding chapter names in
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ pub mod project;
|
|||
pub mod serialize;
|
||||
pub mod vectors;
|
||||
|
||||
use epiphany_bundle::BundleCapabilities;
|
||||
|
||||
use epiphany_bundle::{
|
||||
ChunkKind, DocumentId, ExtensionId, FrontierBytes, LineageId, ProfileDeclaration, ProfileId,
|
||||
ReductionAlgorithmVersion, SchemaVersion, SemVer, SnapshotId,
|
||||
|
|
@ -172,3 +174,21 @@ pub struct TextBlob {
|
|||
/// Uncompressed blob payload carried inline.
|
||||
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,7 +980,12 @@ mod tests {
|
|||
|
||||
let mut initial = Manifest::empty(DocumentId([3; 16]));
|
||||
initial.lineage_id = Some(LineageId([4; 16]));
|
||||
let mut bundle = Bundle::create(MemStore::new(), FileUuid([1; 16]), initial)
|
||||
let mut bundle = Bundle::create(
|
||||
MemStore::new(),
|
||||
FileUuid([1; 16]),
|
||||
initial,
|
||||
crate::production_caps(),
|
||||
)
|
||||
.expect("a freshly created bundle with no canonical roots is valid");
|
||||
|
||||
bundle
|
||||
|
|
@ -1119,7 +1124,13 @@ mod tests {
|
|||
fn a_corrupt_operation_envelope_is_a_typed_error_not_a_panic() {
|
||||
let mut initial = Manifest::empty(DocumentId([5; 16]));
|
||||
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]]));
|
||||
bundle
|
||||
.commit(&[garbage_block], |ctx| {
|
||||
|
|
@ -1144,7 +1155,7 @@ mod tests {
|
|||
let corrupt_at = extension_root.offset as usize;
|
||||
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");
|
||||
match document_from_bundle(&corrupted) {
|
||||
Err(ProjectError::Bundle(_)) => {}
|
||||
|
|
|
|||
|
|
@ -152,7 +152,12 @@ pub fn serialize_document<S: BlockStore>(
|
|||
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();
|
||||
if let Some(base) = &document.canonical_base {
|
||||
|
|
@ -380,7 +385,8 @@ mod tests {
|
|||
let bundle = serialize_document(document, MemStore::new(), FileUuid([1; 16]))
|
||||
.expect("a well-formed document serializes");
|
||||
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]
|
||||
|
|
@ -632,4 +638,32 @@ mod tests {
|
|||
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
|
||||
is its own amendment with its own review round.
|
||||
|
||||
**IMPLEMENTED 2026-08-09, STAGED, and NOT YET ACCEPTED.** The implementation remains
|
||||
staged and uncommitted; only the amendments are committed.
|
||||
**IMPLEMENTED AND ACCEPTED 2026-08-09, by the repository owner after execution review
|
||||
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
|
||||
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
|
||||
files** — across all eight reviews.
|
||||
|
||||
**What remains is the owner's acceptance decision.** This document does not make it.
|
||||
Round 1's ratification was claimed by the author after a single round and withdrawn;
|
||||
**that precedent is why this block records the evidence and stops.**
|
||||
**The repository owner accepted the implementation on 2026-08-09, after execution
|
||||
review 8.** That decision is recorded here; it does not recast the evidence as proof of
|
||||
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
|
||||
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
|
||||
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 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
|
||||
and before committing, and never `git reset`/`restore`/`checkout`/`stash`.**
|
||||
|
||||
**Leave the work STAGED. Do not commit.** The execution report is then subject to
|
||||
**independent review before completion is accepted**, covering in particular
|
||||
**M7's three observations and its control**.
|
||||
**The executing agent left the work STAGED and did not commit.** The execution report
|
||||
was then subject to **independent review before completion was accepted**, covering in
|
||||
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.
|
||||
\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}
|
||||
|
||||
\begin{requirement}
|
||||
|
|
@ -16820,6 +16875,28 @@ layouts they own versus inherit:
|
|||
staleness. \sectionsc{Schema Versioning} gains the corresponding
|
||||
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
|
||||
\end{longtable}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue