Container major 1: the format epoch carries base provenance

FORMAT_MAJOR becomes 1 and FORMAT_MINOR restarts at 0. The decoder
stops being exact-major-only: it classifies three ways through a named
FormatEpoch carried on FixedHeader, so major 0 is decoded deliberately
as legacy rather than refused. Old readers already fail closed on an
unknown major, so that half needed no mechanism — which is why the major
is the right carrier, and why the header's immutability, fatal to
FORMAT_MINOR as a provenance field, is what makes it sound as an epoch
field.

The matrix: a major-0 bundle with no base may open; one carrying a base
is refused; one attempting to add a base is refused and told to repack.
That last row is the non-inheritance rule. Three errors, none of which
degrades to read-only: two permanent legacy/repack errors, and
ReductionAuthorityUnavailable, which is temporary, names P13-S27, and
must not say repack — a major-1 container is already the right epoch.

Until P13-S27 lands, both major-1 base boundaries are closed: opening a
major-1 bundle that already carries a base, and committing one into it.
Neither may be left open while the epoch asserts a validation that never
ran.

Text projection cannot mint a base. serialize_document staged a carried
base into a fresh bundle and build_manifest wrote it, so an old or
hand-authored document could be laundered straight through the boundary.
All three sides now refuse: projection, parsing, and a new dedicated
SerializeError variant — none of which existed to be "retained".
COMPANION_VERSION moves to 0.14.0 and the corpus is rebuilt to 20
vectors and ten rejection classes, with canonical_bases reach dropping
2 -> 0. That is a real capability loss and is recorded as one.

Corruption keeps precedence in both epochs: a corrupt major-1 base fails
as malformed, never as the temporary authority error a user would
reasonably retry.

All 11 mutations were run and observed, not reasoned about. M4 is the
signing one — with the legacy commit refusal removed, a legacy container
gains a base in place, which is exactly the counterexample that killed
FORMAT_MINOR. M11 confirms the third error is distinct while test 3
stays green, proving the mutation stayed inside the major-1 branches. M7
fails on both epoch halves. M8 falls through to
SerializeError::Bundle(ReductionAuthorityUnavailable), confirming the
text layer's own refusal is what the test asserts.

Two touch-table gaps surfaced during execution, both the same shape: a
.tex requirement addition moves hardcoded counts in
requirement_labels.rs, and the companion bump moves a second normative
version literal spelled version~0.13.0 rather than (0 13 0). Neither
file was in any touch table; the second was caught only because a test
exists for exactly that failure.

P13-S27 is unblocked — its pin 2a is resolved from outside, as its own
prohibition required — and inherits three obligations: both interim
refusals converted to validation, M8's deferred laundering
demonstration, and pin 3c's two suspended conformance assertions.
P13-S16 remains blocked on S27.

Workspace green at 1569.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
This commit is contained in:
Levi Neuwirth 2026-08-07 12:51:14 -04:00
parent 973297e1a1
commit bc06706e41
21 changed files with 1241 additions and 283 deletions

View File

@ -18,7 +18,7 @@ use crate::chunk::{
};
use crate::codec::DecodeError;
use crate::error::{BundleError, IntegrityAnomaly};
use crate::header::{FixedHeader, SLOT_A_OFFSET, SLOT_B_OFFSET};
use crate::header::{FixedHeader, FormatEpoch, SLOT_A_OFFSET, SLOT_B_OFFSET};
use crate::ids::{BlobId, FileUuid, ReductionAlgorithmVersion, SchemaVersion, WallClockTime};
use crate::manifest::{BlobRef, Manifest, ProfileDeclaration};
use crate::opindex::OperationIndex;
@ -408,6 +408,19 @@ impl<S: BlockStore> Bundle<S> {
"canonical base profile is not declared by the manifest",
)));
}
// Format-epoch matrix (`CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3,
// rows 2 and 5i). Corruption precedence runs first (both checks
// above): only once the base is structurally sound do we ask
// whether this epoch may open it at all. A legacy (major-0)
// container can never open with a base — the epoch is not
// retroactive (row 2, permanent). A major-1 container is the
// right epoch, but until P13-S27 lands there is no
// reduction-authority capability to validate it against (row 5i,
// interim — TEMPORARY, removed by P13-S27, pin 3a).
return Err(match header.epoch {
FormatEpoch::Legacy => BundleError::LegacyBundleHasCanonicalBase,
FormatEpoch::Current => BundleError::ReductionAuthorityUnavailable,
});
}
// An unknown *required* extension forces read-only (Chapter 8 §"Behavior
// Under Unknown Extensions").
@ -761,6 +774,28 @@ impl<S: BlockStore> Bundle<S> {
// least one declared profile.
let active_profile = validate_emittable_manifest(&manifest)?;
// Format-epoch matrix (`CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3, rows 3
// and 6i): a commit may never introduce (or replace) a canonical
// base. `self.manifest.canonical_base` is always `None` here — `open`
// and `create` both already refuse ever producing a live bundle whose
// base is `Some`, so any `Some` reaching this point is necessarily a
// fresh introduction. Row 3 — a legacy (major-0) container can never
// become base-bearing in place, because its header can never say the
// base was validated (permanent; the epoch is not inheritable). Row
// 6i — a major-1 container is the right epoch, but until P13-S27
// lands there is no reduction-authority capability to validate a
// newly introduced base against (interim — TEMPORARY, removed by
// P13-S27, pin 3a). Placed after `validate_emittable_manifest` (which
// already rejects a base whose profile is undeclared) so that a
// structurally malformed base is still reported as malformed, not
// masked by this categorical epoch refusal.
if manifest.canonical_base.is_some() {
return Err(match self.header.epoch {
FormatEpoch::Legacy => BundleError::LegacyBaseIntroductionRejected,
FormatEpoch::Current => BundleError::ReductionAuthorityUnavailable,
});
}
// Before publishing, validate that every canonical root the new manifest
// declares actually resolves to a present, hash-intact chunk of the right
// kind and shape (the new chunks are written and flushed; retained roots
@ -1489,49 +1524,51 @@ mod tests {
// Major 2: the MaterializedState embeds no data-model-major values,
// and re-stamping byte-identical content churns its address). With
// the Snapshot KIND now admitting the major-2 acceleration form, the
// per-role check must catch a mis-stamped base: read-only + anomaly,
// at commit and again on reopen.
let mut bundle = fresh_bundle();
let base = StagedChunk {
// per-role check (`mis_stamped_canonical_base`) must catch a
// mis-stamped base.
//
// CONTRACT_FORMAT_EPOCH_MAJOR1.md pin 3a (the "named trap"): this test
// used to commit the mis-stamped base directly and assert read-only
// immediately, both at commit and at reopen. Neither half is
// reachable any more — pin 3a's epoch guard (rows 2/3/5i/6i, both
// exercised above in this module) now refuses *any* canonical base,
// mis-stamped or not, before `mis_stamped_canonical_base` is ever
// consulted, at both `open` and `commit`. That guard is a different
// axis (container format major) from this one (data-model schema
// major) and does not contradict pin 4 — `ReductionAuthorityUnavailable`
// must not degrade to read-only — so the fix is not to weaken pin 4
// toward this test. It is to test what remains reachable:
// `mis_stamped_canonical_base` itself, a pure function of a
// `Manifest`, independent of the now-categorical bundle-lifecycle
// refusal that sits in front of it.
let mut m = Manifest::empty(DocumentId([9; 16]));
let payload = vec![7u8, 7, 7];
let hash = chunk_content_hash(ChunkKind::Snapshot, SchemaVersion::V1, &payload);
let root = ChunkRef {
id: ChunkId(hash),
kind: ChunkKind::Snapshot,
schema_version: SchemaVersion::V1,
payload: vec![7u8, 7, 7],
offset: BODY_START,
compressed_length: payload.len() as u64,
uncompressed_length: payload.len() as u64,
compression: CompressionAlgorithm::None,
hash,
};
bundle
.commit(&[base], |ctx| {
let mut m = ctx.previous_manifest.clone();
let root = ctx.new_chunks[0];
let mut sid = [0u8; 16];
sid.copy_from_slice(&root.hash.as_bytes()[..16]);
m.canonical_base = Some(SnapshotRef {
snapshot_id: SnapshotId(sid),
covers_causal_frontier: FrontierBytes::from_bytes(vec![]),
reduction_algorithm_version: ReductionAlgorithmVersion(0),
profile_id: ProfileId::Full,
hash: root.hash,
root,
});
m
})
.expect("a structurally-valid mis-stamped base is publishable");
assert!(
bundle.is_read_only(),
"a mis-stamped canonical base forces read-only at commit"
m.canonical_base = Some(SnapshotRef {
snapshot_id: SnapshotId([1; 16]),
covers_causal_frontier: FrontierBytes::empty(),
reduction_algorithm_version: ReductionAlgorithmVersion(0),
profile_id: ProfileId::Full,
hash: root.hash,
root,
});
assert_eq!(
mis_stamped_canonical_base(&m),
Some(1),
"a canonical base root stamped above major 0 is still flagged by the per-role \
check, even though pin 3a's epoch guard now intercepts every base before this \
check ever runs"
);
assert!(bundle.anomalies().iter().any(|a| matches!(
a,
IntegrityAnomaly::UnsupportedCanonicalChunkMajor { schema_major: 1 }
)));
let image = bundle.into_store().into_bytes();
let reopened = Bundle::open(MemStore::from_bytes(image)).expect("reopen");
assert!(
reopened.is_read_only(),
"a mis-stamped canonical base forces read-only at open"
);
assert!(reopened.anomalies().iter().any(|a| matches!(
a,
IntegrityAnomaly::UnsupportedCanonicalChunkMajor { schema_major: 1 }
)));
}
fn fresh_bundle() -> Bundle<MemStore> {
@ -1543,6 +1580,307 @@ mod tests {
.unwrap()
}
// -----------------------------------------------------------------
// CONTRACT_FORMAT_EPOCH_MAJOR1.md: format-epoch matrix (pins 1-4).
//
// After this rung no `create`/`commit` path may ever produce a
// base-bearing bundle (pin 3c): `create` already refused one, and rows
// 3/6i below now refuse committing one into either epoch, so a
// base-bearing fixture can only be built as a hand-crafted image, the
// same technique `craft_image`/`craft_image_with_manifest_bytes` already
// use further down this file.
// -----------------------------------------------------------------
/// A fixed header at `format_major`, bypassing `FixedHeader::new` (which
/// always stamps the *current* epoch) so a legacy (major-0) fixture can
/// be built too. Same technique as `craft_image` below: public fields,
/// public `encode()`.
fn header_for_major(format_major: u16, file_uuid: FileUuid) -> FixedHeader {
let epoch = if format_major == 0 {
FormatEpoch::Legacy
} else {
FormatEpoch::Current
};
FixedHeader {
format_major,
format_minor: 0,
header_length: crate::header::HEADER_LEN as u32,
superblock_a_offset: SLOT_A_OFFSET,
superblock_b_offset: SLOT_B_OFFSET,
file_uuid,
epoch,
}
}
/// A minimal valid bundle image at `format_major`, with no canonical base
/// (matrix rows 1 and 4).
fn craft_image_epoch(format_major: u16) -> Vec<u8> {
let mut m = Manifest::empty(DocumentId([1; 16]));
m.profile_declarations = vec![ProfileDeclaration::full()];
let payload = m.encode();
let mut image = vec![0u8; BODY_START as usize];
image.extend_from_slice(&payload);
let sb = Superblock {
generation: 0,
manifest_offset: BODY_START,
manifest_length: payload.len() as u64,
manifest_hash: manifest_chunk_hash(&payload),
manifest_schema_version: SchemaVersion::V0,
reduction_algorithm_version: ReductionAlgorithmVersion(0),
profile_id: ProfileId::Full,
commit_state: CommitState::Committed,
commit_timestamp: WallClockTime(0),
};
image[0..crate::header::HEADER_LEN as usize]
.copy_from_slice(&header_for_major(format_major, FileUuid([1; 16])).encode());
image[SLOT_A_OFFSET as usize..SLOT_A_OFFSET as usize + SUPERBLOCK_LEN as usize]
.copy_from_slice(&sb.encode());
image
}
/// A bundle image at `format_major` carrying a canonical base wired
/// directly into the manifest and superblock bytes, bypassing
/// `create`/`commit` entirely — the only way left to build this fixture
/// (pin 3c). `base_schema_major` mis-stamps the base's own root
/// independent of the container's format major; `base_version` and
/// `superblock_version` are equal for a self-consistent base (rows
/// 2/5i/11) and unequal for the corrupt-base precedence fixture (test 7).
fn craft_image_with_base(
format_major: u16,
base_schema_major: u16,
base_version: ReductionAlgorithmVersion,
superblock_version: ReductionAlgorithmVersion,
) -> Vec<u8> {
let base_schema = SchemaVersion::new(base_schema_major, 0);
let snap_payload = vec![7u8, 7, 7];
let snap_hash = chunk_content_hash(ChunkKind::Snapshot, base_schema, &snap_payload);
let root = ChunkRef {
id: ChunkId(snap_hash),
kind: ChunkKind::Snapshot,
schema_version: base_schema,
offset: BODY_START,
compressed_length: snap_payload.len() as u64,
uncompressed_length: snap_payload.len() as u64,
compression: CompressionAlgorithm::None,
hash: snap_hash,
};
let mut m = Manifest::empty(DocumentId([1; 16]));
m.profile_declarations = vec![ProfileDeclaration::full()];
m.canonical_base = Some(SnapshotRef {
snapshot_id: SnapshotId([1; 16]),
covers_causal_frontier: FrontierBytes::empty(),
reduction_algorithm_version: base_version,
profile_id: ProfileId::Full,
hash: root.hash,
root,
});
let manifest_payload = m.encode();
let manifest_offset = BODY_START + snap_payload.len() as u64;
let mut image = vec![0u8; BODY_START as usize];
image.extend_from_slice(&snap_payload);
image.extend_from_slice(&manifest_payload);
let sb = Superblock {
generation: 0,
manifest_offset,
manifest_length: manifest_payload.len() as u64,
manifest_hash: manifest_chunk_hash(&manifest_payload),
manifest_schema_version: SchemaVersion::V0,
reduction_algorithm_version: superblock_version,
profile_id: ProfileId::Full,
commit_state: CommitState::Committed,
commit_timestamp: WallClockTime(0),
};
image[0..crate::header::HEADER_LEN as usize]
.copy_from_slice(&header_for_major(format_major, FileUuid([1; 16])).encode());
image[SLOT_A_OFFSET as usize..SLOT_A_OFFSET as usize + SUPERBLOCK_LEN as usize]
.copy_from_slice(&sb.encode());
image
}
/// `Bundle<S>` has no `Debug` impl, so `Result::expect_err`/`unwrap_err`
/// cannot be used directly on `Bundle::open`'s result; this extracts the
/// error by hand.
fn open_err(image: Vec<u8>, context: &str) -> BundleError {
match Bundle::open(MemStore::from_bytes(image)) {
Err(e) => e,
Ok(_) => panic!("{context}"),
}
}
#[test]
fn a_legacy_major_0_bundle_without_a_base_opens() {
// Matrix row 1: a legacy container with no base opens cleanly —
// nothing unverifiable is exposed.
let image = craft_image_epoch(0);
let bundle = Bundle::open(MemStore::from_bytes(image))
.expect("a legacy bundle without a base opens");
assert!(!bundle.is_read_only());
assert!(bundle.anomalies().is_empty());
assert_eq!(bundle.header().epoch, FormatEpoch::Legacy);
}
#[test]
fn a_legacy_major_0_bundle_with_a_base_is_rejected() {
// Matrix row 2: a legacy container already carrying a
// (self-consistent) base is a hard reject at open — a pre-epoch base
// was never validated against a reduction authority.
let image = craft_image_with_base(
0,
0,
ReductionAlgorithmVersion(0),
ReductionAlgorithmVersion(0),
);
let err = open_err(image, "a legacy base-bearing bundle must not open");
assert!(matches!(err, BundleError::LegacyBundleHasCanonicalBase));
assert!(!matches!(err, BundleError::LegacyBaseIntroductionRejected));
assert!(!matches!(err, BundleError::ReductionAuthorityUnavailable));
}
#[test]
fn adding_a_base_to_a_legacy_bundle_is_rejected_and_names_repack() {
// Matrix row 3: a legacy container that opened clean under row 1
// must not become base-bearing in place — the epoch is not
// inheritable, because its header can never say the base was
// validated.
let image = craft_image_epoch(0);
let mut bundle = Bundle::open(MemStore::from_bytes(image))
.expect("a legacy bundle without a base opens");
let staged = StagedChunk {
kind: ChunkKind::Snapshot,
schema_version: SchemaVersion::V0,
payload: vec![7u8, 7, 7],
};
let err = bundle
.commit(&[staged], |ctx| {
let mut m = ctx.previous_manifest.clone();
let root = ctx.new_chunks[0];
m.canonical_base = Some(SnapshotRef {
snapshot_id: SnapshotId([1; 16]),
covers_causal_frontier: FrontierBytes::empty(),
reduction_algorithm_version: ReductionAlgorithmVersion(0),
profile_id: ProfileId::Full,
hash: root.hash,
root,
});
m
})
.expect_err("committing a base into a legacy bundle must be refused");
assert!(matches!(err, BundleError::LegacyBaseIntroductionRejected));
assert!(!matches!(err, BundleError::LegacyBundleHasCanonicalBase));
assert!(!matches!(err, BundleError::ReductionAuthorityUnavailable));
assert!(
err.to_string().to_lowercase().contains("repack"),
"row-3 error must name repack: {err}"
);
assert_eq!(
bundle.generation(),
0,
"the refused commit did not advance the bundle"
);
}
#[test]
fn a_major_1_bundle_round_trips_and_refuses_to_introduce_a_base() {
// Matrix row 4 (round-trip half) + row 6i (refusal half). Renamed
// from "..._validates_its_base", which pin 3a forbids claiming until
// P13-S27 lands: this rung does not validate a base, it refuses one
// outright.
let mut bundle = fresh_bundle();
assert_eq!(bundle.header().epoch, FormatEpoch::Current);
// Round trip: ordinary non-base-bearing history commits and reopens.
let block = StagedChunk::operation_block(block::encode_block(&[vec![1u8, 2, 3]]));
bundle
.commit(&[block], |ctx| {
let mut m = ctx.previous_manifest.clone();
m.operation_roots.push(ctx.new_chunks[0]);
m
})
.expect("a major-1 bundle commits ordinary non-base-bearing history");
let image = bundle.into_store().into_bytes();
let mut reopened =
Bundle::open(MemStore::from_bytes(image)).expect("a major-1 bundle round trips");
assert!(!reopened.is_read_only());
assert_eq!(reopened.manifest().operation_roots.len(), 1);
// Refusal: introducing a base is refused (row 6i), distinctly from
// both legacy errors.
let staged = StagedChunk {
kind: ChunkKind::Snapshot,
schema_version: SchemaVersion::V0,
payload: vec![9u8, 9, 9],
};
let err = reopened
.commit(&[staged], |ctx| {
let mut m = ctx.previous_manifest.clone();
let root = ctx.new_chunks[0];
m.canonical_base = Some(SnapshotRef {
snapshot_id: SnapshotId([2; 16]),
covers_causal_frontier: FrontierBytes::empty(),
reduction_algorithm_version: ReductionAlgorithmVersion(0),
profile_id: ProfileId::Full,
hash: root.hash,
root,
});
m
})
.expect_err(
"a major-1 bundle must refuse to introduce a base during the S28 -> P13-S27 interval",
);
assert!(matches!(err, BundleError::ReductionAuthorityUnavailable));
assert!(!matches!(err, BundleError::LegacyBundleHasCanonicalBase));
assert!(!matches!(err, BundleError::LegacyBaseIntroductionRejected));
}
#[test]
fn a_corrupt_base_fails_as_malformed_before_any_epoch_error() {
// Pin 3's precedence rule, in BOTH epochs: a base whose version
// disagrees with its superblock's is corrupt and must fail with the
// existing malformed-bundle error — never row 2's legacy error, and
// never row 5i's `ReductionAuthorityUnavailable`. Collapsing
// tampering into staleness would erase the distinction P13-S27's
// authority check rests on. Renamed from `..._corrupt_legacy_base...`:
// the major-1 half is the one an earlier draft missed.
for format_major in [0u16, 1u16] {
let image = craft_image_with_base(
format_major,
0,
ReductionAlgorithmVersion(1),
ReductionAlgorithmVersion(2), // disagrees with the base's own version
);
let err = open_err(image, "a corrupt base must not open");
assert!(
matches!(err, BundleError::Decode(DecodeError::Malformed(_))),
"format_major {format_major}: expected the malformed-bundle error, got {err:?}"
);
assert!(!matches!(err, BundleError::LegacyBundleHasCanonicalBase));
assert!(!matches!(err, BundleError::ReductionAuthorityUnavailable));
}
}
#[test]
fn opening_a_major_1_bundle_that_already_carries_a_base_is_refused() {
// Matrix row 5i, the read-side branch pin 3a adds — an earlier draft
// omitted it entirely.
let image = craft_image_with_base(
1,
0,
ReductionAlgorithmVersion(0),
ReductionAlgorithmVersion(0),
);
let err = open_err(
image,
"a major-1 bundle already carrying a base must not open during the interval",
);
assert!(matches!(err, BundleError::ReductionAuthorityUnavailable));
assert!(!matches!(err, BundleError::LegacyBundleHasCanonicalBase));
assert!(!matches!(err, BundleError::LegacyBaseIntroductionRejected));
}
// -----------------------------------------------------------------
// G-minor (`spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4): s10, s11, s12.
// -----------------------------------------------------------------

View File

@ -116,6 +116,40 @@ pub enum BundleError {
/// extension, or a recovery/anomaly open). Chapter 8 §"Behavior Under
/// Unknown Extensions" / §"Superblock Selection".
ReadOnly,
/// A legacy (format major 0) container's manifest already carries a
/// canonical base (`spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md` matrix row 2).
/// A pre-epoch base was never validated against a reduction authority, so
/// it is not safe materialized state — the document already contains
/// unverifiable canonical state. **Permanent**: legacy containers never
/// gain that validation retroactively. The fix is to repack into a fresh
/// major-1 bundle; that flow is not built by this rung (pin 5) and this
/// error MUST NOT degrade to a read-only open — a pre-authority base is
/// not a restricted-but-correct view.
LegacyBundleHasCanonicalBase,
/// A commit into a legacy (format major 0) container attempted to add or
/// replace the canonical base (matrix row 3). The document itself is
/// fine, but this operation cannot be performed in this container: a
/// bundle that opened clean under row 1 must not become base-bearing in
/// place, because its header can never say it was validated — the epoch
/// is not inheritable. **Permanent**, like
/// [`BundleError::LegacyBundleHasCanonicalBase`]. The fix is to repack
/// into a fresh major-1 bundle; not built by this rung (pin 5), and this
/// error MUST NOT degrade to a read-only open.
LegacyBaseIntroductionRejected,
/// A major-1 container's canonical base cannot yet be validated: matrix
/// rows 5i/6i, opening a container that already carries a base, or
/// committing one into it. The container is the **right** epoch — this is
/// not a request to repack — but no reduction-authority capability exists
/// yet to validate the base against. **Temporary**: `P13-S27` supplies
/// that capability and replaces both branches with real validation, not
/// this categorical refusal. Never mentions repack (repacking a
/// already-correct-epoch container would be wrong advice), and MUST NOT
/// degrade to a read-only open — a pre-authority base is not a
/// restricted-but-correct view.
ReductionAuthorityUnavailable,
}
impl core::fmt::Display for BundleError {
@ -187,6 +221,18 @@ impl core::fmt::Display for BundleError {
}
BundleError::Decode(e) => write!(f, "decode error: {e}"),
BundleError::ReadOnly => f.write_str("bundle is open read-only; edits are refused"),
BundleError::LegacyBundleHasCanonicalBase => f.write_str(
"legacy (format major 0) bundle carries a canonical base, which was never \
validated against a reduction authority; repack into a fresh major-1 bundle",
),
BundleError::LegacyBaseIntroductionRejected => f.write_str(
"cannot add or replace a canonical base in a legacy (format major 0) bundle; \
repack into a fresh major-1 bundle",
),
BundleError::ReductionAuthorityUnavailable => f.write_str(
"this major-1 container's canonical base cannot yet be validated: no \
reduction-authority capability exists until P13-S27 lands",
),
}
}
}

View File

@ -36,10 +36,39 @@ pub const SLOT_A_OFFSET: u64 = 64;
pub const SLOT_B_OFFSET: u64 = 320;
/// The format major version this crate writes and understands.
pub const FORMAT_MAJOR: u16 = 0;
///
/// **Format epoch** (`spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md`): a major-1
/// container is one whose every base-bearing commit was validated against a
/// supplied reduction authority. Major 0 is the pre-epoch **legacy** format —
/// still decoded, deliberately, but never trusted to carry a canonical base
/// (see [`FormatEpoch`]). A new major restarts minor numbering, which is why
/// [`FORMAT_MINOR`] resets to `0` here rather than continuing from legacy's
/// `1`.
pub const FORMAT_MAJOR: u16 = 1;
/// The format minor version this crate writes.
pub const FORMAT_MINOR: u16 = 1;
///
/// Reset to `0` by the major-1 epoch bump (`spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md`
/// pin 1): a new major restarts minor numbering.
pub const FORMAT_MINOR: u16 = 0;
/// Which format-epoch a decoded header belongs to
/// (`spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 2). A named enum rather than a
/// boolean so the legacy case is a value the type system carries — every
/// consumer of the epoch matches this, rather than re-deriving
/// `format_major == 0` at each use site.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum FormatEpoch {
/// Format major 0: decoded deliberately, for backward compatibility, but
/// never trusted to carry a canonical base (the epoch is not retroactive
/// — `CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3, row 2/3).
Legacy,
/// Format major 1: the current epoch. Every base-bearing commit is meant
/// to be validated against a supplied reduction authority — the
/// capability itself lands with P13-S27; until then this rung refuses
/// base introduction outright (pin 3a).
Current,
}
/// Byte range covered by the header CRC: everything before the CRC field.
const HEADER_CRC_RANGE: usize = 60;
@ -59,6 +88,9 @@ pub struct FixedHeader {
pub superblock_b_offset: u64,
/// Physical-bundle UUID, set at creation, changed on Save As.
pub file_uuid: FileUuid,
/// The format epoch this header's major classifies to (pin 2). Carried
/// on the header rather than re-derived at each call site.
pub epoch: FormatEpoch,
}
impl FixedHeader {
@ -71,6 +103,7 @@ impl FixedHeader {
superblock_a_offset: SLOT_A_OFFSET,
superblock_b_offset: SLOT_B_OFFSET,
file_uuid,
epoch: FormatEpoch::Current,
}
}
@ -116,12 +149,20 @@ impl FixedHeader {
let _magic = r.take_array::<8>()?;
let format_major = r.get_u16()?;
let format_minor = r.get_u16()?;
if format_major != FORMAT_MAJOR {
return Err(BundleError::UnsupportedFormatVersion {
major: format_major,
minor: format_minor,
});
}
// Pin 2's explicit three-way classification: 0 is legacy (decoded
// deliberately, marked as such), FORMAT_MAJOR is current, anything
// else is unsupported — unchanged from the pre-epoch exact-major
// rejection for that third arm.
let epoch = match format_major {
0 => FormatEpoch::Legacy,
FORMAT_MAJOR => FormatEpoch::Current,
_ => {
return Err(BundleError::UnsupportedFormatVersion {
major: format_major,
minor: format_minor,
})
}
};
let header_length = r.get_u32()?;
if header_length != HEADER_LEN as u32 {
return Err(BundleError::UnsupportedHeaderLength {
@ -147,6 +188,7 @@ impl FixedHeader {
superblock_a_offset,
superblock_b_offset,
file_uuid,
epoch,
})
}
}
@ -155,6 +197,14 @@ impl FixedHeader {
mod tests {
use super::*;
#[test]
fn format_epoch_constants_are_major_1_minor_0() {
// Gate 7 (`CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 1): the epoch bump,
// asserted in a test rather than only by reading the constants.
assert_eq!(FORMAT_MAJOR, 1);
assert_eq!(FORMAT_MINOR, 0);
}
#[test]
fn header_round_trips() {
let h = FixedHeader::new(FileUuid([0xAB; 16]));
@ -192,6 +242,21 @@ mod tests {
));
}
#[test]
fn an_unknown_major_is_still_unsupported_format_version() {
// Pin 2's third arm: neither 0 (legacy) nor FORMAT_MAJOR (current) —
// still hard-rejected, exactly as the pre-epoch exact-major check was
// (`CONTRACT_FORMAT_EPOCH_MAJOR1.md`).
let mut bytes = FixedHeader::new(FileUuid::ZERO).encode();
bytes[8..10].copy_from_slice(&2u16.to_le_bytes()); // format_major = 2
let crc = crc32c(&bytes[0..60]);
bytes[60..64].copy_from_slice(&crc.to_le_bytes());
assert!(matches!(
FixedHeader::decode(&bytes),
Err(BundleError::UnsupportedFormatVersion { major: 2, minor: 0 })
));
}
#[test]
fn reserved_bytes_are_ignored_on_read() {
// A future minor version may use reserved bytes; current readers must

View File

@ -81,7 +81,7 @@ pub use crc::crc32c;
pub use epiphany_determinism::{ChunkId, ContentHash};
pub use error::{BundleError, IntegrityAnomaly};
pub use header::{
FixedHeader, FORMAT_MAJOR, FORMAT_MINOR, HEADER_LEN, SLOT_A_OFFSET, SLOT_B_OFFSET,
FixedHeader, FormatEpoch, FORMAT_MAJOR, FORMAT_MINOR, HEADER_LEN, SLOT_A_OFFSET, SLOT_B_OFFSET,
};
pub use ids::{
BlobId, DocumentId, ExtensionId, FileUuid, FrontierBytes, LineageId, ManifestId,

View File

@ -17,8 +17,11 @@
//! deliberately not `std::env::temp_dir()`, which is commonly tmpfs on Linux,
//! where fsync is a near-no-op and the commit budget would be measured against
//! RAM. The corpus is moderate: 1,000 generated operation envelopes packed
//! into operation blocks plus a canonical `MaterializedState` snapshot wired
//! as the manifest's `canonical_base`. Both
//! into operation blocks plus a canonical `MaterializedState` snapshot. That
//! snapshot's manifest placement (`canonical_base`) is suspended for the S28
//! -> P13-S27 interval (`CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3c) — see
//! `Fixture::base_root`'s doc comment — so it is staged and read back
//! directly by `ChunkRef` instead. Both
//! are expected to **Pass** today, so a regression fails `cargo bench` loudly.
//! The read row's honesty note: the spec sizes its 200 ms against a 100-page
//! orchestral score; no such corpus generator exists yet, so this row is the
@ -34,9 +37,9 @@
//! envelope block, i.e. block append + manifest rewrite + superblock flip,
//! every write fsync'd. This mirrors `bundle_harness`'s commit driver.
//! * **open_bootstrap_read** — `FileStore::open` + `Bundle::open` (superblock
//! selection + header + manifest decode) + reading the `canonical_base`
//! snapshot and every operation block — the bytes a first interactive frame
//! needs.
//! selection + header + manifest decode) + reading the cold-reduction
//! snapshot (by direct `ChunkRef`, see above) and every operation block —
//! the bytes a first interactive frame needs.
//!
//! Criterion measures; the budget gate in `main` asserts (see
//! `epiphany_testkit::budget` for the Pass/Xfail semantics and the documented
@ -50,9 +53,8 @@ use std::time::Duration;
use criterion::{BatchSize, Criterion};
use epiphany_bundle::{
pack_operation_blocks, Bundle, ChunkKind, CommitContext, DocumentId, FileStore, FileUuid,
FrontierBytes, Manifest, MemStore, ProfileId, ReductionAlgorithmVersion, SchemaVersion,
SnapshotId, SnapshotRef, StagedChunk,
pack_operation_blocks, Bundle, ChunkKind, ChunkRef, CommitContext, DocumentId, FileStore,
FileUuid, Manifest, MemStore, SchemaVersion, StagedChunk,
};
use epiphany_determinism::CanonicalEncode;
use epiphany_ops::{OperationEnvelope, OperationSet};
@ -71,8 +73,21 @@ const EDIT_ENVELOPES: usize = 4;
/// Everything the two rows measure against, built once from a fixed seed.
struct Fixture {
/// The committed base image (blocks + canonical-base snapshot).
/// The committed base image (blocks + the corpus's cold-reduction
/// snapshot).
base_image: Vec<u8>,
/// The cold-reduction snapshot's `ChunkRef`.
///
/// **Suspended manifest placement** (`CONTRACT_FORMAT_EPOCH_MAJOR1.md`
/// pin 3c, amended 2026-08-07): this used to be `manifest.canonical_base`
/// — its correct semantic home — but pin 3a's format-epoch matrix refuses
/// every base introduction and every base-bearing open during the S28 ->
/// P13-S27 interval. The ref is carried here instead and read back
/// directly via `Bundle::read_chunk`, exactly as
/// `epiphany_testkit::roundtrip::assert_reduction_serialization_stable`
/// now does. **Not** re-homed to `acceleration_snapshots` — nothing in
/// `epiphany-bundle` verifies that field.
base_root: ChunkRef,
/// The typical edit, staged (one small operation block).
edit: Vec<StagedChunk>,
/// Temp-dir file paths: one per row so commit growth never skews reads.
@ -99,8 +114,9 @@ fn staged_blocks(envelopes: &[OperationEnvelope]) -> Vec<StagedChunk> {
}
/// Builds the moderate base corpus: 1,000 envelopes committed as operation
/// blocks, then their cold reduction committed as a `Snapshot` chunk wired to
/// the manifest's `canonical_base` (the roundtrip harness's snapshot shape).
/// blocks, then their cold reduction committed as a `Snapshot` chunk — staged
/// but not wired into the manifest (pin 3c; see `Fixture::base_root`'s doc
/// comment).
fn build_fixture(dir: &Path) -> Fixture {
let mut rng = Rng::new(0x00F1_B0DE_0001);
let envelopes = generators::operation_envelopes(&mut rng, BASE_ENVELOPES, 3, 40, 40);
@ -123,27 +139,18 @@ fn build_fixture(dir: &Path) -> Fixture {
schema_version: SchemaVersion::V0,
payload: canonical,
};
let frontier = generators::frontier_bytes(&envelopes);
let mut base_root = None;
bundle
.commit(&[snapshot], |ctx| {
let mut manifest = ctx.previous_manifest.clone();
let root = ctx.new_chunks[0];
let mut sid = [0u8; 16];
sid.copy_from_slice(&root.hash.as_bytes()[..16]);
manifest.canonical_base = Some(SnapshotRef {
snapshot_id: SnapshotId(sid),
covers_causal_frontier: FrontierBytes::from_bytes(frontier.clone()),
reduction_algorithm_version: ReductionAlgorithmVersion(0),
profile_id: ProfileId::Full,
hash: root.hash,
root,
});
manifest
base_root = Some(ctx.new_chunks[0]);
ctx.previous_manifest.clone()
})
.expect("commit canonical-base snapshot");
.expect("commit cold-reduction snapshot");
let base_root = base_root.expect("commit ran the build closure");
Fixture {
base_image: bundle.into_store().into_bytes(),
base_root,
edit: staged_blocks(&edit_envelopes),
commit_path: dir.join("commit.epb"),
read_path: dir.join("read.epb"),
@ -165,17 +172,15 @@ fn typical_edit_commit(mut bundle: Bundle<FileStore>, edit: &[StagedChunk]) -> u
}
/// The timed read: open (superblock selection + manifest decode) + the
/// bootstrap chunks — canonical-base snapshot and every operation block.
fn open_bootstrap_read(path: &Path) -> usize {
/// bootstrap chunks — the cold-reduction snapshot (read directly by its
/// `ChunkRef`; see `Fixture::base_root`'s doc comment for why it is not
/// `manifest.canonical_base` right now) and every operation block.
fn open_bootstrap_read(path: &Path, base_root: &ChunkRef) -> usize {
let bundle = Bundle::open(FileStore::open(path).expect("open store")).expect("open bundle");
let manifest = bundle.manifest();
let mut bytes = 0usize;
let base = manifest
.canonical_base
.as_ref()
.expect("the fixture wires a canonical base");
bytes += bundle
.read_chunk(&base.root)
.read_chunk(base_root)
.expect("snapshot chunk reads")
.len();
for root in &manifest.operation_roots {
@ -206,7 +211,7 @@ fn criterion_measurements(criterion: &mut Criterion, fixture: &Fixture, quick: b
fs::write(&fixture.read_path, &fixture.base_image).expect("write read-row image");
group.bench_function("open_bootstrap_read", |b| {
b.iter(|| open_bootstrap_read(&fixture.read_path))
b.iter(|| open_bootstrap_read(&fixture.read_path, &fixture.base_root))
});
group.finish();
@ -223,7 +228,7 @@ fn budget_gate(fixture: &Fixture, quick: bool) -> Vec<budget::GateReport> {
let read_median = budget::median_time(
if quick { 8 } else { 25 },
|| (),
|()| open_bootstrap_read(&fixture.read_path),
|()| open_bootstrap_read(&fixture.read_path, &fixture.base_root),
);
vec![
budget::latency_gate(

View File

@ -225,14 +225,28 @@ fn canonical_score_bytes(envelopes: &[OperationEnvelope]) -> Vec<u8> {
/// (acceptance criterion 4): the operation
/// set reduces to canonical bytes; re-reducing the same set yields byte-identical
/// bytes; and those bytes survive content-addressed storage in a real bundle —
/// stored as a `Snapshot` chunk referenced by the manifest's `canonical_base`
/// (its correct semantic home), hash-verified on reopen and read back
/// stored as a `Snapshot` chunk, hash-verified on reopen and read back
/// byte-identically.
///
/// The snapshot's `covers_causal_frontier` is the frontier the snapshot actually
/// materializes ([`crate::generators::frontier_bytes`] over the reduced
/// envelopes), so it is semantically consistent — not a falsely-empty frontier
/// that would invite a replay layer to reapply already-materialized effects.
/// **Canonical-base wiring suspended** (`CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin
/// 3c, amended 2026-08-07). The snapshot's correct semantic home is the
/// manifest's `canonical_base` — that has not changed — but pin 3a's
/// format-epoch matrix refuses every base introduction (`Bundle::commit`) and
/// every base-bearing open (`Bundle::open`) during the S28 → P13-S27
/// interval, in both format epochs. So this harness cannot wire the snapshot
/// there right now: it stages the snapshot as an ordinary chunk instead and
/// carries the resulting `ChunkRef` across the reopen out of band, reading it
/// back directly via [`Bundle::read_chunk`] (which hash-verifies any
/// `ChunkRef`, not only a canonical one). The
/// serialize → load → decode → reserialize cycle below is unchanged; only the
/// snapshot's manifest placement is suspended. **Not** re-homed to
/// `acceleration_snapshots` — that field is verified nowhere in
/// `epiphany-bundle` (not in `open`, not in `verify_canonical_chunks`), so a
/// reference there would verify nothing while looking like preserved
/// coverage. Two assertions lapse until P13-S27 restores the wiring:
/// `verify_canonical_chunks`'s base branch (its `base.hash != base.root.hash`
/// cross-check), and the reopened manifest actually carrying the base. Both
/// are owed back in `spec/CONTRACT_P13S27_REDUCTION_AUTHORITY.md`.
///
/// After reopen, the snapshot payload is decoded through
/// [`MaterializedState::decode_canonical`], compared structurally with the
@ -248,9 +262,13 @@ pub fn assert_reduction_serialization_stable(envelopes: &[OperationEnvelope], se
"re-reduction changed the canonical score bytes"
);
// serialize: stage the canonical state as a real **Snapshot** chunk and
// reference it from the manifest's `canonical_base` — its correct semantic
// home (a materialized snapshot), with the right chunk kind.
// serialize: stage the canonical state as a real **Snapshot** chunk.
// P13-S27 / CONTRACT_FORMAT_EPOCH_MAJOR1.md pin 3c: the manifest's
// `canonical_base` is the snapshot's correct semantic home, but that
// wiring is suspended for the S28 -> P13-S27 interval (see the doc
// comment above) — the chunk is staged but not referenced from any
// manifest root, and its `ChunkRef` is captured directly from the commit
// closure instead.
let mut rng = Rng::new(seed);
let uuid = FileUuid(rng.array16());
let doc = DocumentId(rng.array16());
@ -261,42 +279,26 @@ pub fn assert_reduction_serialization_stable(envelopes: &[OperationEnvelope], se
schema_version: SchemaVersion::V0,
payload: canonical.clone(),
};
let mut snapshot_root = None;
bundle
.commit(&[snapshot], |ctx| {
let mut m = ctx.previous_manifest.clone();
let root = ctx.new_chunks[0];
let mut sid = [0u8; 16];
sid.copy_from_slice(&root.hash.as_bytes()[..16]);
m.canonical_base = Some(SnapshotRef {
snapshot_id: SnapshotId(sid),
// The frontier the snapshot actually materializes (covering every
// reduced envelope), not a falsely-empty one.
covers_causal_frontier: FrontierBytes::from_bytes(generators::frontier_bytes(
envelopes,
)),
reduction_algorithm_version: ReductionAlgorithmVersion(0),
profile_id: ProfileId::Full,
hash: root.hash,
root,
});
m
snapshot_root = Some(ctx.new_chunks[0]);
ctx.previous_manifest.clone()
})
.expect("commit snapshot");
let snapshot_root = snapshot_root.expect("commit ran the build closure");
let image = bundle.into_store().into_bytes();
// load: reopen from exactly those bytes; the snapshot chunk is hash-verified
// on open and read back byte-identically.
// load: reopen from exactly those bytes; the snapshot chunk is read back
// directly by its `ChunkRef` (not through `manifest.canonical_base`,
// suspended above) — `read_chunk` hash-verifies any `ChunkRef`, so the
// cycle's integrity guarantee is unchanged.
let reopened = Bundle::open(MemStore::from_bytes(image)).expect("reopen bundle");
reopened
.verify_canonical_chunks()
.expect("canonical chunks intact");
let base = reopened
.manifest()
.canonical_base
.as_ref()
.expect("a canonical base");
let loaded = reopened
.read_chunk(&base.root)
.read_chunk(&snapshot_root)
.expect("read snapshot chunk back");
assert_eq!(
loaded, canonical,

View File

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

View File

@ -56,7 +56,18 @@ use epiphany_ops::OperationEnvelope;
/// and `create-view` to the `kind` production — the same reasoning as every
/// prior kind append: extending the grammar without moving this constant
/// would leave two incompatible grammars both claiming `(0 11 0)`.
pub const COMPANION_VERSION: (u32, u32, u32) = (0, 13, 0);
///
/// Bumped again 0.12.0 → 0.13.0 by the genesis tranche G3b, which appended
/// `create-measure` to the `kind` production.
///
/// Bumped again 0.13.0 → 0.14.0 by `spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md`
/// pin 3b: a canonical base can no longer be projected, parsed, or
/// serialized by this companion at all — refusing a document the companion
/// previously serialized is a semantic change, so the bump is load-bearing,
/// not cosmetic. The `canonical-base` grammar production is retained (it is
/// what the refusal is defined against, and what a future rebuild/repack
/// flow will emit), but no document containing it round-trips.
pub const COMPANION_VERSION: (u32, u32, u32) = (0, 14, 0);
/// A parsed canonical Text Projection document.
///

View File

@ -136,6 +136,18 @@ pub fn parse_document(text: &str) -> Result<TextDocument, TextError> {
// canonical-base?
let canonical_base = take_line(&mut lines, "canonical-base", parse_canonical_base)?;
// `CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3b: text projection cannot mint a
// canonical base. The grammar still defines the production — it is what
// this refusal is defined against, and what a future rebuild/repack flow
// will emit — but no document containing it parses. `NotCanonical`
// matches the existing `(blob ...)` rejection just below: syntactically
// well-formed, but never a text this companion can accept.
if canonical_base.is_some() {
return Err(TextError::NotCanonical(
"a (canonical-base ...) line cannot be canonical text: no reduction-authority \
validation exists for text projection to have produced it",
));
}
// blob*: collected only so the rejection below can fire; see the module
// documentation and `req:textproj:reject-unreferenced-blobs`.
@ -650,12 +662,13 @@ mod tests {
// Bumped with `COMPANION_VERSION` (0.7.0 → 0.8.0, genesis G1; 0.8.0 →
// 0.9.0, genesis G2a; 0.9.0 → 0.10.0, G-minor; 0.10.0 → 0.11.0, genesis
// G2b; 0.11.0 → 0.12.0, genesis G3a; 0.12.0 → 0.13.0, genesis G3b). Kept
// G2b; 0.11.0 → 0.12.0, genesis G3a; 0.12.0 → 0.13.0, genesis G3b;
// 0.13.0 → 0.14.0, `CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3b). Kept
// a literal because `projection` takes `&[&str]` and a formatted String
// would ripple through every call site;
// `the_test_header_tracks_the_implemented_version` below fails loudly if
// the two ever drift.
const HEADER: &str = "(text-projection (0 13 0))";
const HEADER: &str = "(text-projection (0 14 0))";
const DOCUMENT: &str = "(document #x00000000000000000000000000000001 (schema 0 1))";
/// A minimal but complete valid projection: just the two mandatory lines.
@ -757,28 +770,37 @@ mod tests {
.retain_named_checkpoints
);
assert!(document.extensions.is_empty());
let base = document
.canonical_base
.as_ref()
.expect("the worked example carries a canonical base");
assert_eq!(
base.snapshot_id,
SnapshotId([0x1f, 0x8b, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
);
assert_eq!(base.covers_causal_frontier, FrontierBytes::empty());
assert_eq!(
base.reduction_algorithm_version,
ReductionAlgorithmVersion(1)
);
assert_eq!(base.profile_id, ProfileId::Full);
assert_eq!(base.root_schema_version, SchemaVersion::V0);
assert_eq!(base.root_payload, vec![0u8, 0u8]);
// Pin 3b (`CONTRACT_FORMAT_EPOCH_MAJOR1.md`): the worked example no
// longer carries a canonical base — the companion's own chapter
// retired that spelling to a second, explicitly-refused listing
// (`spec/text_projection.tex`, "A Worked Example").
assert!(document.canonical_base.is_none());
assert!(document.blobs.is_empty());
assert_eq!(document.envelopes.len(), 1);
}
/// The base-bearing spelling `the_worked_example_parses_to_its_documented_fields`
/// used to exercise, before pin 3b retired it from the worked example
/// itself: still grammar-valid, no longer canonical text. Pinned as a
/// literal here (rather than re-reading the spec's second listing) so
/// this test does not depend on the exact prose the orchestrator chose to
/// wrap it in.
#[test]
fn parsing_base_bearing_text_is_refused() {
let base =
"(canonical-base #x1f8b0000000000000000000000000000 #x 1 full (schema 0 1) #x0000)";
let text = projection(&[HEADER, DOCUMENT, base]);
let err = parse_document(&text).expect_err("base-bearing text must be refused");
assert_eq!(
err,
TextError::NotCanonical(
"a (canonical-base ...) line cannot be canonical text: no reduction-authority \
validation exists for text projection to have produced it",
)
);
}
// -----------------------------------------------------------------
// A synthetic document exercising lineage, multiple profiles, an
// extension with preserved chunks, a canonical base, and multiple
@ -794,8 +816,11 @@ mod tests {
let extension = "(extension #x00000000000000000000000000000003 (1 0 0) true \
((chunk operation-envelope-block (schema 0 1) #xaa) (chunk snapshot (schema 0 1) #xbb)) \
#xaabb #xccdd)";
let base =
"(canonical-base #x00000000000000000000000000000004 #x 1 full (schema 0 1) #x0102)";
// Pin 3b (`CONTRACT_FORMAT_EPOCH_MAJOR1.md`): this document used to
// also carry a `(canonical-base ...)` line here, exercising every
// section at once. Base-bearing text is refused outright now — see
// `parsing_base_bearing_text_is_refused` — so this fixture drops it
// and keeps every other feature it was written to exercise together.
let e1 = sample_envelope(1, 1, 10);
let e2 = sample_envelope(2, 1, 20);
@ -809,7 +834,6 @@ mod tests {
profile_full,
profile_read_only,
extension,
base,
&e1_line,
&e2_line,
]);
@ -832,7 +856,7 @@ mod tests {
vec![0xaa, 0xbb]
);
assert_eq!(document.extensions[0].edit_barriers, vec![0xcc, 0xdd]);
assert!(document.canonical_base.is_some());
assert!(document.canonical_base.is_none());
assert_eq!(document.envelopes, vec![e1, e2]);
}

View File

@ -41,8 +41,8 @@
use std::collections::BTreeSet;
use epiphany_bundle::{
BlobId, BlockStore, Bundle, BundleError, ChunkKind, DocumentId, LineageId, ProfileConstraints,
ProfileDeclaration, ProfileId, RetentionPolicy, SchemaVersion, SemVer,
BlobId, BlockStore, Bundle, BundleError, ChunkKind, DocumentId, LineageId, Manifest,
ProfileConstraints, ProfileDeclaration, ProfileId, RetentionPolicy, SchemaVersion, SemVer,
};
use epiphany_core::textvalue::Sexp;
use epiphany_ops::{
@ -67,6 +67,15 @@ pub enum ProjectError {
/// A stored operation-envelope block held bytes that do not decode to a
/// canonical [`OperationEnvelope`].
Envelope(EnvelopeDecodeError),
/// The bundle's manifest carries a canonical base
/// (`spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3b: "text projection
/// cannot mint a canonical base"). Symmetric with
/// [`crate::serialize::SerializeError::CanonicalBaseUnsupported`] and
/// [`crate::parse`]'s own refusal — `req:textproj:roundtrip` quantifies
/// over every bundle and every valid text, so all three sides move
/// together: a document this companion cannot produce from text must
/// also never be produced *as* text.
CanonicalBaseUnsupported,
}
impl core::fmt::Display for ProjectError {
@ -74,6 +83,10 @@ impl core::fmt::Display for ProjectError {
match self {
ProjectError::Bundle(e) => write!(f, "bundle read failed: {e}"),
ProjectError::Envelope(e) => write!(f, "operation envelope failed to decode: {e}"),
ProjectError::CanonicalBaseUnsupported => f.write_str(
"the bundle's manifest carries a canonical base, which this companion cannot \
project: text projection cannot mint a canonical base",
),
}
}
}
@ -83,6 +96,7 @@ impl std::error::Error for ProjectError {
match self {
ProjectError::Bundle(e) => Some(e),
ProjectError::Envelope(e) => Some(e),
ProjectError::CanonicalBaseUnsupported => None,
}
}
}
@ -425,6 +439,33 @@ fn canonical_blobs<S: BlockStore>(
// Bundle<S> -> TextDocument.
// ===========================================================================
/// The check [`document_from_bundle`] and [`project_bundle`] apply before
/// doing any work (pin 3b): a base-bearing bundle is refused, symmetric with
/// [`crate::serialize::serialize_document`]'s and [`crate::parse::parse_document`]'s
/// own refusals.
///
/// Exposed as its own function, over a bare [`Manifest`] rather than inlined
/// into `document_from_bundle`, so it is independently unit-testable. That
/// indirection is load-bearing, not stylistic: during the S28 → P13-S27
/// interval, `CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3a's bundle-layer
/// refusals (rows 2/3/5i/6i of its epoch matrix) close *every* path —
/// `create`, `open`, and `commit` alike — that could ever produce a live
/// `Bundle` whose manifest carries a canonical base, in any crate, not only
/// this one (pin 3c: "no bundle anywhere may carry a canonical base ... in
/// production, in tests, or in the conformance suite"). So this predicate
/// cannot be driven end-to-end through a real `Bundle::open`/`create`/`commit`
/// call right now — there is no way to construct the `&Bundle<S>` argument
/// `document_from_bundle` would need. `projecting_a_base_bearing_bundle_is_refused`
/// (below) tests this function directly against a hand-built `Manifest`
/// instead, and its own doc comment records this as a contract-execution
/// finding, not something patched around.
fn reject_canonical_base(manifest: &Manifest) -> Result<(), ProjectError> {
if manifest.canonical_base.is_some() {
return Err(ProjectError::CanonicalBaseUnsupported);
}
Ok(())
}
/// Builds a [`TextDocument`] from a bundle's current manifest, reading every
/// chunk and blob payload the projection carries inline: each extension's
/// preserved chunks, the canonical base's root chunk, and (today, always
@ -440,6 +481,7 @@ pub fn document_from_bundle<S: BlockStore>(
bundle: &Bundle<S>,
) -> Result<TextDocument, ProjectError> {
let manifest = bundle.manifest();
reject_canonical_base(manifest)?;
let mut decoded = Vec::new();
for root in &manifest.operation_roots {
@ -563,9 +605,9 @@ mod tests {
use std::path::{Path, PathBuf};
use epiphany_bundle::{
encode_block, BlobRef, CompressionAlgorithm, ExtensionDeclaration, ExtensionId, FileUuid,
FrontierBytes, Manifest, MemStore, ProfileRegistryId, ReductionAlgorithmVersion,
SnapshotId, SnapshotRef, StagedChunk,
chunk_content_hash, encode_block, BlobRef, ChunkRef, CompressionAlgorithm,
ExtensionDeclaration, ExtensionId, FileUuid, FrontierBytes, Manifest, MemStore,
ProfileRegistryId, ReductionAlgorithmVersion, SnapshotId, SnapshotRef, StagedChunk,
};
use epiphany_core::textvalue::read_sexp;
use epiphany_core::{OperationId, RegionId, ReplicaId, WallClockTime};
@ -896,12 +938,12 @@ mod tests {
payload: b"extension-chunk-payload".to_vec(),
};
let base_chunk = StagedChunk {
kind: ChunkKind::Snapshot,
schema_version: SchemaVersion::V0,
payload: b"canonical-base-payload".to_vec(),
};
// `CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3c: no bundle anywhere may
// carry a canonical base for the S28 -> P13-S27 interval — `commit`
// now refuses one outright, so this fixture used to stage a
// `base_chunk` and wire it into `manifest.canonical_base` here can no
// longer do so. The base-bearing scenario is exercised separately, at
// the `Manifest` level, by `projecting_a_base_bearing_bundle_is_refused`.
let blob_chunk = StagedChunk {
kind: ChunkKind::Blob,
schema_version: SchemaVersion::V0,
@ -915,13 +957,7 @@ mod tests {
bundle
.commit(
&[
op_block_0,
op_block_1,
extension_chunk,
base_chunk,
blob_chunk,
],
&[op_block_0, op_block_1, extension_chunk, blob_chunk],
|ctx| {
let mut manifest = ctx.previous_manifest.clone();
manifest.operation_roots = vec![ctx.new_chunks[0], ctx.new_chunks[1]];
@ -933,22 +969,14 @@ mod tests {
affected_object_kinds: vec![0xAA],
edit_barriers: vec![0xBB, 0xCC],
}];
manifest.canonical_base = Some(SnapshotRef {
snapshot_id: SnapshotId([7; 16]),
covers_causal_frontier: FrontierBytes::from_bytes(vec![1, 2, 3]),
reduction_algorithm_version: ReductionAlgorithmVersion(1),
profile_id: ProfileId::Full,
root: ctx.new_chunks[3],
hash: ctx.new_chunks[3].hash,
});
manifest.blob_roots = vec![BlobRef {
blob_id: BlobId(ctx.new_chunks[4].hash),
blob_id: BlobId(ctx.new_chunks[3].hash),
media_type: "application/octet-stream".to_owned(),
offset: ctx.new_chunks[4].offset,
compressed_length: ctx.new_chunks[4].compressed_length,
uncompressed_length: ctx.new_chunks[4].uncompressed_length,
offset: ctx.new_chunks[3].offset,
compressed_length: ctx.new_chunks[3].compressed_length,
uncompressed_length: ctx.new_chunks[3].uncompressed_length,
compression: CompressionAlgorithm::None,
hash: ctx.new_chunks[4].hash,
hash: ctx.new_chunks[3].hash,
declared_max_uncompressed_length: None,
}];
manifest
@ -995,9 +1023,9 @@ mod tests {
assert_eq!(extension.chunks[0].kind, ChunkKind::ExtensionData);
assert_eq!(extension.chunks[0].payload, b"extension-chunk-payload");
let base = document.canonical_base.as_ref().expect("base was staged");
assert_eq!(base.snapshot_id, SnapshotId([7; 16]));
assert_eq!(base.root_payload, b"canonical-base-payload");
// `build_sample_bundle` no longer stages a canonical base (pin 3c: no
// bundle anywhere may carry one during the S28 -> P13-S27 interval).
assert!(document.canonical_base.is_none());
// The trap: a non-empty `blob_roots` still yields zero canonical
// blobs, because nothing can reach one yet.
@ -1041,7 +1069,6 @@ mod tests {
"(lineage",
"(profile",
"(extension",
"(canonical-base",
"(envelope",
"(envelope",
],
@ -1095,6 +1122,54 @@ mod tests {
}
}
#[test]
fn projecting_a_base_bearing_bundle_is_refused() {
// Pin 3b's projection side: `document_from_bundle`/`project_bundle`
// refuse a bundle whose manifest carries a canonical base.
//
// This cannot be driven end-to-end through a live `Bundle<S>` right
// now: `CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3a's refusals (this same
// rung, `epiphany-bundle`) close *every* path that could produce
// one — `Bundle::create` already rejected an initial base before this
// rung, and `Bundle::open`/`Bundle::commit` now refuse one
// categorically in both format epochs (matrix rows 2/3/5i/6i) — so no
// crate, including this one, can construct a `Bundle` whose manifest
// carries a canonical base during the S28 -> P13-S27 interval (pin
// 3c: "no bundle anywhere may carry a canonical base ... in
// production, in tests, or in the conformance suite"). That is
// reported as a contract-execution finding, not patched around here.
//
// What IS independently checkable, without a live `Bundle`, is the
// exact predicate `document_from_bundle` guards on
// (`reject_canonical_base`): a `Manifest` — a public, freestanding
// type — carrying `canonical_base = Some(...)`.
let mut manifest = Manifest::empty(DocumentId([1; 16]));
let payload = b"snapshot".to_vec();
let hash = chunk_content_hash(ChunkKind::Snapshot, SchemaVersion::V0, &payload);
let root = ChunkRef {
id: epiphany_bundle::ChunkId(hash),
kind: ChunkKind::Snapshot,
schema_version: SchemaVersion::V0,
offset: epiphany_bundle::BODY_START,
compressed_length: payload.len() as u64,
uncompressed_length: payload.len() as u64,
compression: CompressionAlgorithm::None,
hash,
};
manifest.canonical_base = Some(SnapshotRef {
snapshot_id: SnapshotId([1; 16]),
covers_causal_frontier: FrontierBytes::empty(),
reduction_algorithm_version: ReductionAlgorithmVersion(0),
profile_id: ProfileId::Full,
hash: root.hash,
root,
});
assert!(matches!(
reject_canonical_base(&manifest),
Err(ProjectError::CanonicalBaseUnsupported)
));
}
// -----------------------------------------------------------------
// Suite reach: count, don't just claim, coverage of an extension, a
// canonical base, and a multi-envelope document.
@ -1103,13 +1178,27 @@ mod tests {
#[test]
fn test_suite_reach_covers_extension_base_and_multi_envelope_documents() {
let rich = document_from_bundle(&build_sample_bundle()).expect("bundle reads cleanly");
// `build_sample_bundle` no longer carries a canonical base (pin 3c),
// so `with_canonical_base` reach is exercised here instead, directly
// on a `TextDocument` value — no live `Bundle` is involved, so pin
// 3a's bundle-layer refusal does not apply (this is not projection,
// parsing, or serialization; see
// `projecting_a_base_bearing_bundle_is_refused`, which covers the
// refusal itself).
let minimal = TextDocument {
document_id: DocumentId([6; 16]),
manifest_schema_version: SchemaVersion::V0,
lineage_id: None,
profiles: vec![ProfileDeclaration::full()],
extensions: Vec::new(),
canonical_base: None,
canonical_base: Some(TextCanonicalBase {
snapshot_id: SnapshotId([6; 16]),
covers_causal_frontier: FrontierBytes::empty(),
reduction_algorithm_version: ReductionAlgorithmVersion(1),
profile_id: ProfileId::Full,
root_schema_version: SchemaVersion::V0,
root_payload: b"reach-only-canonical-base".to_vec(),
}),
blobs: Vec::new(),
envelopes: vec![sample_envelope(1, 1)],
};

View File

@ -70,6 +70,21 @@ pub enum SerializeError {
/// non-canonical root into the manifest or silently drop the caller's
/// payload bytes. Neither is acceptable, so serialization refuses instead.
NonEmptyBlobs,
/// The document carries a canonical base
/// (`spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3b: "text projection cannot
/// mint a canonical base"). `TextDocument` has no container-major or epoch
/// field, and `parse` accepts an unbounded reduction-algorithm version, so
/// an old or hand-authored text document could otherwise be laundered
/// straight through the format-epoch boundary into a brand-new major-1
/// container. A dedicated variant, not a `SerializeError::Bundle`
/// passthrough: the container-layer refusal
/// (`epiphany_bundle::BundleError::ReductionAuthorityUnavailable`) is
/// temporary and interim-only, while this refusal is a permanent property
/// of what the text medium can prove — a text format cannot carry
/// unforgeable provenance, so refusal is the only rule it can actually
/// enforce. This is real capability loss: base-bearing documents stop
/// round-tripping through text until a repack flow exists.
CanonicalBaseUnsupported,
/// The bundle itself rejected the manifest or a staged chunk — e.g. the
/// document declares no profile this implementation understands, or a
/// staged root fails `Bundle::commit`'s structural validation.
@ -82,6 +97,10 @@ impl fmt::Display for SerializeError {
SerializeError::NonEmptyBlobs => f.write_str(
"the document carries a populated blobs vector, but no blob can be canonical today",
),
SerializeError::CanonicalBaseUnsupported => f.write_str(
"the document carries a canonical base, which this companion cannot serialize: \
text projection cannot mint a canonical base",
),
SerializeError::Bundle(error) => {
write!(f, "the bundle rejected the serialized document: {error}")
}
@ -94,6 +113,7 @@ impl std::error::Error for SerializeError {
match self {
SerializeError::Bundle(error) => Some(error),
SerializeError::NonEmptyBlobs => None,
SerializeError::CanonicalBaseUnsupported => None,
}
}
}
@ -114,8 +134,12 @@ impl From<BundleError> for SerializeError {
/// [`ChunkRef`](epiphany_bundle::ChunkRef)s that commit assigns.
///
/// Returns [`SerializeError::NonEmptyBlobs`] if `document.blobs` is non-empty
/// (see the module documentation), or [`SerializeError::Bundle`] if the bundle
/// itself refuses the manifest or a staged root.
/// (see the module documentation), [`SerializeError::CanonicalBaseUnsupported`]
/// if `document.canonical_base` is present (pin 3b: text projection cannot
/// mint a canonical base — checked as defence for a directly constructed
/// `TextDocument`, since `parse` also refuses one on the way in), or
/// [`SerializeError::Bundle`] if the bundle itself refuses the manifest or a
/// staged root.
pub fn serialize_document<S: BlockStore>(
document: &TextDocument,
store: S,
@ -124,6 +148,9 @@ pub fn serialize_document<S: BlockStore>(
if !document.blobs.is_empty() {
return Err(SerializeError::NonEmptyBlobs);
}
if document.canonical_base.is_some() {
return Err(SerializeError::CanonicalBaseUnsupported);
}
let mut bundle = Bundle::create(store, file_uuid, empty_manifest(document))?;
@ -335,11 +362,16 @@ mod tests {
document
}
/// Carries an extension, a canonical base, and many envelopes at once — the
/// document every round-trip law needs to be checked against together.
/// Carries an extension and many envelopes at once — the document every
/// round-trip law needs to be checked against together.
///
/// Used to carry a canonical base too (hence the name); pin 3b's
/// base-bearing exclusion means that capability moved to
/// `document_with_canonical_base` alone, so this fixture keeps every
/// other feature and drops the base
/// (`CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3b).
fn rich_document(seed: u64) -> TextDocument {
let mut document = document_with_extension(seed);
document.canonical_base = document_with_canonical_base(seed).canonical_base;
document.envelopes = envelopes(seed, 60);
document
}
@ -365,6 +397,18 @@ mod tests {
);
}
#[test]
fn text_projection_serialize_produces_a_major_1_container() {
// Pin 6: every writer path stamps the epoch, including text
// projection. `serialize_document` (via `build_manifest`) is the one
// production writer that reaches a canonical base — `project.rs:936`
// is a `#[cfg(test)]` fixture writer, not production.
let document = minimal_document(11);
let bundle = serialize_document(&document, MemStore::new(), FileUuid([1; 16]))
.expect("a well-formed document serializes");
assert_eq!(bundle.header().format_major, epiphany_bundle::FORMAT_MAJOR);
}
#[test]
fn document_identity_round_trips() {
let mut document = minimal_document(1);
@ -393,31 +437,23 @@ mod tests {
}
#[test]
fn canonical_base_round_trips_snapshot_id_and_payload() {
fn serializing_a_text_document_with_a_canonical_base_is_refused() {
// Pin 3b: text projection cannot mint a canonical base. Built from
// the existing base-bearing fixture (`document_with_canonical_base`),
// which used to round-trip through `serialize_document` /
// `document_from_bundle` before this rung — that capability is a
// real, intentional loss (base-bearing documents stop round-tripping
// through text until a repack flow exists).
let document = document_with_canonical_base(4);
let reopened = serialize_and_reopen(&document);
let expected = document
.canonical_base
.as_ref()
.expect("fixture carries a canonical base");
let base = reopened
.manifest()
.canonical_base
.as_ref()
.expect("canonical base present after reopen");
assert_eq!(base.snapshot_id, expected.snapshot_id);
assert_eq!(base.covers_causal_frontier, expected.covers_causal_frontier);
assert_eq!(
base.reduction_algorithm_version,
expected.reduction_algorithm_version
);
assert_eq!(base.profile_id, expected.profile_id);
assert_eq!(base.root.kind, ChunkKind::Snapshot);
assert_eq!(base.root.schema_version, expected.root_schema_version);
let payload = reopened
.read_chunk(&base.root)
.expect("root chunk reads and verifies");
assert_eq!(payload, expected.root_payload);
let result = serialize_document(&document, MemStore::new(), FileUuid([1; 16]));
assert!(matches!(
result,
Err(SerializeError::CanonicalBaseUnsupported)
));
// M8 depends on this distinction: the dedicated variant, never a
// `SerializeError::Bundle` passthrough from the container-layer
// refusal this would otherwise fall through to.
assert!(!matches!(result, Err(SerializeError::Bundle(_))));
}
#[test]
@ -533,7 +569,9 @@ mod tests {
let document = rich_document(10);
let reopened = serialize_and_reopen(&document);
assert_eq!(reopened.manifest().document_id, document.document_id);
assert!(reopened.manifest().canonical_base.is_some());
// `rich_document` no longer carries a base (pin 3b) — every other
// root it carries still round-trips together.
assert!(reopened.manifest().canonical_base.is_none());
assert_eq!(reopened.manifest().extension_declarations.len(), 1);
assert_eq!(reopened.manifest().operation_roots.len(), 1);
reopened
@ -579,6 +617,17 @@ mod tests {
);
for document in &documents {
if document.canonical_base.is_some() {
// Pin 3b: a base-bearing document is refused outright, not
// round-tripped — the one round-trip law this suite must now
// except.
let result = serialize_document(document, MemStore::new(), FileUuid([1; 16]));
assert!(matches!(
result,
Err(SerializeError::CanonicalBaseUnsupported)
));
continue;
}
let reopened = serialize_and_reopen(document);
assert_eq!(reopened.manifest().document_id, document.document_id);
}

View File

@ -123,16 +123,26 @@ pub struct ReachCounts {
/// The exact non-vacuity contract of this corpus: four accepted documents, two
/// reaching each optional/rich feature, and one actually rejected vector for
/// each of the nine distinct rejection classes implemented by this layer.
/// each of the ten distinct rejection classes implemented by this layer.
///
/// `canonical_bases` is pinned at **0**, not two:
/// `CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3b refuses projecting, parsing, and
/// serializing any base-bearing document, so a canonical base is no longer
/// reachable through text at all. The two formerly base-bearing accepts
/// (`extension_base_two_envelopes`, `rich_document`) keep every other feature
/// they exercised but lose their base, and the base-bearing spelling survives
/// only as the new `canonical_base_present` reject vector (class
/// `canonical-base-unsupported`).
pub fn expected_reach() -> ReachCounts {
ReachCounts {
extensions: 2,
canonical_bases: 2,
canonical_bases: 0,
custom_profiles: 2,
lineages: 2,
multi_envelope: 2,
reject_classes: [
("blob-line", 1),
("canonical-base-unsupported", 1),
("extension-chunk-order", 1),
("extension-declaration-order", 1),
("missing-trailing-lf", 1),
@ -341,7 +351,12 @@ fn accept_documents() -> Vec<(&'static str, String)> {
blobs: Vec::new(),
envelopes: vec![sample_envelope(1, 100)],
};
let extension_base_multi = TextDocument {
// `CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3b: text projection can no longer
// carry a canonical base at all, so this document (still named for the
// two envelopes it exercises, not for a base it no longer carries) keeps
// its extension and non-baseline schema version but drops the base that
// used to make it `extension_base_multi`.
let extension_two_envelopes = TextDocument {
document_id: DocumentId([3; 16]),
// A non-baseline carried version, so the corpus exercises the
// `document` line's schema field at a value other than the
@ -350,17 +365,19 @@ fn accept_documents() -> Vec<(&'static str, String)> {
lineage_id: None,
profiles: profiles(false),
extensions: vec![extension(1, &[1, 2])],
canonical_base: Some(base(3)),
canonical_base: None,
blobs: Vec::new(),
envelopes: vec![sample_envelope(2, 200), sample_envelope(3, 300)],
};
// Base removed (pin 3b); lineage, custom profile, two extensions, and
// multiple envelopes are all retained.
let rich = TextDocument {
document_id: DocumentId([4; 16]),
manifest_schema_version: SchemaVersion::V0,
lineage_id: Some(LineageId([0x14; 16])),
profiles: profiles(true),
extensions: vec![extension(1, &[1, 2]), extension(2, &[3])],
canonical_base: Some(base(4)),
canonical_base: None,
blobs: Vec::new(),
envelopes: vec![
sample_envelope(4, 400),
@ -459,7 +476,7 @@ fn accept_documents() -> Vec<(&'static str, String)> {
),
(
"extension_base_two_envelopes",
project_text_document(&extension_base_multi),
project_text_document(&extension_two_envelopes),
),
("rich_document", project_text_document(&rich)),
("create_staff_group", project_text_document(&staff_group)),
@ -548,7 +565,7 @@ pub fn document_vectors() -> Vec<TextVector> {
};
let minimal = by_name("minimal");
let lineage_custom = by_name("lineage_custom_profile");
let extension_base_multi = by_name("extension_base_two_envelopes");
let extension_two_envelopes = by_name("extension_base_two_envelopes");
let rich = by_name("rich_document");
let mut vectors: Vec<TextVector> = accepts
@ -556,17 +573,16 @@ pub fn document_vectors() -> Vec<TextVector> {
.map(|(name, text)| (SURFACE, "accept", "-", *name, text.as_bytes().to_vec()))
.collect();
// The rejected version must be one this crate does NOT implement. Genesis
// tranche G3b moved `COMPANION_VERSION` to 0.13.0, which had been this
// vector's "future" version — leaving it would have made the negative
// vector assert that the *correct* header is rejected. It now names
// 0.12.0, the immediately superseded companion, which is the better test
// anyway: rejecting the version right behind you is exactly the deferred
// The rejected version must be one this crate does NOT implement.
// `CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3b moved `COMPANION_VERSION` to
// 0.14.0; this vector now names 0.13.0, the immediately superseded
// companion (previously 0.12.0, when the committed version was 0.13.0) —
// rejecting the version right behind you is exactly the deferred
// migrate-on-read posture (`req:textproj:header-version`).
let wrong_version = replace_once(
minimal,
"(text-projection (0 14 0))",
"(text-projection (0 13 0))",
"(text-projection (0 12 0))",
);
vectors.push((
SURFACE,
@ -590,12 +606,18 @@ pub fn document_vectors() -> Vec<TextVector> {
blob.into_bytes(),
));
// Re-expressed on a non-base section pair (pin 3b: neither accept carries
// a canonical base to invert against any more). `projection`'s order is
// `header document lineage? profile* extension* canonical-base? blob*
// envelope*`, so a profile/extension inversion reaches
// `out-of-order-sections` exactly as the old canonical-base/extension
// inversion did, without a base.
vectors.push((
SURFACE,
"reject",
"out-of-order-sections",
"canonical_base_before_extension",
swap_first_lines(extension_base_multi, "(extension ", "(canonical-base ").into_bytes(),
swap_first_lines(extension_two_envelopes, "(profile ", "(extension ").into_bytes(),
));
vectors.push((
@ -611,7 +633,7 @@ pub fn document_vectors() -> Vec<TextVector> {
"reject",
"operation-envelope-order",
"envelopes_reversed",
swap_first_two_lines(extension_base_multi, "(envelope ").into_bytes(),
swap_first_two_lines(extension_two_envelopes, "(envelope ").into_bytes(),
));
vectors.push((
@ -631,7 +653,7 @@ pub fn document_vectors() -> Vec<TextVector> {
));
let chunks_reversed = replace_once(
extension_base_multi,
extension_two_envelopes,
"((chunk extension-data (schema 0 1) #x01) (chunk extension-data (schema 0 1) #x02))",
"((chunk extension-data (schema 0 1) #x02) (chunk extension-data (schema 0 1) #x01))",
);
@ -653,6 +675,28 @@ pub fn document_vectors() -> Vec<TextVector> {
missing_lf,
));
// NEW (pin 3b): a base-bearing text, refused by the parse-side check —
// built from the pre-change base-bearing spelling, so the corpus keeps a
// base-bearing text as a negative rather than losing the spelling
// entirely.
let canonical_base_present = TextDocument {
document_id: DocumentId([11; 16]),
manifest_schema_version: SchemaVersion::V0,
lineage_id: None,
profiles: profiles(false),
extensions: Vec::new(),
canonical_base: Some(base(11)),
blobs: Vec::new(),
envelopes: Vec::new(),
};
vectors.push((
SURFACE,
"reject",
"canonical-base-unsupported",
"canonical_base_present",
project_text_document(&canonical_base_present).into_bytes(),
));
vectors
}
@ -886,7 +930,7 @@ mod tests {
#[test]
fn the_reference_implementation_agrees_with_every_vector() {
match verify(COMMITTED) {
Ok(count) => assert_eq!(count, 19, "the corpus has unexpectedly thinned"),
Ok(count) => assert_eq!(count, 20, "the corpus has unexpectedly thinned"),
Err(failures) => panic!(
"{} disagreement(s):\n{}",
failures.len(),
@ -957,20 +1001,21 @@ mod tests {
}
/// (t12) Genesis tranche G3b: text projection round-trips the new
/// `create-measure` kind, and the companion version is **0.13.0**, with
/// the negative vector rejecting **0.12.0** (the immediately superseded
/// `create-measure` kind, and the companion version is **0.14.0**
/// (`CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3b bumped it from 0.13.0), with
/// the negative vector rejecting **0.13.0** (the immediately superseded
/// companion).
///
/// **Mutation:** drop `OperationKindTag::CreateMeasure` from
/// `OperationKind::parse` in `textproj_kind.rs`; must fail. Separately,
/// leave `COMPANION_VERSION` at `(0, 12, 0)`; the negative vector must
/// leave `COMPANION_VERSION` at `(0, 13, 0)`; the negative vector must
/// fail.
#[test]
fn t12_g3b_kinds_round_trip_and_companion_is_0_13_0_rejecting_0_12_0() {
fn t12_g3b_kinds_round_trip_and_companion_is_0_14_0_rejecting_0_13_0() {
assert_eq!(
crate::COMPANION_VERSION,
(0, 13, 0),
"the companion version must be 0.13.0"
(0, 14, 0),
"the companion version must be 0.14.0"
);
let name = "create_measure";
@ -988,7 +1033,7 @@ mod tests {
);
// The negative vector must reject exactly the immediately superseded
// companion, 0.12.0.
// companion, 0.13.0.
let rows = parse(COMMITTED).expect("the committed corpus parses");
let superseded = rows
.iter()
@ -997,8 +1042,8 @@ mod tests {
assert_eq!(superseded.verdict, "reject");
let text = String::from_utf8(superseded.text.clone()).expect("utf8");
assert!(
text.contains("(text-projection (0 12 0))"),
"the negative vector must name the immediately superseded companion 0.12.0, got: {text}"
text.contains("(text-projection (0 13 0))"),
"the negative vector must name the immediately superseded companion 0.13.0, got: {text}"
);
assert!(
parse_document(&text).is_err(),

View File

@ -1,14 +1,21 @@
# Contract — P13-S27: the reduction version gets an outside witness
**Status:** DRAFT — **BLOCKED on P13-S28** (the format-epoch rung). Pins 1 and
310 are settled, internally consistent, and ratifiable as a plan. **Pin 2a is
an open question this contract states rather than answers**, and it is not
answerable inside this rung: the provenance carrier it needs is a format-epoch
design, which P13-S28 owns.
**Status:** DRAFT — **BLOCKED on the format-epoch rung**
(`spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md`), which is **ratified and in
implementation** but has not yet landed. Pins 1 and 310 are settled, internally
consistent, and ratifiable as a plan.
**This is bounded analysis, not an abandoned implementation plan.** It may not be
dispatched. Pin 2a MUST NOT be treated as a sub-pin that drifts into this rung's
scope — see its own prohibition.
**Pin 2a is RESOLVED as of 2026-08-07** — from outside this rung, by that
contract's pin 8, exactly as its prohibition required. Legacy bases are refused
by container epoch, never by version arithmetic; see the resolution block under
pin 2a. This contract additionally **inherits three obligations** from that rung
(the two interim refusals it must convert to validation, M8's deferred laundering
demonstration, and pin 3c's two suspended conformance assertions) — recorded in
the same place.
**It becomes dispatchable when the format rung lands, not before.** Until then
this remains bounded analysis. Pin 2a's original prohibition stands for the
record: it was never amended into a disposition from inside this contract.
**Rung type:** **capability + API change.** No wire bytes move and no schema
major or minor changes — `BundleError` has no discriminant and no encoder
@ -16,10 +23,10 @@ major or minor changes — `BundleError` has no discriminant and no encoder
change is `Bundle::open`'s **and `Bundle::create`'s** signatures, at 57 and 32
call sites.
**Does NOT by itself unblock P13-S16.** This rung installs the authority and
validates both read and write paths. S16 additionally requires the **legacy-base
disposition of pin 2a**, which is an open question this contract states rather
than answers.
**Now DOES unblock P13-S16, once it lands.** This rung installs the authority and
validates both read and write paths; S16's remaining precondition was pin 2a's
legacy-base disposition, and that is resolved. The chain is therefore
format-epoch rung → **P13-S27****P13-S16**, with no open question left in it.
**Rulings already made (2026-07-31), not re-opened here:**
@ -246,6 +253,74 @@ dispatchable merely because S27 lands.** This pin deliberately remains an open
question. It is not to be amended into a disposition without its own ratification
round.
### Pin 2a — **RESOLVED 2026-08-07 by the format-epoch rung.**
The ratification round this pin demanded is the one the format-epoch contract
had: `spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md`, ratified after four adversarial
review rounds, whose **pin 8** exists to resolve this pin from outside it. The
open analysis above is retained verbatim as the reasoning that produced the
answer, not superseded prose.
**The disposition is none of (i), (ii) or (iii): it is the container epoch.**
> **Reduction-version authority is meaningful only in major-1 containers.
> Legacy bases are refused by container epoch, never by version arithmetic.**
That is why the collision this pin identified never has to be adjudicated. A
pre-S27 base carrying `1` and a legitimately rebuilt S16 base carrying `1` are
indeed indistinguishable **as numbers** — and they never meet, because the
pre-S27 base can only exist in a major-0 container, which is refused at the
epoch boundary before any version is compared. The `u32` never has to carry
provenance, because the container already does.
Each rejected option, and why the epoch beats it: **(i)** normalizing the corpus
would have to find every artifact, and a missed one is silently wrong forever;
**(ii)** a non-colliding epoch value is a convention a hand-authored document can
simply declare — `parse.rs:591` accepts an unbounded `u32`; **(iii)** widening
the type makes the wire meaning richer and buys nothing the container property
does not already give. All three try to make a number carry provenance. The
epoch makes the *file* carry it.
**S27's own baseline stays `0`** (pin 2 is unchanged). What changes is that the
question "what about a base older than the authority?" is no longer S27's to
answer.
### Inherited from the format-epoch rung — obligations S27 must discharge
The format rung lands **before** this one and closes two things temporarily,
naming S27 as what reopens them. Both are owed work here, not optional:
1. **The interim refusals become real validation.** The format rung's pin 3a
temporarily refuses **both** major-1 base boundaries — opening a major-1
container that already carries a base, and committing a base into one —
through a third, temporary error (`ReductionAuthorityUnavailable`) that is
distinct from its two legacy/repack errors. S27 replaces **both** branches
with capability validation. Replacing only one leaves a hole exactly where
the format rung's own review found one.
2. **The deferred laundering demonstration** (format-rung M8). That rung could
not demonstrate the text-import laundering path end to end, because pin 3a
refuses every major-1 base commit categorically, so the "a base-bearing text
document really does serialize into a major-1 container" observation is
unreachable there. Under S27 a base commit succeeds or fails **on its
version**, so the demonstration becomes performable and is owed: with pin 3b's
text refusal removed, show that a base-bearing document whose raw version
happens to match the current authority serializes into a major-1 container
indistinguishable from a validated one. That is the false provenance the text
refusal exists to prevent, and it has never been observed — only reasoned
about.
3. **Two conformance assertions come back** (format-rung pin 3c). Criterion 4's
bookkeeping-projection counterpart, `assert_reduction_serialization_stable`
(`testkit/src/roundtrip.rs:241`), keeps its serialize → load → decode →
reserialize cycle through the interval but loses exactly two
canonical-base-specific assertions: `verify_canonical_chunks`'s base branch
(`bundle.rs:613``:621`, including the `base.hash != base.root.hash`
cross-check), and the reopened manifest actually carrying the base
(`roundtrip.rs:293``:297`). S27 restores both, since a base-bearing container
becomes constructible again the moment validation replaces refusal. The
harness carries a marker naming this contract at the suspension point.
**Pin 3 — `BundleCapabilities`, required at both constructors, carried on the
`Bundle`.**
@ -404,10 +479,27 @@ Named, permanent, in `epiphany-bundle`:
intact. A writer check that corrupts the document while refusing is worse than
no check.
**Added 2026-08-07 with pin 2a's resolution — the inherited obligations, stated
as tests so they cannot be discharged by prose:**
6. **`a_major_1_bundle_carrying_a_base_opens_when_the_authority_matches`** — the
read-side half of the format rung's pin 3a, converted from temporary refusal
to real validation. Its sibling is test 2, which is the same path when the
authority *disagrees*. Both branches must exist; the format rung's own review
found a draft that closed only one.
7. **The two restored conformance assertions** (format-rung pin 3c), in
`assert_reduction_serialization_stable` (`testkit/src/roundtrip.rs:241`):
`verify_canonical_chunks` covering the base again, and the reopened manifest
carrying it. **Restoring them means deleting the suspension marker** that
names this contract — if the marker is still in the tree when this rung
reports, the restoration did not happen.
Tests 2 and 3 must be **paired in review**: each asserts the other's error is
*not* produced. A test that only checks its own variant cannot show the two
paths are distinguishable, which is the whole point of pin 6.
Tests 6 and 2 stand in the same relation to each other.
---
## §4. Mutation plan

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -240,7 +240,7 @@
{\Large\scshape\color{epiphanyslate}Binary Format}\\[6pt]
{\large\itshape\color{epiphanyslate}A companion to the Core Specification}\\[14pt]
{\color{epiphanygold}\rule{3in}{0.8pt}}\\[24pt]
{\normalsize\color{epiphanyink}Version 0.16.0 --- The genesis operation tranche closes: \texttt{CreateMeasure} (G3b)}\\[4pt]
{\normalsize\color{epiphanyink}Version 0.17.0 --- The container format major becomes 1: the header carries an epoch}\\[4pt]
{\small\color{epiphanyslate}Normative for the byte layouts it defines}
\vfill
\end{titlepage}
@ -1805,10 +1805,18 @@ format major version. All integers little-endian.
\endhead
\tablenums{0..8} & magic & \texttt{MUSCBND\textbackslash0} (8 bytes, ASCII,
trailing NUL) \\
\tablenums{8..10} & \texttt{format\_major} (\texttt{u16}) & \tablenums{0}
in this version; readers reject other values \\
\tablenums{10..12} & \texttt{format\_minor} (\texttt{u16}) & \tablenums{1}
written in this version \\
\tablenums{8..10} & \texttt{format\_major} (\texttt{u16}) & \tablenums{1}
written in this version. The container \emph{epoch}: a major-1
container is one whose every base-bearing commit was validated
against a supplied reduction authority. \tablenums{0} is
\emph{legacy} and \MUST{} be decoded deliberately as such, not
rejected; any other value is rejected. A legacy container \MAY{}
be opened only while it carries no canonical base, and \MUSTNOT{}
acquire one --- the epoch is fixed at creation and cannot be
inherited by a later commit (Core Specification,
\sectionsc{The Container Epoch}) \\
\tablenums{10..12} & \texttt{format\_minor} (\texttt{u16}) & \tablenums{0}
written in this version; minor numbering restarts at a new major \\
\tablenums{12..16} & \texttt{header\_length} (\texttt{u32}) &
\tablenums{64}; readers reject other values \\
\tablenums{16..24} & \texttt{superblock\_a\_offset} (\texttt{u64}) &
@ -3715,6 +3723,28 @@ only}: implementations need not agree on an error taxonomy.
ladder: G1 $\rightarrow$ G2a $\rightarrow$ G-minor $\rightarrow$ G2b
$\rightarrow$ G3a $\rightarrow$ G3b. Semantics: Operation Catalog
\sectionsc{CreateMeasure}, 0.13.0. \\
\today & The Fixed Header & 0.17.0 --- The container format major becomes
\tablenums{1}, and \texttt{format\_minor} restarts at \tablenums{0}
(\texttt{spec/CONTRACT\_FORMAT\_EPOCH\_MAJOR1.md}). This is the first
\emph{container} major boundary, and it is not a change of wire layout: no
field moves, no offset changes, and every byte range in the header table is
what it was. What changes is what the major \emph{means}. A major-1 container
is one whose every base-bearing commit was validated against a supplied
reduction authority; major \tablenums{0} is legacy and \MUST{} be decoded
deliberately as such rather than rejected.
The major is the carrier precisely because the header never changes after
creation. A superblock field could not do this job: a legacy container that
later commits a validated base would keep carrying whatever its creation
stamped, so provenance would be inheritable by a document that never earned
it. A minor bump could not do it either --- readers ignore minor, and this
document's own rules limit a minor change to appending append-safe
discriminants, whereas making a previously-valid canonical base newly
rejectable is a semantic acceptance change. The full epoch matrix, including
the rule that a legacy container may open but \MUSTNOT{} acquire a canonical
base, is normative in the Core Specification,
\sectionsc{The Container Epoch}. \\
\bottomrule
\end{longtable}

Binary file not shown.

View File

@ -10868,6 +10868,65 @@ pub struct FixedHeader {
\texttt{file\_uuid}.
\end{requirement}
\subsubsection{The Container Epoch}
\label{sec:format:container-epoch}
The \texttt{format\_major} field is not only a wire-layout
discriminator. It is the container's \emph{epoch}: the statement of
which guarantees held at the moment the file was created. Because the
header never changes after creation, the epoch is a property of the
file's creation and cannot drift as the document is committed to ---
which is precisely what makes it able to carry a provenance guarantee
that a later generation must not be able to inherit.
Major \tablenums{1} is the first epoch to use that capacity. A major-1
container is one whose every base-bearing commit was validated against
a supplied reduction authority; a major-0 container carries no such
guarantee, and no sequence of commits can promote one into the other.
\begin{requirement}
\label{req:format:container-epoch}
A reader \MUST{} classify \texttt{format\_major} three ways rather
than testing it for equality with the version it writes:
\tablenums{0} is \emph{legacy} and \MUST{} be decoded deliberately as
such; \tablenums{1} is \emph{current}; any other value \MUST{} be
rejected as an unsupported format version. The classification
\MUST{} be carried as a value the reader can act on, not re-derived
by comparison at each use site.
Behaviour is then determined by the container's epoch together with
the presence of a canonical base:
\begin{itemize}
\item \emph{Legacy, no canonical base} --- the bundle \MAY{} be
opened. Nothing whose validity cannot be established is exposed.
\item \emph{Legacy, canonical base present} --- the bundle
\MUST{} be refused. A base predating any reduction authority is
not safe materialized state, and \MUSTNOT{} be served in a
read-only or otherwise restricted mode: a restricted view of
unverifiable canonical state is still unverifiable canonical
state, presented confidently.
\item \emph{Legacy, a commit attempts to add or replace a
canonical base} --- the commit \MUST{} be refused, and the
implementation \MUST{} direct the writer to repack into a fresh
major-1 container. This is the rule that makes the epoch
non-inheritable, and it is the reason a header field can carry
this guarantee where a superblock field could not.
\item \emph{Current, no canonical base} --- the bundle \MAY{} be
opened, and \MAY{} be committed to.
\item \emph{Current, canonical base present or introduced} ---
the base \MUST{} be validated against the reduction authority
the opening implementation supplies.
\end{itemize}
Where a container's canonical base disagrees with its active
superblock, that disagreement is \emph{corruption} and \MUST{} be
reported as a malformed bundle \emph{before} any epoch rule above is
applied, in either epoch. Ordering the epoch rules first would
collapse tampering into staleness and erase the distinction the
reduction authority depends on.
\end{requirement}
\subsection{The Superblock Slots}
Each superblock slot is exactly 256 bytes. The bundle has two slots
@ -12229,6 +12288,18 @@ pub struct SchemaVersion {
Schema versioning rules:
\begin{itemize}
\item These rules govern \emph{schema} versions --- the manifest's
\texttt{manifest\_schema\_version} and each chunk's
\texttt{schema\_version}. The container's
\texttt{format\_major} is a separate axis, and a change to it
now means more than a change of wire layout: it declares a new
container \emph{epoch}, with guarantees that a reader may rely
on and that no later commit can confer on a container created
before them (Section~\ref{sec:format:container-epoch}). A
container format major boundary is therefore not satisfied by
re-encoding bytes; a document produced under an older epoch
acquires the new epoch's guarantees only by being repacked into
a newly created container.
\item Major version changes are non-backward-compatible.
Readers that do not support the chunk's major schema
version \MUST{} handle it according to the chunk's role:
@ -16720,6 +16791,35 @@ layouts they own versus inherit:
metadata: it enters no content hash and no canonical encoding, so no
wire form and no companion version moves.
\\
\midrule
\today & The Fixed Header (container epoch) &
Chapter~8 gains \sectionsc{The Container Epoch} and
\ref{req:format:container-epoch}
(\texttt{spec/CONTRACT\_FORMAT\_EPOCH\_MAJOR1.md}).
\texttt{format\_major} becomes \tablenums{1} and \texttt{format\_minor}
restarts at \tablenums{0}. A reader classifies the major three ways ---
legacy \tablenums{0}, current \tablenums{1}, unsupported otherwise ---
and carries the classification as a value rather than re-deriving it by
comparison, so a legacy container is decoded deliberately instead of
refused.
The epoch exists because a canonical base is only meaningful when it has
been validated against a reduction authority, and nothing in a document
could previously attest that: the reduction-algorithm version is a bare
integer that a writer copies from the base's own self-report, so the one
check that would detect a stale base necessarily passes for a base that
was propagated conformingly. The header can attest it because the header
never changes after creation --- which makes the epoch a property of the
file's creation that no later commit can confer. A legacy container may
therefore open while it carries no base, \MUSTNOT{} acquire one, and is
refused outright if it already has one; a repack into a freshly created
major-1 container is the only path forward, and read-only degradation is
explicitly not offered, since a restricted view of unverifiable canonical
state is still unverifiable canonical state. Corruption keeps precedence
over every epoch rule in both epochs, so tampering is never reported as
staleness. \sectionsc{Schema Versioning} gains the corresponding
distinction between the schema axis and the container axis.
\\
\bottomrule
\end{longtable}

Binary file not shown.

View File

@ -234,7 +234,7 @@
{\Large\scshape\color{epiphanyslate}Text Projection}\\[6pt]
{\large\itshape\color{epiphanyslate}A companion to the Core Specification}\\[14pt]
{\color{epiphanygold}\rule{3in}{0.8pt}}\\[24pt]
{\normalsize\color{epiphanyink}Version 0.13.0 --- The genesis ladder closes: \texttt{create-measure} reaches the grammar}\\[4pt]
{\normalsize\color{epiphanyink}Version 0.14.0 --- Canonical bases leave the companion: the container epoch carries what text cannot}\\[4pt]
{\small\color{epiphanyslate}Normative for the text form it defines}
\vfill
\end{titlepage}
@ -483,7 +483,7 @@ A projection is, in order:
\begin{requirement}
\label{req:textproj:header-version}
A parser implementing this companion \MUST{} accept exactly one header
version: \texttt{(0 13 0)}, the version of the companion it implements. It
version: \texttt{(0 14 0)}, the version of the companion it implements. It
\MUST{} reject any other version at line one.
Multi-version acceptance and text migrate-on-read are deferred in the same
@ -534,7 +534,7 @@ projection introduces no ordering of its own.
A parser \MUST{} reject every \texttt{(blob ...)} line whose blob is
unreferenced by canonical state
(Requirement~\ref{req:textproj:canonical-blobs}). At companion
version~0.13.0, neither a canonical operation nor canonical reduced state can
version~0.14.0, neither a canonical operation nor canonical reduced state can
carry a \texttt{BlobId}; canonical state therefore cannot reference a blob,
and a parser \MUST{} reject every \texttt{(blob ...)} line.
\end{requirement}
@ -906,6 +906,31 @@ are ratified.
whose canonical document semantics are identical to the original's. The
bundle's physical layout, chunking, and compression \MAY{} differ.
\textbf{Documents carrying a canonical base are outside this requirement's
domain, and are refused rather than round-tripped.} The exclusion is stated
here, in the requirement's own terms, because the equations below quantify
universally and a reader checking them against an implementation would
otherwise find a conforming implementation failing them. The companion
\MUST{} refuse a base-bearing document at every one of its three boundaries:
projecting a base-bearing bundle to text, parsing text that declares a
canonical base, and serializing a directly constructed document that carries
one.
The reason is that this format cannot carry the guarantee the base needs. A
canonical base is only meaningful when it has been validated against a
reduction authority, and that validation is attested by the \emph{container}
epoch (Core Specification, \sectionsc{The Container Epoch}), which a text
document has no way to hold: every field a text format defines can be typed
by hand, so any provenance marker it carried would reduce to trusting its
author. Serializing a base-bearing text document into a freshly created
container would therefore mint a container asserting a validation that never
occurred. A canonical base enters a container only through an explicit
rebuild or repack flow, never through text import.
This is a real loss of capability and is stated as one: a base-bearing bundle
does not round-trip through text. The quantifiers below range over documents
that carry no canonical base.
Equivalently, and more usefully to an implementer: for every bundle $B$,
\[
\textrm{semantics}(\textrm{parse}(\textrm{project}(B))) =
@ -1140,13 +1165,12 @@ an example that cannot be parsed teaches the wrong lesson. The conformance vecto
that pin \emph{real} bytes are a deliverable of the implementation.
A document of one operation --- a transposition of two pitches up a perfect
fifth, over a compacted base --- projects to five lines:
fifth --- projects to four lines:
\begin{lstlisting}
(text-projection (0 13 0))
(text-projection (0 14 0))
(document #x05050505050505050505050505050505 (schema 0 1))
(profile full (0 1 0) (constraints 67108864 (retention 1 () true)))
(canonical-base #x1f8b0000000000000000000000000000 #x 1 full (schema 0 1) #x0000)
(envelope #x00000000000000070000000000000001 #x00000000000000000000000011223344 (stamp 42 7 #x00000000000000070000000000000001) (causal ((#x0000000000000001 3)) (#x00000000000000020000000000000009)) (some #x00000000000000070000000000000005) (primitive (transpose-interval (#x00000000000000070000000000000001 #x00000000000000070000000000000002) (transposition-interval 4 7))))
\end{lstlisting}
@ -1155,6 +1179,19 @@ The envelope's targets are a \emph{set}: strictly increasing, no duplicates
$\mathrm{seq}^{\Uparrow}$). A parser \MUST{} reject a duplicate rather than
absorb it, exactly as the binary decoder does.
Earlier revisions of this companion showed the same document over a compacted
base, as five lines. The fifth line is retained here as the spelling a parser
now \emph{refuses}:
\begin{lstlisting}
(canonical-base #x1f8b0000000000000000000000000000 #x 1 full (schema 0 1) #x0000)
\end{lstlisting}
The grammar still defines the section --- it is what a refusal is defined
against, and what a future rebuild or repack flow will emit --- but no document
containing it parses, projects, or serializes
(\texttt{req:textproj:roundtrip}).
% ===========================================================================
\chapter{Revision History}
\label{ch:history}
@ -1417,6 +1454,27 @@ absorb it, exactly as the binary decoder does.
grammars both claiming \texttt{(0 12 0)}. Cached projections at
\texttt{(0 12 0)} do not migrate; a stale \texttt{TextProjection} chunk is
regenerated, not converted. \\
\today & Chapters 4, 5 & 0.14.0 --- Canonical bases leave the companion
(\texttt{spec/CONTRACT\_FORMAT\_EPOCH\_MAJOR1.md}, pin 3b). The container
format major becomes an \emph{epoch} attesting that every base-bearing commit
was validated against a reduction authority (Core Specification,
\sectionsc{The Container Epoch}); a text document cannot hold that attestation,
because every field this format defines can be typed by hand. Serializing a
base-bearing document into a freshly created container would therefore mint a
container asserting a validation that never happened.
\texttt{req:textproj:roundtrip} accordingly excludes base-bearing documents in
its own terms, and all three boundaries refuse them: projection, parsing, and
serialization.
The first two of those refusals are \emph{new}; serialization had none to
retain. This is a real capability loss and is recorded as one --- a
base-bearing bundle does not round-trip through text until a rebuild or repack
flow exists. The forcing reason for the version bump is the usual one in
reverse: refusing a document the companion previously serialized is a semantic
change, and holding the version would leave two mutually incompatible readings
of \texttt{(0 13 0)}. The \texttt{canonical-base} production is retained in the
grammar --- a refusal is defined against it, and a repack flow will emit it. \\
\bottomrule
\end{longtable}

View File

@ -22,22 +22,23 @@
# document bytes are normative. `<utf8-hex>` is lowercase with no separators.
# textproj.document
textproj.document accept - minimal 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a
textproj.document accept - set_tuning_context 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830353035303530353035303530353035303530353035303530353035303530352028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303037202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203730302030202378303030303030303030303030303030313030303030303030303030303030303729202863617573616c2028292028292920282920287072696d697469766520287365742d74756e696e672d636f6e74657874202874756e696e672d636f6e746578742d73657474696e67732022636d6e2d31322220227465742d31322220287265666572656e63652d70697463682028636d6e2061203020342920237830303030303030303030663037623430292028736d75666c2d76657273696f6e2d726571756972656d656e742028736d75666c2d76657273696f6e2031203430292028736d75666c2d76657273696f6e20312034302929202829292929290a
textproj.document accept - lineage_custom_profile 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a
textproj.document accept - extension_base_two_envelopes 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a
textproj.document accept - rich_document 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a
textproj.document accept - create_staff_group 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830363036303630363036303630363036303630363036303630363036303630362028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303038202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203830302030202378303030303030303030303030303030313030303030303030303030303030303829202863617573616c2028292028292920282920287072696d697469766520286372656174652d73746166662d67726f7570202873746166662d67726f757020237830303030303030303030303030303031303030303030303030303030303030312028736f6d65202273746166662d67726f75702d312229206772616e642d737461666620282378303030303030303030303030303030313030303030303030303030303030303129292929290a
textproj.document accept - create_part_definition 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830373037303730373037303730373037303730373037303730373037303730372028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303039202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203930302030202378303030303030303030303030303030313030303030303030303030303030303929202863617573616c2028292028292920282920287072696d697469766520286372656174652d706172742d646566696e6974696f6e2028706172742d646566696e6974696f6e20237830303030303030303030303030303031303030303030303030303030303030312022706172742d312220282378303030303030303030303030303030313030303030303030303030303030303129292929290a
textproj.document accept - create_analysis_layer 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830383038303830383038303830383038303830383038303830383038303830382028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303061202378303030303030303030303030303030303030303030303030303030303030616220287374616d7020313030302030202378303030303030303030303030303030313030303030303030303030303030306129202863617573616c2028292028292920282920287072696d697469766520286372656174652d616e616c797369732d6c617965722028616e616c797369732d6c61796572202378303030303030303030303030303030313030303030303030303030303030303120226c617965722d3122292929290a
textproj.document accept - create_view 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830393039303930393039303930393039303930393039303930393039303930392028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303062202378303030303030303030303030303030303030303030303030303030303030616220287374616d7020313130302030202378303030303030303030303030303030313030303030303030303030303030306229202863617573616c2028292028292920282920287072696d697469766520286372656174652d766965772028766965772d646566696e6974696f6e20237830303030303030303030303030303031303030303030303030303030303030312022766965772d312220282378303030303030303030303030303030313030303030303030303030303030303129292929290a
textproj.document accept - create_measure 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830613061306130613061306130613061306130613061306130613061306130612028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303063202378303030303030303030303030303030303030303030303030303030303030616220287374616d7020313230302030202378303030303030303030303030303030313030303030303030303030303030306329202863617573616c2028292028292920282920287072696d697469766520286372656174652d6d656173757265202378303030303030303030303030303030313030303030303030303030303030303120286d6561737572652023783030303030303030303030303030303130303030303030303030303030303031202877616c6c2d636c6f636b2031303030303030303030292028736f6d652023783030303030303030303030303030303130303030303030303030303030303031292028736f6d65203129206175746f292929290a
textproj.document reject wrong-header-version superseded_companion_version 28746578742d70726f6a656374696f6e202830203132203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a
textproj.document reject blob-line unreferenced_blob 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28626c6f6220226170706c69636174696f6e2f6f637465742d73747265616d222028292023783031290a
textproj.document reject out-of-order-sections canonical_base_before_extension 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a
textproj.document reject repeated-singular-section lineage_repeated 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a
textproj.document reject operation-envelope-order envelopes_reversed 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a
textproj.document reject profile-declaration-order profiles_reversed 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a
textproj.document reject extension-declaration-order extensions_reversed 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303430343034303430343034303430343034303430343034303430343034303420237830313032303420312066756c6c2028736368656d61203020312920237861303034290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a
textproj.document reject extension-chunk-order extension_chunks_reversed 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312929202378303120237830313032290a2863616e6f6e6963616c2d62617365202378303330333033303330333033303330333033303330333033303330333033303320237830313032303320312066756c6c2028736368656d61203020312920237861303033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a
textproj.document reject missing-trailing-lf final_lf_missing 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e20312028292074727565292929
textproj.document accept - minimal 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a
textproj.document accept - set_tuning_context 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830353035303530353035303530353035303530353035303530353035303530352028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303037202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203730302030202378303030303030303030303030303030313030303030303030303030303030303729202863617573616c2028292028292920282920287072696d697469766520287365742d74756e696e672d636f6e74657874202874756e696e672d636f6e746578742d73657474696e67732022636d6e2d31322220227465742d31322220287265666572656e63652d70697463682028636d6e2061203020342920237830303030303030303030663037623430292028736d75666c2d76657273696f6e2d726571756972656d656e742028736d75666c2d76657273696f6e2031203430292028736d75666c2d76657273696f6e20312034302929202829292929290a
textproj.document accept - lineage_custom_profile 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a
textproj.document accept - extension_base_two_envelopes 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a
textproj.document accept - rich_document 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a
textproj.document accept - create_staff_group 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830363036303630363036303630363036303630363036303630363036303630362028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303038202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203830302030202378303030303030303030303030303030313030303030303030303030303030303829202863617573616c2028292028292920282920287072696d697469766520286372656174652d73746166662d67726f7570202873746166662d67726f757020237830303030303030303030303030303031303030303030303030303030303030312028736f6d65202273746166662d67726f75702d312229206772616e642d737461666620282378303030303030303030303030303030313030303030303030303030303030303129292929290a
textproj.document accept - create_part_definition 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830373037303730373037303730373037303730373037303730373037303730372028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303039202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203930302030202378303030303030303030303030303030313030303030303030303030303030303929202863617573616c2028292028292920282920287072696d697469766520286372656174652d706172742d646566696e6974696f6e2028706172742d646566696e6974696f6e20237830303030303030303030303030303031303030303030303030303030303030312022706172742d312220282378303030303030303030303030303030313030303030303030303030303030303129292929290a
textproj.document accept - create_analysis_layer 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830383038303830383038303830383038303830383038303830383038303830382028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303061202378303030303030303030303030303030303030303030303030303030303030616220287374616d7020313030302030202378303030303030303030303030303030313030303030303030303030303030306129202863617573616c2028292028292920282920287072696d697469766520286372656174652d616e616c797369732d6c617965722028616e616c797369732d6c61796572202378303030303030303030303030303030313030303030303030303030303030303120226c617965722d3122292929290a
textproj.document accept - create_view 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830393039303930393039303930393039303930393039303930393039303930392028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303062202378303030303030303030303030303030303030303030303030303030303030616220287374616d7020313130302030202378303030303030303030303030303030313030303030303030303030303030306229202863617573616c2028292028292920282920287072696d697469766520286372656174652d766965772028766965772d646566696e6974696f6e20237830303030303030303030303030303031303030303030303030303030303030312022766965772d312220282378303030303030303030303030303030313030303030303030303030303030303129292929290a
textproj.document accept - create_measure 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830613061306130613061306130613061306130613061306130613061306130612028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303063202378303030303030303030303030303030303030303030303030303030303030616220287374616d7020313230302030202378303030303030303030303030303030313030303030303030303030303030306329202863617573616c2028292028292920282920287072696d697469766520286372656174652d6d656173757265202378303030303030303030303030303030313030303030303030303030303030303120286d6561737572652023783030303030303030303030303030303130303030303030303030303030303031202877616c6c2d636c6f636b2031303030303030303030292028736f6d652023783030303030303030303030303030303130303030303030303030303030303031292028736f6d65203129206175746f292929290a
textproj.document reject wrong-header-version superseded_companion_version 28746578742d70726f6a656374696f6e202830203133203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a
textproj.document reject blob-line unreferenced_blob 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28626c6f6220226170706c69636174696f6e2f6f637465742d73747265616d222028292023783031290a
textproj.document reject out-of-order-sections canonical_base_before_extension 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a
textproj.document reject repeated-singular-section lineage_repeated 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a
textproj.document reject operation-envelope-order envelopes_reversed 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a
textproj.document reject profile-declaration-order profiles_reversed 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830323032303230323032303230323032303230323032303230323032303230322028736368656d612030203129290a286c696e656167652023783132313231323132313231323132313231323132313231323132313231323132290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303031202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203130302030202378303030303030303030303030303030313030303030303030303030303030303129202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030312929290a
textproj.document reject extension-declaration-order extensions_reversed 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830343034303430343034303430343034303430343034303430343034303430342028736368656d612030203129290a286c696e656167652023783134313431343134313431343134313431343134313431343134313431343134290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2870726f66696c652028637573746f6d20237863636363636363636363636363636363636363636363636363636363636363632920283120322033292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303230323032303230323032303230323032303230323032303230323032303220283120302032292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830332929202378303220237830323033290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322929202378303120237830313032290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303034202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203430302030202378303030303030303030303030303030313030303030303030303030303030303429202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030342929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303035202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203530302030202378303030303030303030303030303030313030303030303030303030303030303529202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030352929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303036202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203630302030202378303030303030303030303030303030313030303030303030303030303030303629202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030362929290a
textproj.document reject extension-chunk-order extension_chunks_reversed 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830333033303330333033303330333033303330333033303330333033303330332028736368656d612030203829290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a28657874656e73696f6e202378303130313031303130313031303130313031303130313031303130313031303120283120302031292066616c73652028286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830322920286368756e6b20657874656e73696f6e2d646174612028736368656d61203020312920237830312929202378303120237830313032290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303032202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203230302030202378303030303030303030303030303030313030303030303030303030303030303229202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030322929290a28656e76656c6f70652023783030303030303030303030303030303130303030303030303030303030303033202378303030303030303030303030303030303030303030303030303030303030616220287374616d70203330302030202378303030303030303030303030303030313030303030303030303030303030303329202863617573616c2028292028292920282920287072696d6974697665202864656c6574652d726567696f6e20237830303030303030303030303030303031303030303030303030303030303030332929290a
textproj.document reject missing-trailing-lf final_lf_missing 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830313031303130313031303130313031303130313031303130313031303130312028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e20312028292074727565292929
textproj.document reject canonical-base-unsupported canonical_base_present 28746578742d70726f6a656374696f6e202830203134203029290a28646f63756d656e7420237830623062306230623062306230623062306230623062306230623062306230622028736368656d612030203129290a2870726f66696c652066756c6c20283020312030292028636f6e73747261696e74732036373130383836342028726574656e74696f6e203120282920747275652929290a2863616e6f6e6963616c2d62617365202378306230623062306230623062306230623062306230623062306230623062306220237830313032306220312066756c6c2028736368656d61203020312920237861303062290a