epiphany/crates/epiphany-textproj/src/serialize.rs

672 lines
29 KiB
Rust

//! Serialization of parsed Text Projection documents into canonical bundles.
//!
//! [`serialize_document`] stages the payloads a [`TextDocument`] carries inline
//! — the canonical base's root chunk, every extension's preserved chunks, and
//! one operation-envelope block — and lets [`Bundle::create`] /
//! [`Bundle::commit`] do everything `req:textproj:derive-or-carry` says a
//! serializer must not: assign offsets, content-address every chunk, and
//! de-duplicate identical content. **Nothing here computes a `ChunkId`, a
//! `ContentHash`, or an offset** — every physical field written into the
//! manifest comes straight from the [`ChunkRef`](epiphany_bundle::ChunkRef)s
//! `commit` hands back.
//!
//! # Block splitting is a free physical choice
//!
//! `req:textproj:roundtrip` only requires the *bundle's* physical layout to
//! round-trip in the second, byte-checkable equation (`project(serialize(parse(T)))
//! == T`, quantified over texts, not bundles); how a serializer packs envelopes
//! into blocks is unconstrained. This module always emits **exactly one**
//! operation-envelope block, containing every envelope the document carries (in
//! the order the document carries them) — the simplest possible choice, and
//! sufficient because block boundaries are storage artifacts, never semantic
//! structure (Chapter 8: *"the set of envelopes is the union of all envelopes
//! across all referenced blocks"*).
//!
//! # No accelerator is (re)written
//!
//! A [`TextDocument`] carries no `operation_index_root`, `acceleration_snapshots`,
//! `text_projection_root`, `integrity_root`, or `operation_block_summaries` —
//! they are non-canonical and the companion text does not carry them
//! (`req:textproj:derive-or-carry`). The manifest this module builds leaves every
//! one of those fields at its empty/`None` default. A bundle serialized from a
//! `TextDocument` therefore comes back from this module *without* any
//! accelerator a previous generation might have had. That reads as data loss; it
//! is not — none of those fields contributes to canonical document semantics,
//! and a consumer that wants them back rebuilds them the same way any bundle
//! writer does (e.g. `epiphany_bundle::fuzz` and the testkit's operation-index
//! harness both scan-and-rebuild rather than trust a carried-forward index).
//!
//! # No blob is staged
//!
//! [`TextDocument::blobs`] is never staged into `manifest.blob_roots` here. Today
//! that vector is always empty: no operation payload or Chapter-5 value in
//! `epiphany-core`/`epiphany-ops` carries a `BlobId`, so no blob is reachable from
//! canonical state (`req:textproj:canonical-blobs`), and this companion's own
//! parser rejects every `(blob ...)` line rather than populate the vector (see
//! `parse`). [`serialize_document`] still checks: a `TextDocument` assembled
//! directly against the type (bypassing `parse`) with a populated `blobs` vector
//! is refused with [`SerializeError::NonEmptyBlobs`] rather than silently
//! dropped — wiring a blob into the manifest here would require a canonical
//! `declared_max_uncompressed_length`/compression policy this companion version
//! does not specify, and silently discarding the caller's bytes is worse.
use std::fmt;
use epiphany_bundle::{
encode_block, BlockStore, Bundle, BundleError, ChunkKind, CommitContext, ExtensionDeclaration,
FileUuid, Manifest, SchemaVersion, SnapshotRef, StagedChunk,
};
use epiphany_determinism::CanonicalEncode;
use epiphany_ops::{operation_block_introduced_minor, OperationEnvelope};
use crate::TextDocument;
/// An error [`serialize_document`] cannot recover from.
#[derive(Debug)]
pub enum SerializeError {
/// The document's `blobs` vector is non-empty. See the module documentation:
/// no blob is canonical today, so a document built by this companion's
/// `parse` never carries one, and staging one here would either wire a
/// 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.
Bundle(BundleError),
}
impl fmt::Display for SerializeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
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}")
}
}
}
}
impl std::error::Error for SerializeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
SerializeError::Bundle(error) => Some(error),
SerializeError::NonEmptyBlobs => None,
SerializeError::CanonicalBaseUnsupported => None,
}
}
}
impl From<BundleError> for SerializeError {
fn from(error: BundleError) -> Self {
SerializeError::Bundle(error)
}
}
/// Serializes a [`TextDocument`] into a freshly created bundle over `store`.
///
/// Two-phase, because [`Bundle::create`] requires a manifest with no canonical
/// roots or blobs (there is nothing to reference before any chunk is written):
/// this creates an empty generation-0 bundle carrying only the document's
/// identity and profile declarations, then stages every payload the document
/// carries inline and commits once, building the real manifest from the
/// [`ChunkRef`](epiphany_bundle::ChunkRef)s that commit assigns.
///
/// Returns [`SerializeError::NonEmptyBlobs`] if `document.blobs` is non-empty
/// (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,
file_uuid: FileUuid,
) -> Result<Bundle<S>, SerializeError> {
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),
crate::production_caps(),
)?;
let mut staged = Vec::new();
if let Some(base) = &document.canonical_base {
// The canonical base's root chunk: kind and schema version are carried
// verbatim from the document (they are not derivable from anything),
// and its payload is staged exactly as the document carries it. `commit`
// computes the chunk's id, hash, and offset.
staged.push(StagedChunk {
kind: ChunkKind::Snapshot,
schema_version: base.root_schema_version,
payload: base.root_payload.clone(),
});
}
for extension in &document.extensions {
for chunk in &extension.chunks {
staged.push(StagedChunk {
kind: chunk.kind,
schema_version: chunk.schema_version,
payload: chunk.payload.clone(),
});
}
}
staged.push(stage_operation_envelope_block(document));
// The carried manifest SchemaVersion is supplied explicitly (G-minor pin
// 6.1/8/11): this companion never derives it, it round-trips exactly
// what the document declares.
bundle.commit_versioned(&staged, document.manifest_schema_version, |ctx| {
build_manifest(document, ctx)
})?;
Ok(bundle)
}
/// The manifest [`Bundle::create`] is given: the document's identity and
/// declared profiles, and nothing else. `create` itself rejects a manifest
/// carrying canonical roots or blobs, so every root is added by the subsequent
/// commit (see [`build_manifest`]). If `document.profiles` is empty or
/// otherwise unemittable, `Bundle::create` reports that itself — this function
/// does not invent a fallback profile the document did not declare.
fn empty_manifest(document: &TextDocument) -> Manifest {
let mut manifest = Manifest::empty(document.document_id);
manifest.lineage_id = document.lineage_id;
manifest.profile_declarations = document.profiles.clone();
manifest
}
/// Encodes every envelope the document carries, in the order it carries them,
/// into a single operation-envelope block payload (see the module
/// documentation on why one block is the right choice here). The block's
/// schema version is the max over its envelopes' `schema_major` — never a fixed
/// baseline — so a block that carries a higher-major payload (e.g. a v1
/// `CreateRegion` or a v2 cross-cutting value) is never mis-stamped major 0,
/// mirroring `StagedChunk::operation_block_versioned`'s own contract.
fn stage_operation_envelope_block(document: &TextDocument) -> StagedChunk {
let payloads: Vec<Vec<u8>> = document
.envelopes
.iter()
.map(CanonicalEncode::to_canonical_bytes)
.collect();
let major = document
.envelopes
.iter()
.map(OperationEnvelope::schema_major)
.max()
.unwrap_or(0);
let epoch_max = operation_block_introduced_minor(&document.envelopes);
StagedChunk::operation_block_versioned(
encode_block(&payloads),
SchemaVersion::for_major_at_epoch(major, epoch_max),
)
}
/// Builds the committed manifest from the previous (empty) manifest and the
/// [`ChunkRef`](epiphany_bundle::ChunkRef)s [`Bundle::commit`] assigned to the
/// chunks [`serialize_document`] staged, in the same order it staged them: the
/// canonical base's root (if any), then each extension's preserved chunks in
/// turn, then the single operation-envelope block. Every physical field
/// (`ChunkId`, `ContentHash`, offset) comes from `ctx.new_chunks` — nothing here
/// recomputes one (`req:textproj:derive-or-carry`). `operation_index_root`,
/// `acceleration_snapshots`, `text_projection_root`, `integrity_root`,
/// `operation_block_summaries`, and `blob_roots` are left untouched at the
/// previous manifest's empty defaults; see the module documentation for why
/// that is correct rather than lossy.
fn build_manifest(document: &TextDocument, ctx: &CommitContext) -> Manifest {
let mut manifest = ctx.previous_manifest.clone();
let mut cursor = 0usize;
if let Some(base) = &document.canonical_base {
let root = ctx.new_chunks[cursor];
cursor += 1;
manifest.canonical_base = Some(SnapshotRef {
snapshot_id: base.snapshot_id,
covers_causal_frontier: base.covers_causal_frontier.clone(),
reduction_algorithm_version: base.reduction_algorithm_version,
profile_id: base.profile_id,
root,
hash: root.hash,
});
}
let mut extension_declarations = Vec::with_capacity(document.extensions.len());
for extension in &document.extensions {
let mut preserved_chunk_roots = Vec::with_capacity(extension.chunks.len());
for _ in &extension.chunks {
preserved_chunk_roots.push(ctx.new_chunks[cursor]);
cursor += 1;
}
extension_declarations.push(ExtensionDeclaration {
extension_id: extension.extension_id,
version: extension.version,
required: extension.required,
preserved_chunk_roots,
affected_object_kinds: extension.affected_object_kinds.clone(),
edit_barriers: extension.edit_barriers.clone(),
});
}
manifest.extension_declarations = extension_declarations;
manifest.operation_roots = vec![ctx.new_chunks[cursor]];
cursor += 1;
debug_assert_eq!(
cursor,
ctx.new_chunks.len(),
"every chunk staged by serialize_document must be wired into the manifest exactly once"
);
manifest
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{TextCanonicalBase, TextChunk, TextExtension};
use epiphany_bundle::{
DocumentId, ExtensionId, FrontierBytes, LineageId, MemStore, ProfileConstraints,
ProfileDeclaration, ProfileId, ReductionAlgorithmVersion, SemVer, SnapshotId,
};
use epiphany_determinism::fuzz::SplitMix64;
use epiphany_ops::{decode_envelope, fuzz::gen_envelope_set};
/// A real, varied envelope set (every operation kind the generator reaches),
/// so the schema-major derivation and the envelope round trip both have
/// something to bite on.
fn envelopes(seed: u64, n: usize) -> Vec<OperationEnvelope> {
let mut rng = SplitMix64::new(seed);
gen_envelope_set(&mut rng, n)
}
/// A profile declaration distinguishable from `ProfileDeclaration::full()`'s
/// default version, so a test that forgets to carry `document.profiles`
/// through cannot pass by accident (`full()` would still be a `Full`
/// profile, just at a different version).
fn base_profile() -> ProfileDeclaration {
ProfileDeclaration {
profile_id: ProfileId::Full,
version: SemVer::new(0, 2, 0),
constraints: ProfileConstraints::DEFAULT_FULL,
}
}
fn minimal_document(seed: u64) -> TextDocument {
TextDocument {
document_id: DocumentId([seed as u8; 16]),
manifest_schema_version: SchemaVersion::V0,
lineage_id: None,
profiles: vec![base_profile()],
extensions: Vec::new(),
canonical_base: None,
blobs: Vec::new(),
envelopes: envelopes(seed, 5),
}
}
fn document_with_extension(seed: u64) -> TextDocument {
let mut document = minimal_document(seed);
document.extensions.push(TextExtension {
extension_id: ExtensionId([7; 16]),
version: SemVer::new(1, 0, 0),
required: false,
chunks: vec![
TextChunk {
kind: ChunkKind::ExtensionData,
schema_version: SchemaVersion::V0,
payload: b"chunk-a".to_vec(),
},
TextChunk {
kind: ChunkKind::ExtensionData,
schema_version: SchemaVersion::V0,
payload: b"chunk-b".to_vec(),
},
],
affected_object_kinds: vec![0xAA],
edit_barriers: vec![0xBB, 0xCC],
});
document
}
fn document_with_canonical_base(seed: u64) -> TextDocument {
let mut document = minimal_document(seed);
document.canonical_base = Some(TextCanonicalBase {
snapshot_id: SnapshotId([9; 16]),
covers_causal_frontier: FrontierBytes(vec![1, 2, 3]),
reduction_algorithm_version: ReductionAlgorithmVersion(1),
profile_id: ProfileId::Full,
root_schema_version: SchemaVersion::V0,
root_payload: b"snapshot-root".to_vec(),
});
document
}
/// 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.envelopes = envelopes(seed, 60);
document
}
fn serialize_and_reopen(document: &TextDocument) -> Bundle<MemStore> {
let bundle = serialize_document(document, MemStore::new(), FileUuid([1; 16]))
.expect("a well-formed document serializes");
let image = bundle.into_store().into_bytes();
Bundle::open(MemStore::from_bytes(image), crate::production_caps())
.expect("the serialized bundle reopens")
}
#[test]
fn s13_text_document_round_trips_the_carried_manifest_schema_version() {
let mut document = minimal_document(42);
document.manifest_schema_version = SchemaVersion::new(0, 8);
let reopened = serialize_and_reopen(&document);
let bundle_document =
crate::project::document_from_bundle(&reopened).expect("bundle reads cleanly");
assert_eq!(
bundle_document.manifest_schema_version,
SchemaVersion::new(0, 8),
"the carried manifest SchemaVersion must round-trip exactly, never the baseline"
);
}
#[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);
document.lineage_id = Some(LineageId([2; 16]));
let reopened = serialize_and_reopen(&document);
assert_eq!(reopened.manifest().document_id, document.document_id);
assert_eq!(reopened.manifest().lineage_id, document.lineage_id);
}
#[test]
fn profile_declarations_round_trip() {
let document = minimal_document(2);
let reopened = serialize_and_reopen(&document);
assert_eq!(reopened.manifest().profile_declarations, document.profiles);
}
#[test]
fn missing_profile_declaration_surfaces_the_bundles_own_rejection() {
// serialize_document must not invent a fallback profile the document
// did not declare: an empty `profiles` propagates Bundle::create's own
// "no declared profile" rejection.
let mut document = minimal_document(3);
document.profiles.clear();
let result = serialize_document(&document, MemStore::new(), FileUuid([1; 16]));
assert!(matches!(result, Err(SerializeError::Bundle(_))));
}
#[test]
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 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]
fn extension_round_trips_fields_and_chunk_payloads() {
let document = document_with_extension(5);
let reopened = serialize_and_reopen(&document);
let expected = &document.extensions[0];
let declaration = reopened
.manifest()
.extension_declarations
.iter()
.find(|d| d.extension_id == expected.extension_id)
.expect("extension declaration present after reopen");
assert_eq!(declaration.version, expected.version);
assert_eq!(declaration.required, expected.required);
assert_eq!(
declaration.affected_object_kinds,
expected.affected_object_kinds
);
assert_eq!(declaration.edit_barriers, expected.edit_barriers);
assert_eq!(
declaration.preserved_chunk_roots.len(),
expected.chunks.len()
);
// The manifest's own canonical encoding sorts preserved_chunk_roots by
// ChunkRef (kind, hash, offset), not by the document's chunk order, so
// compare payload sets rather than assume position survives.
let mut payloads: Vec<Vec<u8>> = declaration
.preserved_chunk_roots
.iter()
.map(|root| reopened.read_chunk(root).expect("preserved chunk reads"))
.collect();
let mut expected_payloads: Vec<Vec<u8>> =
expected.chunks.iter().map(|c| c.payload.clone()).collect();
payloads.sort();
expected_payloads.sort();
assert_eq!(
payloads, expected_payloads,
"preserved chunk payloads survive, order aside"
);
}
#[test]
fn envelopes_round_trip_through_a_single_operation_block() {
let document = minimal_document(6);
assert!(document.envelopes.len() > 1, "fixture reach check");
let reopened = serialize_and_reopen(&document);
assert_eq!(
reopened.manifest().operation_roots.len(),
1,
"one operation-envelope block, by design"
);
let root = reopened.manifest().operation_roots[0];
let payloads = reopened
.read_operation_block(&root)
.expect("operation block reads");
let recovered: Vec<OperationEnvelope> = payloads
.iter()
.map(|bytes| decode_envelope(bytes).expect("canonical envelope decodes"))
.collect();
assert_eq!(recovered, document.envelopes);
}
#[test]
fn operation_block_schema_major_is_derived_not_hardcoded() {
let document = rich_document(7);
let expected_major = document
.envelopes
.iter()
.map(OperationEnvelope::schema_major)
.max()
.unwrap_or(0);
assert!(
expected_major > 0,
"fixture must include a schema-major-bearing operation to exercise the derivation"
);
let expected_epoch_max = operation_block_introduced_minor(&document.envelopes);
let reopened = serialize_and_reopen(&document);
let root = reopened.manifest().operation_roots[0];
assert_eq!(
root.schema_version,
SchemaVersion::for_major_at_epoch(expected_major, expected_epoch_max)
);
}
#[test]
fn a_document_with_no_envelopes_still_stages_one_empty_operation_block() {
let mut document = minimal_document(8);
document.envelopes.clear();
let reopened = serialize_and_reopen(&document);
assert_eq!(reopened.manifest().operation_roots.len(), 1);
let root = reopened.manifest().operation_roots[0];
assert!(reopened
.read_operation_block(&root)
.expect("empty operation block still reads")
.is_empty());
}
#[test]
fn nonempty_blobs_are_rejected_rather_than_dropped() {
let mut document = minimal_document(9);
document.blobs.push(crate::TextBlob {
media_type: "audio/wav".to_string(),
declared_max_uncompressed_length: None,
payload: b"nope".to_vec(),
});
let result = serialize_document(&document, MemStore::new(), FileUuid([1; 16]));
assert!(matches!(result, Err(SerializeError::NonEmptyBlobs)));
}
#[test]
fn a_rich_document_round_trips_every_root_at_once() {
let document = rich_document(10);
let reopened = serialize_and_reopen(&document);
assert_eq!(reopened.manifest().document_id, document.document_id);
// `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
.verify_canonical_chunks()
.expect("every canonical chunk this module wrote is intact");
assert!(!reopened.is_read_only());
assert!(reopened.anomalies().is_empty());
}
/// `req:textproj` verification discipline item 3: a round-trip suite that
/// never exercises an extension, a canonical base, or a multi-envelope
/// document proves far less than its green tick suggests. Count what this
/// suite actually covers.
#[test]
fn the_suite_exercises_an_extension_a_canonical_base_and_multiple_envelopes() {
let documents = vec![
minimal_document(20),
document_with_extension(21),
document_with_canonical_base(22),
rich_document(23),
];
let with_extension = documents
.iter()
.filter(|d| !d.extensions.is_empty())
.count();
let with_canonical_base = documents
.iter()
.filter(|d| d.canonical_base.is_some())
.count();
let with_multiple_envelopes = documents.iter().filter(|d| d.envelopes.len() > 1).count();
assert!(
with_extension >= 1,
"reach: no document carries an extension"
);
assert!(
with_canonical_base >= 1,
"reach: no document carries a canonical base"
);
assert!(
with_multiple_envelopes >= 1,
"reach: no document carries more than one envelope"
);
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);
}
}
/// P13-S27 test 10a — the test M5a breaks. **In `epiphany-textproj`**,
/// because `epiphany-bundle` must not depend on `epiphany-ops` (pin 1, §0.3)
/// and so no test there can reach the real authority.
///
/// # The `1` is a deliberate LITERAL, and that is load-bearing
///
/// Comparing against `CURRENT_REDUCTION_ALGORITHM_VERSION` would compare the
/// constant with itself laundered through one function call: mutate the
/// constant and **both sides move**, so the assertion would hold for every
/// value and M5a could not break it. **Do not "tidy" this into the
/// constant** — doing so makes M5a vacuous while leaving every test green,
/// a failure invisible to the suite (contract §7 item 4b exists to catch it).
///
/// **This test failed when P13-S16 bumped the authority `0` → `1`, exactly as
/// S27 predicted it would**, and the literal below was updated by hand. That
/// is the tripwire working, not friction: editing this literal is how a rung
/// *states* that the authority moved. A future bump must break this test
/// again.
#[test]
fn serialize_document_supplies_the_real_reduction_authority() {
let document = minimal_document(42);
let bundle = serialize_document(&document, MemStore::new(), FileUuid([1; 16]))
.expect("a base-free document serializes");
assert_eq!(
bundle.capabilities().current_reduction_version,
ReductionAlgorithmVersion(1),
"the production writer must supply the real authority, not a literal of its own"
);
}
}