diff --git a/crates/epiphany-textproj/src/project.rs b/crates/epiphany-textproj/src/project.rs index b1d3874..ffb458b 100644 --- a/crates/epiphany-textproj/src/project.rs +++ b/crates/epiphany-textproj/src/project.rs @@ -557,14 +557,42 @@ pub fn document_from_bundle( // TextDocument -> text. // =========================================================================== -/// Projects a whole [`TextDocument`] to its canonical text: the header, then -/// every present section in the normative `projection` sequence (header, +/// Projects a whole [`TextDocument`] to its canonical text, refusing a +/// base-bearing document (`CONTRACT_FORMAT_EPOCH_MAJOR1.md` pin 3b). +/// +/// This is the **document-level** half of pin 3b's projection refusal. +/// [`document_from_bundle`] guards the bundle-level half, but guarding only +/// that one left the refusal asymmetric in a way the pin forbids: this +/// function is public and takes a directly constructed [`TextDocument`], so a +/// caller that never touches a [`Bundle`] could mint text carrying a +/// `(canonical-base ...)` line — text that [`crate::parse::parse_document`] +/// then rejects. A projector able to emit what the parser refuses is exactly +/// the asymmetry pin 3b exists to close, and `req:textproj:roundtrip`'s second +/// equation quantifies over every valid text. +/// +/// Sections are written in the normative `projection` sequence (header, /// document, lineage?, profile*, extension*, canonical-base?, blob*, /// envelope*), one line per element, each terminated by a single LF /// (`req:textproj:envelope-per-line`). Section order is normative and is -/// simply the order this function writes in; it introduces no ordering of its -/// own beyond `req:textproj:derived-ordering`'s blob sort. -pub fn project_text_document(document: &TextDocument) -> String { +/// simply the order [`render_text_document`] writes in; it introduces no +/// ordering of its own beyond `req:textproj:derived-ordering`'s blob sort. +pub fn project_text_document(document: &TextDocument) -> Result { + if document.canonical_base.is_some() { + return Err(ProjectError::CanonicalBaseUnsupported); + } + Ok(render_text_document(document)) +} + +/// Renders a [`TextDocument`] to text **without** pin 3b's refusal, so it will +/// emit a `(canonical-base ...)` line when the document carries one. +/// +/// Deliberately **not public**. Its only legitimate caller is the corpus's +/// `canonical_base_present` negative vector, which must contain the base +/// spelling in order to assert that a parser refuses it — the spelling has to +/// be produced by something, and producing it is not the same as permitting +/// it. Every other caller goes through [`project_text_document`], which +/// refuses first. +pub(crate) fn render_text_document(document: &TextDocument) -> String { let mut lines: Vec = Vec::new(); lines.push(project_header()); lines.push(project_document( @@ -596,7 +624,7 @@ pub fn project_text_document(document: &TextDocument) -> String { /// Composes both stages: reads `bundle` into a [`TextDocument`], then projects /// it to its canonical text. pub fn project_bundle(bundle: &Bundle) -> Result { - Ok(project_text_document(&document_from_bundle(bundle)?)) + project_text_document(&document_from_bundle(bundle)?) } #[cfg(test)] @@ -1045,7 +1073,8 @@ mod tests { fn projected_text_never_emits_a_blob_line_and_orders_sections_correctly() { let bundle = build_sample_bundle(); let document = document_from_bundle(&bundle).expect("bundle reads cleanly"); - let text = project_text_document(&document); + let text = + project_text_document(&document).expect("this fixture carries no canonical base"); assert!( !text.lines().any(|line| line.starts_with("(blob ")), @@ -1080,7 +1109,8 @@ mod tests { fn project_bundle_composes_both_stages() { let bundle = build_sample_bundle(); let via_two_stages = - project_text_document(&document_from_bundle(&bundle).expect("bundle reads cleanly")); + project_text_document(&document_from_bundle(&bundle).expect("bundle reads cleanly")) + .expect("the sample bundle carries no canonical base"); let via_one_call = project_bundle(&bundle).expect("bundle reads cleanly"); assert_eq!(via_one_call, via_two_stages); } @@ -1170,6 +1200,49 @@ mod tests { )); } + #[test] + fn projecting_a_base_bearing_text_document_is_refused() { + // Pin 3b's projection side, on the half that a caller can actually + // reach today. `project_text_document` is public and takes a directly + // constructed `TextDocument`, so guarding only `document_from_bundle` + // left the refusal asymmetric: this path could emit a + // `(canonical-base ...)` line that `parse_document` then rejects, and + // a projector able to produce what the parser refuses is exactly what + // pin 3b forbids. Unlike the bundle-level guard above, this one needs + // no live `Bundle` and is therefore reachable by any caller of this + // crate. + let document = TextDocument { + document_id: DocumentId([7; 16]), + manifest_schema_version: SchemaVersion::V0, + lineage_id: None, + profiles: vec![ProfileDeclaration::full()], + extensions: Vec::new(), + canonical_base: Some(TextCanonicalBase { + snapshot_id: SnapshotId([7; 16]), + covers_causal_frontier: FrontierBytes::empty(), + reduction_algorithm_version: ReductionAlgorithmVersion(0), + profile_id: ProfileId::Full, + root_schema_version: SchemaVersion::V0, + root_payload: b"snapshot-root".to_vec(), + }), + blobs: Vec::new(), + envelopes: Vec::new(), + }; + assert!(matches!( + project_text_document(&document), + Err(ProjectError::CanonicalBaseUnsupported) + )); + + // And the crate-private formatter still emits the base line — the + // corpus's negative vector depends on it. The refusal is a property of + // the public projector, not of the renderer: if this ever stops + // emitting the section, the `canonical_base_present` reject vector + // silently stops carrying the spelling it exists to reject. + assert!(render_text_document(&document) + .lines() + .any(|line| line.starts_with("(canonical-base "))); + } + // ----------------------------------------------------------------- // Suite reach: count, don't just claim, coverage of an extension, a // canonical base, and a multi-envelope document. diff --git a/crates/epiphany-textproj/src/vectors.rs b/crates/epiphany-textproj/src/vectors.rs index 02d58ac..8cd613d 100644 --- a/crates/epiphany-textproj/src/vectors.rs +++ b/crates/epiphany-textproj/src/vectors.rs @@ -32,7 +32,7 @@ use epiphany_ops::{ }; use crate::parse::parse_document; -use crate::project::{project_bundle, project_text_document}; +use crate::project::{project_bundle, project_text_document, render_text_document}; use crate::serialize::serialize_document; use crate::{TextCanonicalBase, TextChunk, TextDocument, TextExtension}; @@ -468,28 +468,52 @@ fn accept_documents() -> Vec<(&'static str, String)> { }; vec![ - ("minimal", project_text_document(&minimal)), - ("set_tuning_context", project_text_document(&tuning_context)), + ( + "minimal", + project_text_document(&minimal).expect("an accept document carries no canonical base"), + ), + ( + "set_tuning_context", + project_text_document(&tuning_context) + .expect("an accept document carries no canonical base"), + ), ( "lineage_custom_profile", - project_text_document(&lineage_custom), + project_text_document(&lineage_custom) + .expect("an accept document carries no canonical base"), ), ( "extension_base_two_envelopes", - project_text_document(&extension_two_envelopes), + project_text_document(&extension_two_envelopes) + .expect("an accept document carries no canonical base"), + ), + ( + "rich_document", + project_text_document(&rich).expect("an accept document carries no canonical base"), + ), + ( + "create_staff_group", + project_text_document(&staff_group) + .expect("an accept document carries no canonical base"), ), - ("rich_document", project_text_document(&rich)), - ("create_staff_group", project_text_document(&staff_group)), ( "create_part_definition", - project_text_document(&part_definition), + project_text_document(&part_definition) + .expect("an accept document carries no canonical base"), ), ( "create_analysis_layer", - project_text_document(&analysis_layer), + project_text_document(&analysis_layer) + .expect("an accept document carries no canonical base"), + ), + ( + "create_view", + project_text_document(&view).expect("an accept document carries no canonical base"), + ), + ( + "create_measure", + project_text_document(&measure).expect("an accept document carries no canonical base"), ), - ("create_view", project_text_document(&view)), - ("create_measure", project_text_document(&measure)), ] } @@ -679,6 +703,13 @@ pub fn document_vectors() -> Vec { // 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. + // + // This is the one legitimate caller of `render_text_document`, the + // crate-private formatter that skips pin 3b's projection refusal: + // `project_text_document` now refuses a base-bearing document, and a + // negative vector still has to *contain* the spelling it asserts is + // refused. Producing the bytes is not the same as permitting them — every + // other vector above goes through the checked projector. let canonical_base_present = TextDocument { document_id: DocumentId([11; 16]), manifest_schema_version: SchemaVersion::V0, @@ -694,7 +725,7 @@ pub fn document_vectors() -> Vec { "reject", "canonical-base-unsupported", "canonical_base_present", - project_text_document(&canonical_base_present).into_bytes(), + render_text_document(&canonical_base_present).into_bytes(), )); vectors @@ -992,7 +1023,8 @@ mod tests { let document = parse_document(&text).unwrap_or_else(|e| panic!("{name} must parse: {e}")); assert_eq!(document.envelopes.len(), 1, "{name} carries one envelope"); - let reprojected = project_text_document(&document); + let reprojected = project_text_document(&document) + .expect("an accept document carries no canonical base"); assert_eq!( reprojected, text, "{name}: project(serialize(parse(T))) == T must hold" @@ -1026,7 +1058,8 @@ mod tests { .1; let document = parse_document(&text).unwrap_or_else(|e| panic!("{name} must parse: {e}")); assert_eq!(document.envelopes.len(), 1, "{name} carries one envelope"); - let reprojected = project_text_document(&document); + let reprojected = + project_text_document(&document).expect("an accept document carries no canonical base"); assert_eq!( reprojected, text, "{name}: project(serialize(parse(T))) == T must hold" diff --git a/spec/PASS13_CANDIDATES.md b/spec/PASS13_CANDIDATES.md index c59cca3..3d4d8ec 100644 --- a/spec/PASS13_CANDIDATES.md +++ b/spec/PASS13_CANDIDATES.md @@ -123,4 +123,4 @@ evidence in isolation. | P13-S25 | **The committed decode corpus's numbered tag rows lock byte→byte, not variant→byte — one row already has the property the other thirty-nine lack.** `ops/src/vectors.rs:206`–`:209` emits one row per tag as `format!("tag_{:02}", tag.discriminant())` carrying `[discriminant]`: **both the name and the payload derive from the value alone**, so `tag_32` asserts that `0x20` round-trips and never that `SetCanvasLayoutDefaults` is 32. The `Registered` row (`:210`–`:217`) is different — its name is the hard-coded string `"registered"` while its bytes are computed from the variant, so the frozen literal at `spec/vectors/decode_vectors.txt:80` binds the association. **Disposition B of P13-S22:** give the numbered rows the same property. It **does** catch the coordinated permutation — by exactly the `Registered` mechanism, with the committed text serving as the independent statement — and it propagates the property to every implementation that reads the cross-impl corpus, which an in-crate Rust test cannot do | `spec/CONTRACT_P13S22_TAGLOCK.md` (disposition B, considered and deferred during the 2026-07-31 ruling; filed rather than left as a closing remark, per the same discipline that moved P13-S22 out of P13-S15's resolved row) | **open. Complementary to P13-S22, not a replacement for it, and not a re-litigation of it.** P13-S22 landed disposition A (`tag_wire_discriminants_are_golden`, `payload.rs:2730`), which fails **by variant name inside the crate**. B cannot supply that: its failure is still *"spec/vectors/decode_vectors.txt is stale. Regenerate: …"* (`testkit/src/vectors.rs:224`) — the misleading diagnosis P13-S22 was filed about — even though the diff text would now name variants. **What B buys is cross-implementation reach; what it costs is churn in a committed artifact other implementations pin.** Both are wanted; neither substitutes for the other. Sequencing note: run B's own signing mutation as the coordinated permutation (literals *and* declaration lines), since the literal-only form is caught today by row ordering and proves nothing | | P13-S26 | **A doc comment in shipped code claims a specification repair that never landed, and the claim is guarded on the code side and nowhere on the specification side.** `crates/epiphany-core/src/invariants.rs:69`–`:71` enumerates invariant 10's four reference classes and states that *“genesis tranche G3a repairs this prose to name what the check body already enforced”*. **It did not.** `core_spec.tex:6570`–`:6572`, the normative enumeration item 10, still reads only *“Every cross-cutting structure's references resolve to extant objects in the graph, except where explicit re-anchoring rules permit transient dangling states during edits”* — naming neither a staff's declared instrument, a staff's group, a staff group's members, a part's staves, a view's active layers, nor any of the meter/time-signature references the Rust doc lists and the check body enforces. The repair landed in the Rust doc comment only. **The asymmetry is the defect's sharp edge:** the Rust doc block is protected by a grep-assert, `t12_invariant_10_doc_comment_names_the_four_reference_classes` (`invariants.rs:4554`, needles at `:4562`–`:4566`), so the side that is *wrong about the other* is the side that is **locked**, while the side that is actually stale is unguarded | this file (found 2026-07-31 during P13-S16 reconnaissance, while verifying that row's invariant-10 citations; no ledger entry covered it) | **open.** **Not a live incorrectness** — the check body is correct and enforces every class; only the normative prose under-describes it, and only the doc comment lies about that. **A P13-S9 instance**, and filed deliberately as one: the loud form (a dangling citation) is caught by `requirement_labels.rs`, and this quiet form — a *true-sounding claim about another document's state* — is caught by nothing. **`invariants.rs:69`–`:71` MUST NOT be “corrected” on its own.** It is currently the only artifact in the tree pointing at the `core_spec.tex` gap; softening the Rust claim in isolation would make the specification defect invisible and convert a caught defect into an uncaught one — which is P13-S9's stated failure mode verbatim. **Repair both sides in one rung**, and consider whether the LaTeX enumeration deserves the grep-assert its Rust mirror already has | | P13-S27 | **The reduction-algorithm-version machinery is self-referential, so the one check that would detect a canonical-semantics change necessarily passes.** `core_spec.tex:11614`–`:11617` is normative — *"Snapshots produced under an earlier algorithm version cannot be used as canonical bases under a later one without rebuilding"* — and `:14369`–`:14372` states that replicas at differing versions *"may produce different canonical states from the same operation set."* The machinery to enforce it appears to exist: `ReductionAlgorithmVersion` (`bundle/src/ids.rs:291`) is a superblock wire field (bytes `68..72`, `superblock.rs:20`); `reduction_version_for` (`bundle.rs:989`) sets a new superblock's value; and `open` (`bundle.rs:396`–`:399`) rejects a mismatch. **But the writer sources the value from the canonical base's own self-report** (mapping the base's `reduction_algorithm_version` through `unwrap_or_default()`), **and the reader compares it only against the superblock that value seeded.** Nothing compares either against the semantics the running implementation actually implements. **The check is not vacuous** — it catches a corrupt or tampered base whose version disagrees with its superblock — but it **necessarily passes for a conformingly propagated stale base**, which is precisely the case the requirement exists to prevent. Supporting: **no constant or accessor anywhere names the implementation's current reduction semantics**, and `ids.rs:288`–`:289` states that *"the algorithm catalog itself lives in `epiphany-ops`"* while nothing of the kind exists in that crate — **a second instance of P13-S26's pattern**, a doc comment asserting a false fact about another module | `spec/CONTRACT_P13S16_PROJECTION.md` pin 0 (found 2026-07-31 while scoping P13-S16, which is a canonical reduction-semantics change and therefore the first rung to need this guarantee; filed in the same ledger edit as the row it blocks) | **open, BLOCKED on P13-S28, and blocking P13-S16.** **Scoped 2026-07-31 as `spec/CONTRACT_P13S27_REDUCTION_AUTHORITY.md` (DRAFT, not dispatchable).** Rulings taken: a typed `BundleCapabilities` required at both `Bundle::open` and `Bundle::create` and carried on the `Bundle` — no default, so every caller states the semantics it implements — and outright rejection on mismatch via a new `CanonicalBaseRequiresRebuild` error, not read-only and not an integrity anomaly. Storing the capability keeps all 57 `commit` sites unchanged; only `open` (57 sites) and `create` (32) move. **The scoping also falsified this row's first reading that the writer path was test-only:** `epiphany-textproj`'s `serialize_document` (`serialize.rs:119`) and `project.rs:936` are production paths that copy a base's `reduction_algorithm_version` verbatim into a fresh `SnapshotRef`, which `commit_versioned` then stamps into the superblock (`bundle.rs:798`) — so production mints self-consistent stale documents **without ever calling `open`**, and the capability must govern writers too. **What blocks it:** contract pin 2a. Baseline authority `0` does not preserve the corpus (`serialize.rs:327` stamps `1` and round-trips it; `vectors.rs:353`/`:363` likewise), and once P13-S16 moves the authority to `1`, a pre-S27 base that happens to carry `1` is **indistinguishable from a legitimately rebuilt one** — a raw `u32` carries no provenance. Four dispositions are recorded there; `FORMAT_MINOR` as a provenance carrier was proposed and **rejected** (the header never changes after creation, `core_spec.tex:10799`, so a legacy bundle committing a freshly validated base keeps its old minor forever; and a minor change may only append append-safe discriminants, `:12258`, not alter acceptance semantics). The surviving requirement — provenance must ride a container property **old readers cannot silently accept** and **a later commit cannot inherit unchanged** — is a format-epoch design, filed as **P13-S28**. **Scope of the claim, deliberately narrow:** this establishes that the **current implementation** has no detection mechanism. It does **not** establish that no reduction-semantics change in the project's history was ever detectable — that needs a history audit not yet done, and the stronger sentence is deliberately not written here. **What closing it requires:** an authority naming the semantics this build implements, and a rejection-or-rebuild path when a base disagrees with it. Until then any rung changing canonical reduction semantics can record its break in prose but cannot make stale bases unusable — which is why P13-S16's contract is complete, ratifiable as a plan, and **not dispatchable**. **Method note:** an earlier draft of S16's pin 0 claimed no writer path existed at all. That was false, and the way it was false is the point — the search behind it looked for `ReductionAlgorithmVersion(` constructor calls, which cannot find a path that propagates an existing value without constructing one. The instrument could not observe the thing it was used to rule out. **UNBLOCKED 2026-08-07:** the format-epoch rung landed and its pin 8 **resolves pin 2a** — reduction-version authority is meaningful only in major-1 containers, so legacy bases are refused by container epoch and never by version arithmetic. The collision pin 2a identified never has to be adjudicated: a pre-S27 base carrying `1` and a rebuilt S16 base carrying `1` are indistinguishable as numbers but can never meet, because the former exists only in a major-0 container, refused at the epoch boundary before any version is compared. The `u32` never has to carry provenance because the container does. **S27 now additionally owes three inherited items** (both interim refusals converted to validation, M8's deferred laundering demonstration, pin 3c's two suspended conformance assertions), recorded in its contract as required tests | -| P13-S28 | **No container property distinguishes a document produced under a validated reduction authority from one produced before any authority existed — and the two candidates that look like they would, cannot.** P13-S27 installs an authority and validates it at read and write time, but cannot state what to do with a canonical base that predates the authority: a raw `ReductionAlgorithmVersion` is a bare `u32` (`bundle/src/ids.rs:291`) carrying no provenance, and the text-projection parser accepts an unbounded one from a document (`textproj/src/parse.rs:591`), so no numeric convention — including a deliberately high epoch — is safe from a hand-authored or third-party document declaring it. **`FORMAT_MINOR` does not work either, for two independent reasons:** the header *"never changes after the file is created"* (`core_spec.tex:10799`–`:10800`) and `commit_versioned` publishes only a superblock (`bundle.rs:791`), so a legacy bundle that commits a base S27 just validated keeps its old minor **permanently** — rejecting minor-≤1 bases would then reject a base the authority itself accepted, and accepting them leaves S16's `1` ambiguous; and `core_spec.tex:12258`–`:12262` limits a minor change to appending append-safe discriminants and calls it backward-compatible, whereas making a previously-valid base newly rejectable is a **semantic acceptance change**, with current readers ignoring minor entirely (`header.rs:119` gates on major alone) so the boundary would bind only readers that already comply. **The requirement that survives:** provenance MUST ride a container property that **old readers cannot silently accept** and that **a later commit cannot inherit unchanged** | `spec/CONTRACT_P13S27_REDUCTION_AUTHORITY.md` pin 2a (filed 2026-07-31; the disposition S27 cannot make from inside itself) | **open. The critical path — P13-S27 and P13-S16 are both blocked on it.** **This rung must own all five, and none may be deferred into S27:** (1) an **old-reader rejection boundary** — pre-boundary readers must fail closed rather than silently open a document whose safety check they do not run; (2) **provenance that survives commits correctly**, i.e. is not inherited unchanged by a later generation and is not lost by one; (3) **legacy-base rebuild/repack behaviour**, stated for real artifacts rather than assumed away; (4) **every writer path, including text projection** — `serialize_document`, `project.rs`, and the committed `.txt` vectors, since a text document can declare any version; (5) **the exact format-version and compatibility consequences**, most plausibly a **major**-version boundary or a generation-scoped attestation paired with an incompatibility boundary. **Not a sub-pin of S27 and must not drift into it** — S27's pin 2a carries an explicit prohibition against being amended into a disposition without its own ratification round. **Scoped and RATIFIED 2026-07-31 as `spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md`** after four adversarial review rounds — 11 pins, 11 tests, 11 mutations, 15 touch rows, 7 gate items. **This row is now a dependency record only; the work lives there and P13-S28 does not execute as a Pass 13 rung.** Rulings taken: the carrier is the **format major** (`FORMAT_MAJOR` 0 → 1, `FORMAT_MINOR` 1 → 0), decoded three ways through a named `FormatEpoch` rather than a bool, with **no** generation-scoped attestation in this epoch; legacy resolves to **hard rejection, not read-only**; and an eight-row epoch matrix in which a major-0 bundle with no base may open, one carrying a base is rejected, and one attempting to *add* a base is rejected and told to repack — **the non-inheritance rule that `FORMAT_MINOR` could not express**. All five things this row required the rung to own are pinned: old-reader boundary (pin 2), commit-surviving provenance (pin 3), legacy repack (pins 4, 5), every writer path including text projection (pins 3b, 6), and the exact format/compatibility consequences (pins 1, 7). **Three findings from the review rounds that changed the rung's shape**, none of them visible at filing: (1) **it cannot stamp major 1 before S27's writer enforcement exists**, so pin 3a temporarily refuses *both* boundaries — opening a major-1 bundle already carrying a base, and committing one into it — through a third, temporary `ReductionAuthorityUnavailable` error that must name P13-S27 and must **not** name repack; (2) **text projection launders provenance straight through the boundary** (`serialize_document` stages a carried base into a fresh bundle and `build_manifest` writes it), resolved as **symmetric document-level refusal** — projection, parsing and a new dedicated `SerializeError` variant, none of which existed to be "retained" — which forces `COMPANION_VERSION` 0.13.0 → **0.14.0** and rebuilds the committed corpus to **20 vectors, ten rejection classes, `canonical_bases` reach 2 → 0**, a real and stated capability loss; (3) **corruption precedence binds in both epochs** — a corrupt major-1 base must still fail as malformed, never as the *temporary* authority error a user would reasonably retry. **IMPLEMENTED 2026-08-07** — amended once before dispatch (pin 3c, touch rows 10/11, gate 8) after reconnaissance found pin 3a's refusals reaching a conformance criterion through a file the touch table did not carry. All 11 tests landed under their contract names, all 11 mutations run and observed, workspace green at 1569. **P13-S27 is unblocked and P13-S16 remains blocked on S27** — pin 8 resolved S27's open pin 2a (legacy bases are refused by container epoch, never by version arithmetic), and S27 additionally inherits three obligations recorded in its own contract: converting **both** interim refusals to validation, M8's deferred laundering demonstration, and pin 3c's two suspended conformance assertions. **Two touch-table gaps found during execution, both of the same shape** — a `.tex` requirement addition moves hardcoded counts in `testkit/tests/requirement_labels.rs`, and a companion-version bump moves a second normative version literal spelled `version~0.13.0` rather than `(0 13 0)`; neither file was in any touch table, and the second was caught only because `requirements_name_only_this_companion_version` exists | +| P13-S28 | **No container property distinguishes a document produced under a validated reduction authority from one produced before any authority existed — and the two candidates that look like they would, cannot.** P13-S27 installs an authority and validates it at read and write time, but cannot state what to do with a canonical base that predates the authority: a raw `ReductionAlgorithmVersion` is a bare `u32` (`bundle/src/ids.rs:291`) carrying no provenance, and the text-projection parser accepts an unbounded one from a document (`textproj/src/parse.rs:591`), so no numeric convention — including a deliberately high epoch — is safe from a hand-authored or third-party document declaring it. **`FORMAT_MINOR` does not work either, for two independent reasons:** the header *"never changes after the file is created"* (`core_spec.tex:10799`–`:10800`) and `commit_versioned` publishes only a superblock (`bundle.rs:791`), so a legacy bundle that commits a base S27 just validated keeps its old minor **permanently** — rejecting minor-≤1 bases would then reject a base the authority itself accepted, and accepting them leaves S16's `1` ambiguous; and `core_spec.tex:12258`–`:12262` limits a minor change to appending append-safe discriminants and calls it backward-compatible, whereas making a previously-valid base newly rejectable is a **semantic acceptance change**, with current readers ignoring minor entirely (`header.rs:119` gates on major alone) so the boundary would bind only readers that already comply. **The requirement that survives:** provenance MUST ride a container property that **old readers cannot silently accept** and that **a later commit cannot inherit unchanged** | `spec/CONTRACT_P13S27_REDUCTION_AUTHORITY.md` pin 2a (filed 2026-07-31; the disposition S27 cannot make from inside itself) | **open. The critical path — P13-S27 and P13-S16 are both blocked on it.** **This rung must own all five, and none may be deferred into S27:** (1) an **old-reader rejection boundary** — pre-boundary readers must fail closed rather than silently open a document whose safety check they do not run; (2) **provenance that survives commits correctly**, i.e. is not inherited unchanged by a later generation and is not lost by one; (3) **legacy-base rebuild/repack behaviour**, stated for real artifacts rather than assumed away; (4) **every writer path, including text projection** — `serialize_document`, `project.rs`, and the committed `.txt` vectors, since a text document can declare any version; (5) **the exact format-version and compatibility consequences**, most plausibly a **major**-version boundary or a generation-scoped attestation paired with an incompatibility boundary. **Not a sub-pin of S27 and must not drift into it** — S27's pin 2a carries an explicit prohibition against being amended into a disposition without its own ratification round. **Scoped and RATIFIED 2026-07-31 as `spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md`** after four adversarial review rounds — 11 pins, 11 tests, 11 mutations, 15 touch rows, 7 gate items. **This row is now a dependency record only; the work lives there and P13-S28 does not execute as a Pass 13 rung.** Rulings taken: the carrier is the **format major** (`FORMAT_MAJOR` 0 → 1, `FORMAT_MINOR` 1 → 0), decoded three ways through a named `FormatEpoch` rather than a bool, with **no** generation-scoped attestation in this epoch; legacy resolves to **hard rejection, not read-only**; and an eight-row epoch matrix in which a major-0 bundle with no base may open, one carrying a base is rejected, and one attempting to *add* a base is rejected and told to repack — **the non-inheritance rule that `FORMAT_MINOR` could not express**. All five things this row required the rung to own are pinned: old-reader boundary (pin 2), commit-surviving provenance (pin 3), legacy repack (pins 4, 5), every writer path including text projection (pins 3b, 6), and the exact format/compatibility consequences (pins 1, 7). **Three findings from the review rounds that changed the rung's shape**, none of them visible at filing: (1) **it cannot stamp major 1 before S27's writer enforcement exists**, so pin 3a temporarily refuses *both* boundaries — opening a major-1 bundle already carrying a base, and committing one into it — through a third, temporary `ReductionAuthorityUnavailable` error that must name P13-S27 and must **not** name repack; (2) **text projection launders provenance straight through the boundary** (`serialize_document` stages a carried base into a fresh bundle and `build_manifest` writes it), resolved as **symmetric document-level refusal** — projection, parsing and a new dedicated `SerializeError` variant, none of which existed to be "retained" — which forces `COMPANION_VERSION` 0.13.0 → **0.14.0** and rebuilds the committed corpus to **20 vectors, ten rejection classes, `canonical_bases` reach 2 → 0**, a real and stated capability loss; (3) **corruption precedence binds in both epochs** — a corrupt major-1 base must still fail as malformed, never as the *temporary* authority error a user would reasonably retry. **IMPLEMENTED 2026-08-07** — amended once before dispatch (pin 3c, touch rows 10/11, gate 8) after reconnaissance found pin 3a's refusals reaching a conformance criterion through a file the touch table did not carry. All 11 tests landed under their contract names, all 11 mutations run and observed, workspace green at 1569. **P13-S27 is unblocked and P13-S16 remains blocked on S27** — pin 8 resolved S27's open pin 2a (legacy bases are refused by container epoch, never by version arithmetic), and S27 additionally inherits three obligations recorded in its own contract: converting **both** interim refusals to validation, M8's deferred laundering demonstration, and pin 3c's two suspended conformance assertions. **Two touch-table gaps found during execution, both of the same shape** — a `.tex` requirement addition moves hardcoded counts in `testkit/tests/requirement_labels.rs`, and a companion-version bump moves a second normative version literal spelled `version~0.13.0` rather than `(0 13 0)`; neither file was in any touch table, and the second was caught only because `requirements_name_only_this_companion_version` exists. **A third gap was caught in review, after the rung was committed:** pin 3b's projection refusal had been implemented only on the **bundle** side (`document_from_bundle`), leaving the public `project_text_document` free to emit a `(canonical-base ...)` line for a directly constructed `TextDocument` — text the parser then rejects. A projector that can produce what the parser refuses is exactly the asymmetry pin 3b exists to close, and the refusal is unreachable through a `Bundle` during the interval anyway, so the *only* reachable half was the unguarded one. The public projector now returns `Result` and refuses; a crate-private `render_text_document` retains the base spelling for the one legitimate caller, the `canonical_base_present` negative vector. **The lesson is the rung's own recurring one:** a guard placed on the path that happened to be named, rather than on every path a caller can reach |