From 8bb917d7c3896b33ef00db775c2076d90d95c136 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Fri, 31 Jul 2026 21:46:56 -0400 Subject: [PATCH] Format epoch ratified: container major 1 owns the provenance boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P13-S28 asked for a container property old readers cannot silently accept and a later commit cannot inherit unchanged. The format major is that property. FORMAT_MAJOR moves 0 -> 1, FORMAT_MINOR resets to 0, and the decoder stops being exact-major-only: it classifies three ways through a named FormatEpoch, deliberately decoding major 0 as legacy. Old readers already fail closed on an unknown major, so that half needs no new mechanism — which is why the major is the right carrier and the header's immutability, fatal to FORMAT_MINOR as a provenance field, is exactly what makes it sound as an epoch field. The eight-row matrix carries the rule: 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. That last row is the non-inheritance rule. Legacy resolves to hard rejection, never read-only — a pre-authority base is not a restricted-but-correct view. Three things the review rounds found, none visible at filing: It cannot stamp major 1 before S27's writer enforcement exists. Pin 3a therefore closes both boundaries temporarily — opening a major-1 bundle already carrying a base, and committing one into it — through a third, temporary error that names P13-S27 and must not name repack, since a major-1 container is already the right epoch. 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 the three existed to be "retained"; an earlier draft claimed otherwise and was wrong. This forces COMPANION_VERSION to 0.14.0 and rebuilds the committed corpus to 20 vectors and ten rejection classes, with canonical_bases reach dropping 2 -> 0. That is a real capability loss and is stated as one. 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 on a container that is in fact tampered with. 11 pins, 11 tests, 11 mutations, 15 touch rows, 7 gate items. S27's contract is a mandatory touch: pin 8 resolves its open pin 2a — legacy bases are refused by container epoch, never by version arithmetic. Documentation only. Not implemented, not dispatched. S27 and S16 stay blocked until this rung lands. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV --- spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md | 660 +++++++++++++++++++++++++++ spec/PASS13_CANDIDATES.md | 2 +- 2 files changed, 661 insertions(+), 1 deletion(-) create mode 100644 spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md diff --git a/spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md b/spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md new file mode 100644 index 0000000..b94229c --- /dev/null +++ b/spec/CONTRACT_FORMAT_EPOCH_MAJOR1.md @@ -0,0 +1,660 @@ +# Contract — Format epoch: container major 1 + +**Status:** **RATIFIED 2026-07-31**, after four adversarial review rounds. Not +yet implemented; not yet dispatched. **The pins below are frozen** — they may +not be edited, only executed. A defect found during execution is reported, not +patched in place. + +**Track:** format epoch. **Not a Pass 13 rung.** `P13-S28` is its dependency +record in `spec/PASS13_CANDIDATES.md` and points here; this contract is where +the work lives. It is sequenced ahead of **P13-S27**, which is ahead of +**P13-S16**. + +**Rung type:** **container format-major boundary.** A new reader must +deliberately decode major 0 as legacy; old readers reject major 1 by the +mechanism that already exists. + +**Rulings taken 2026-07-31, not re-opened here:** + +1. **The carrier is the format major.** No generation-scoped attestation in this + epoch — a superblock attestation cannot make pre-boundary readers fail + closed, and once a major-1 container *is* the boundary, every writer able to + open one is necessarily epoch-aware, so P13-S27's capabilities already + validate every base replacement. The extra field would add wire and + re-derivation complexity while solving no additional case. +2. **Legacy resolves to hard rejection, not read-only.** A pre-authority base is + not safe materialized state. +3. **Its own track**, per above. + +--- + +## §0. What was verified before drafting + +Read out of the working tree at `818a16f`. + +### 0.1 The boundary has the right polarity already, in one direction only + +`FixedHeader::decode` (`header.rs:119`) rejects on `format_major != FORMAT_MAJOR` +with `BundleError::UnsupportedFormatVersion`. `FORMAT_MAJOR = 0`, +`FORMAT_MINOR = 1` (`:39`, `:42`). + +So **old readers already fail closed on a major they do not know** — that half +needs no new mechanism and is why the major is the correct carrier. The half +that must be built is the other one: today's decoder is *exact-major-only*, so a +new reader would reject major 0 outright. It must instead decode major 0 +**deliberately, as legacy**. + +### 0.2 The surface is unusually small + +`FORMAT_MAJOR` appears in exactly four places workspace-wide: `header.rs:39` +(definition), `:68` (stamped into every new header), `:119` (the accept check), +and `lib.rs:84` (re-export). There is no second parallel table. + +### 0.3 No committed byte artifact pins a header — verified, not assumed + +`spec/vectors/decode_vectors.txt` covers `bundle.block`, `bundle.manifest` and +`bundle.operation_index`; it has **no header or superblock surface**. The only +other committed byte artifact is `spec/vectors/textproj_document_vectors.txt`, +which is text-projection documents. A repository-wide search for `*.bin`, +`*.bundle`, `*.epi*` outside `target/` and the out-of-bounds trees finds +**nothing**. + +**Consequence, and its limit.** No *binary* frozen file pins a header, so the +major bump itself regenerates nothing there, and "repack" describes what a +*user's* bundle needs rather than an in-tree artifact. + +**That conclusion does NOT extend to the text companion, and an earlier draft of +this contract wrongly generalized it into a blanket "no vector file may +change" gate.** `spec/vectors/textproj_document_vectors.txt` carries +base-bearing documents (`textproj/src/vectors.rs:353`, `:363`), and pin 3b makes +projecting, parsing and serializing such a document an error. + +**That file WILL change, and how it changes is specified — not left as an +unknown deliverable.** A second earlier draft downgraded the question to +"report whether it moved"; that is not ratifiable either. The corpus was +decoded (it is hex-encoded; see pin 3b) and the exact required edits are pinned +in pin 3b and touch row 5c. + +### 0.4 The header cannot carry per-generation state, which is why this works + +`core_spec.tex:10799`–`:10800`: *"The header never changes after the file is +created."* `commit_versioned` (`bundle.rs:791`) publishes only a superblock. +That immutability is exactly what disqualified `FORMAT_MINOR` as a *provenance* +carrier (P13-S27 pin 2a, rejected option iv) and is exactly what makes the major +sound as an *epoch* carrier: an epoch is a property of the file's creation, and +must not drift with later commits. + +--- + +## §1. Pins + +**Pin 1 — `FORMAT_MAJOR` becomes 1; `FORMAT_MINOR` resets to 0.** +A new major restarts minor numbering. Both constants get doc comments naming +this contract and stating what the epoch means: *a major-1 container is one +whose every base-bearing commit was validated against a supplied reduction +authority.* + +**Pin 2 — the decoder becomes deliberately legacy-aware.** +`header.rs:119`'s exact-major test is replaced by an explicit three-way +classification, and the retained major must be carried on `FixedHeader` so +callers can act on it: + +- `0` → **legacy**, decoded and marked as such; +- `1` → **current**; +- anything else → `UnsupportedFormatVersion`, unchanged. + +**A boolean is not sufficient** — introduce a named enum (e.g. `FormatEpoch`) +so the legacy case is a value the type system carries, not a comparison +re-derived at each use site. Every consumer of the epoch reads that value. + +**Pin 3 — the epoch matrix is normative and is implemented exactly.** + +| # | Container | Canonical base | Behaviour | +|---|---|---|---| +| 1 | major 0 | none | **may open** — nothing unverifiable is exposed | +| 2 | major 0 | present | **hard reject** at `open` | +| 3 | major 0 | commit attempts to add or replace one | **reject**; require repack into a fresh major-1 bundle | +| 4 | major 1 | none | **may open**; may commit non-base-bearing history | +| 5i | major 1 | present at `open` | **INTERIM (this rung → P13-S27): refuse**, third error | +| 5 | major 1 | present at `open` | *post-S27:* opens; base validated against the supplied current authority | +| 6i | major 1 | commit attempts to add or replace one | **INTERIM (this rung → P13-S27): refuse**, third error | +| 6 | major 1 | commit attempts to add or replace one | *post-S27:* validates against the supplied current authority | + +**Rows 5i and 6i are what this rung actually implements**; rows 5 and 6 are what +P13-S27 replaces them with. An earlier draft's matrix stated only the post-S27 +behaviour while pin 3a implemented the interim one — the contract described a +container this rung does not build. Both states are now written, and the +interim rows are the normative ones for this rung's tests. + +Row 3 is the one that would be missed: a legacy bundle that opens cleanly under +row 1 must not become base-bearing in place, because its header can never say it +was validated. **This is the rule that makes the epoch non-inheritable**, and it +is why the boundary works where `FORMAT_MINOR` did not. + +**Corruption precedence is preserved, and it is not automatic. It binds in BOTH +epochs.** A container whose base's version disagrees with its superblock's is +**corrupt**, and MUST still fail with the existing malformed-bundle `DecodeError` +(`bundle.rs:396`–`:401`) **before any of pin 4's three epoch errors is +considered** — row 2's legacy error in a major-0 container, and equally row 5i's +`ReductionAuthorityUnavailable` in a major-1 one. + +**The major-1 half is the one an earlier draft missed.** It pinned precedence +only for legacy containers, so a corrupt major-1 base could be reported as +"authority unavailable" — a *temporary* condition a user would reasonably retry +after P13-S27 — if the new epoch check were placed ahead of the existing +malformed check at `bundle.rs:396`. Test 11 cannot catch this: it uses a +**self-consistent** base by construction, so the malformed branch never runs in +it. Ordering the new checks first would collapse tampering into staleness and +**silently erase the very distinction P13-S27 rests on** (its §0.1 and pin 6). +Test 7 pins the order in both epochs. + +**Pin 3a — the epoch may not assert what is not yet enforced. SEQUENCING.** +This contract's first draft stamped major 1 while §6 forbade implementing +P13-S27's capability — so between this rung and S27, `Bundle::create` would mint +major-1 containers and `commit_versioned` would still copy any carried base +version unchecked. **That is precisely the false provenance the epoch exists to +exclude**, minted by the mechanism meant to prevent it, and test 4 could not +truthfully claim a major-1 bundle "validates its base." + +**A write-side refusal alone is insufficient, and an earlier draft of this pin +stopped there.** `open` (`bundle.rs:393`ff) accepts a matching base/superblock +pair, and S28 adds no authority capability — so a major-1 bundle that *already* +carries a self-consistent base would still open during the interval, its epoch +asserting a validation that never ran. + +**Resolution: until P13-S27 lands, BOTH boundaries are temporarily closed:** + +- **open** a major-1 container that already carries a canonical base → refuse; +- **commit** a base into a major-1 container → refuse. + +**This needs a THIRD error, distinct from both legacy errors** — carried as a +first-class member of pin 4's inventory, not as a footnote to this pin. A +major-1 container does not need repacking — it is the right epoch; what is +missing is S27's authority check. Name the condition for what it is +(`ReductionAuthorityUnavailable`), and assert in tests that it is **neither** +legacy error. Reusing a repack error would tell a user to repack a container +that is already correct. + +Both branches are **temporary and must be marked as such in code**, naming +P13-S27 as what replaces them. S27 converts both into capability validation — +not one of them. + +*(The alternative — co-landing this rung with S27's capability and every +write-path validation — was considered and not taken: it merges two large rungs +and loses the separate ratification each has already had. If you prefer it, this +pin is where it changes.)* + +**Pin 3b — text projection cannot mint a canonical base. TEXT RULE.** +`TextDocument` (`textproj/src/lib.rs:74`ff) has **no container-major or epoch +field**, and the projector deliberately drops physical layout +(`project.rs:29`ff, `req:textproj:derive-or-carry`). Meanwhile `parse.rs:591` +accepts an unbounded `u32` reduction version and `serialize_document` +(`serialize.rs:119`) creates a **fresh** bundle while `build_manifest` (`:216`) +copies the carried base verbatim. So an old or hand-authored text document can +be serialized into a brand-new major-1 container, and once its raw version +matches the current authority nothing downstream can tell it from a validated +base. **Text import is a laundering path straight through the boundary.** + +**Ruled: symmetric document-level refusal.** A one-sided serialize refusal would +leave the companion incoherent — `project_bundle` emits canonical-base text +(`project.rs:479`, `:537`) and `parse` accepts it, so text carrying a base could +be produced and read but never serialized, while `req:textproj:roundtrip` +(`text_projection.tex:903`ff) quantifies over **every** bundle and every valid +text. All three sides move together: + +- **projection** of a base-bearing bundle → error; +- **parsing** of base-bearing text → error; +- **serialization** → a **new, dedicated `SerializeError` variant**, as defence + for a directly constructed `TextDocument`. + +**All three are additions. Serialization has no refusal to "retain" today** — +an earlier draft said it did, and that was simply false: `serialize_document` +(`serialize.rs:119`) stages the carried base as a `Snapshot` chunk +(`serialize.rs:131`–`:141`) and `build_manifest` (`:216`) writes it into the new +manifest. Its documented error set is `NonEmptyBlobs` and `Bundle` +(`serialize.rs:116`–`:118`); neither covers this. The refusal must be **built**, +and it must be its own variant rather than a `SerializeError::Bundle` +passthrough — see M8 for why that distinction is load-bearing. + +**Scope of that rule — text only.** *Text projection* may introduce a canonical +base only through an explicit rebuild/repack flow (pin 5). It does **not** say a +canonical base may only ever be created that way: matrix rows 5/6 admit +validated base introduction in major-1 containers once P13-S27 lands, and +ordinary snapshot producers remain governed by S27, not by this pin. An earlier +draft stated the rule unscoped, which contradicted both. + +`req:textproj:roundtrip` MUST be amended to state the exclusion in its own terms +— **"round trips excepted" is not sufficient**: the requirement quantifies +universally and must say what is now outside its domain and why. + +**Why not the alternative** — carrying provenance through the text companion, +with absent/old classified legacy: `manifest_schema_version` is the existing +precedent for a carried-verbatim field, and its own doc concedes *"the document +author is responsible for updating it."* A text format cannot carry unforgeable +provenance; any field it defines can be typed by hand. Carrying a provenance +marker would therefore reduce to trusting the author, which is exactly what the +epoch was built not to do — it would relocate the laundering one level up rather +than close it. Refusal is the only rule the medium can actually enforce. + +**This is a real capability loss and must be stated, not softened:** base-bearing +documents stop round-tripping through text until a repack flow exists. + +**`COMPANION_VERSION` MUST bump 0.13.0 → 0.14.0.** Not "determine whether it +moves" — an earlier draft left this open and it is not a ratifiable instruction. +Refusing a document the companion previously serialized is a semantic change, +and `parse_header` (`parse.rs:397`) rejects every version but the exact +`COMPANION_VERSION` (`lib.rs:59`), so the bump is load-bearing rather than +cosmetic. + +**Consequence, verified by decoding the corpus** (it is hex-encoded, so a +plaintext grep proves nothing — an earlier draft's grep returned zero and proved +nothing at all): `spec/vectors/textproj_document_vectors.txt` holds **19 vectors +— 10 `accept` and 9 `reject`.** Eighteen carry header `(0 13 0)`; one carries +`(0 12 0)`. Six carry `canonical-base`. So `lib.rs`, `parse.rs`, and the corpus +file are **mandatory** touch rows. + +**The header bump reaches 18 rows, not 10.** All 10 accepts move `(0 13 0)` → +`(0 14 0)`. So do the **8 rejection vectors that also carry `(0 13 0)`** +(`unreferenced_blob`, `canonical_base_before_extension`, `lineage_repeated`, +`envelopes_reversed`, `profiles_reversed`, `extensions_reversed`, +`extension_chunks_reversed`, `final_lf_missing`). If they are left at `(0 13 0)` +they still reject — **at the header, not at the predicate each was written to +exercise.** They would pass their declared verdict while testing nothing, which +is precisely the silently-degrading corpus this rung must not create. + +**`superseded_companion_version` moves `(0 12 0)` → `(0 13 0)`.** Its purpose +(`vectors.rs:559`–`:565`) is to reject *the immediately superseded companion*; +after the bump that is 0.13.0. Leaving it at 0.12.0 would make it assert the +rejection of a two-generation-old version and stop exercising the deferred +migrate-on-read posture it was written for. + +**The corpus takes ONE complete shape, specified here in full.** An earlier +draft left `rich_document` as "reject *or* re-derived" and the new class count +as *n*; neither is executable, and the choice is not free — **the only two +accepted documents carrying extensions are the two base-bearing ones** +(`extension_base_multi` at `vectors.rs:344`, `rich` at `:357`). Converting one to +`reject` and freeing the other drops `extensions` and `multi_envelope` reach +from 2 to 1; converting both drops them to 0. The disposition therefore decides +coverage, not just row count. + +**Ruled shape: 20 vectors — 10 accepts, 10 rejects, ten rejection classes.** + +*Accepts (10, all base-free):* + +| Vector | Change | +|---|---| +| `extension_base_two_envelopes` — the `extension_base_multi` document (`:344`, exported `:461`) | **base removed**; extensions, two envelopes, non-baseline schema version all retained. **Its exported name must stop claiming a base it no longer carries** — and the rename reaches `by_name` at `:551` | +| `rich_document` — the `rich` document (`:357`, exported `:464`) | **base removed**; two extensions, lineage, custom profiles, envelopes all retained | +| the other 8 | header only | + +*Rejects (10):* + +| Vector | Change | +|---|---| +| `superseded_companion_version` | `(0 12 0)` → `(0 13 0)`, per above | +| `canonical_base_before_extension` | **re-expressed with a non-base section pair.** The order is `header document lineage? profile* extension* canonical-base? blob* envelope*` (`parse.rs:45`), so a lineage/profile or profile/extension inversion reaches `out-of-order-sections` without a base | +| `envelopes_reversed`, `extensions_reversed`, `extension_chunks_reversed` | **nothing beyond the header** — they are *derived* from the two accepts above (`:598`, `:614`, `:630`, `:633`), so freeing those accepts frees these automatically. Confirm it rather than assume it | +| `unreferenced_blob`, `lineage_repeated`, `profiles_reversed`, `final_lf_missing` | header only | +| **NEW: `canonical_base_present`** | class **`canonical-base-unsupported`** — a base-bearing text, refused by pin 3b's parse side. Build it from the *pre-change* base-bearing spelling, so the corpus keeps a base-bearing text as a **negative** rather than losing the spelling entirely | + +*Why this shape rather than converting the two accepts to rejects:* it preserves +`extensions: 2` and `multi_envelope: 2` exactly, confines the reach loss to the +one capability pin 3b actually removes, and makes the new class carry a purpose- +built vector instead of a demoted accept that also happened to test three other +things. + +**`expected_reach()` — every count, stated:** + +| Field | Before | After | +|---|---|---| +| `extensions` | 2 | **2** | +| `canonical_bases` | 2 | **0** | +| `custom_profiles` | 2 | **2** | +| `lineages` | 2 | **2** | +| `multi_envelope` | 2 | **2** | +| `reject_classes` | nine classes × 1 | **ten** classes × 1 — the nine existing plus `canonical-base-unsupported` | + +`canonical_bases: 0` is a **real reach loss** and its doc comment +(`vectors.rs:124`–`:126`) must record the cause — canonical bases are no longer +reachable through text at all — and the "nine distinct rejection classes" +wording moves to ten. Silently lowering a non-vacuity count without recording +why converts a stated capability loss into an unexplained weakened assertion. + +**Three further count sites move with it, each verified present:** + +- `vectors.rs:889` asserts the corpus has exactly **19** rows (*"the corpus has + unexpectedly thinned"*) → **20**; +- `t12_g3b_kinds_round_trip_and_companion_is_0_13_0_rejecting_0_12_0` + (`vectors.rs:969`, asserting `COMPANION_VERSION == (0, 13, 0)` at `:971`) — + both its body and **its name** move to 0.14.0/0.13.0; +- `parse.rs:658`'s test `HEADER` constant, and `text_projection.tex:486` and + `:1146`, which spell `(0 13 0)` literally. + +The four base-bearing reject rows were the dangerous ones: they would **still +reject** after pin 3b while their declared class is *informative only* +(`vectors.rs:67`), so `reject_classes` would keep counting them long after the +predicate each names went untested. + +**Pin 4 — THREE distinct errors, and none is read-only.** +An earlier draft specified two, then pin 3a introduced a third without amending +this pin — leaving the error inventory, gate 6 and M11 all describing a +two-error design. The full set: + +| Error | Raised by | Lifetime | Message | +|---|---|---|---| +| **legacy-base** | matrix row 2 | permanent | names **repack** | +| **legacy-base-introduction** | matrix row 3 | permanent | names **repack** | +| **authority-unavailable** (`ReductionAuthorityUnavailable`) | matrix rows 5i, 6i | **temporary — P13-S27 removes it** | names **P13-S27**; **MUST NOT mention repack** | + +- row 2 → the document already contains unverifiable canonical state; +- row 3 → the document is fine, but the operation requested cannot be performed + in this container; +- rows 5i/6i → the container is the **right** epoch; the reader has not yet been + given the authority to validate its base. Repacking would be wrong advice, so + the message must not offer it. + +All three are `BundleError` variants (that type has no discriminant and no +encoder — verified — so this is a pure API change). **None degrades to +read-only:** a pre-authority base is not a restricted-but-correct view, and +exposing it read-only would serve unverifiable canonical state confidently. + +The three must be **mutually distinguishable in tests**, not merely distinct in +source: every test that expects one asserts the other two are not produced. + +**Pin 5 — repack is named, not built.** +This rung provides no repack implementation. It MUST leave the door open for one +and MUST NOT foreclose it: a higher-level, **explicitly non-materializing** +recovery/repack flow may later offer rebuilding when complete history is +available. **That flow is not a read-only `Bundle::open` mode**, and nothing in +this rung may introduce one. + +Record this in the errors' doc comments so a later reader does not "helpfully" +add the read-only path. + +**Pin 6 — every writer path stamps and is checked, including text projection.** +`Bundle::create` stamps major 1. **The production writer that reaches a +canonical base is one, not two:** `textproj::serialize_document` +(`serialize.rs:119`, via `build_manifest` `:212`). **`project.rs:936` is inside +`#[cfg(test)]`** (the module opens at `project.rs:560`) and is a fixture writer; +an earlier draft of this pin listed it as production, taken from a census +without checking its enclosing module. It stays in the test surface. The committed `.txt` document vectors can declare any +`reduction_algorithm_version` (`parse.rs:591` parses an unbounded `u32`), so +**text projection is a writer path in the full sense** and is enumerated here +rather than left to be discovered, as P13-S27's first draft did. + +**Pin 7 — specification updates.** +`binary_format.tex:1808`'s header table states `format_major` is `0` and must +carry the epoch and its meaning. `core_spec.tex`'s Fixed Header subsection +(`:10796`ff) gains the legacy-decode rule and the epoch matrix (**rows 1–4 and +the post-S27 rows 5/6 only — the interim rows 5i/6i are implementation state, +not normative wire semantics, and must not be written into the spec**); the +major-version semantics near `:12467` gain what a major boundary now *means* +beyond wire layout. Revision History rows and version bumps in both. + +**Pin 8 — P13-S27's precondition is stated where S27 can rely on it.** +Reduction-version authority is meaningful **only in major-1 containers**. Record +that here and in S27's contract, so S27's pin 2a resolves to: *legacy bases are +refused by container epoch, never by version arithmetic.* + +**`spec/CONTRACT_P13S27_REDUCTION_AUTHORITY.md` is therefore an edited file of +this rung**, and touch row 9 carries it. Two things land there, not one: + +1. this pin's major-1 precondition, resolving S27's open pin 2a; +2. **M8's deferred laundering demonstration**, which S27 inherits as owed work. + +It is still a **DRAFT** and so may be edited; it is not among the ratified +contracts this session may not touch. An earlier draft left it off the touch +table while two pins required writing to it — and since §6 stages **only** the +touch table by explicit path, that omission would have silently dropped both. + +**Pin 9 — the ledger.** +`spec/PASS13_CANDIDATES.md`: P13-S28's row points here and records the ruling +set. **P13-S28 does not execute as a Pass 13 rung.** S27 and S16 stay blocked +until this contract lands. + +--- + +## §2. Touch table + +| # | File | Change | +|---|---|---| +| 1 | `crates/epiphany-bundle/src/header.rs` | pins 1, 2 | +| 2 | `crates/epiphany-bundle/src/bundle.rs` | pin 3 (open + commit paths) | +| 3 | `crates/epiphany-bundle/src/error.rs` | pins 4, 5 | +| 4 | `crates/epiphany-bundle/src/lib.rs` | re-exports | +| 5 | `crates/epiphany-textproj/src/serialize.rs` | pins 6, 3b — the refusal, and the round-trip laws that must now except base-bearing documents | +| 5b | `crates/epiphany-textproj/src/project.rs` | pin 6 | +| 5c | `crates/epiphany-textproj/src/vectors.rs` **and** `spec/vectors/textproj_document_vectors.txt` | **mandatory**, pin 3b's ruled corpus shape: 18 rows carrying `(0 13 0)` → `(0 14 0)`; `superseded_companion_version` `(0 12 0)` → `(0 13 0)`; both base-bearing accepts re-derived base-free; `canonical_base_before_extension` re-expressed on a non-base pair; **new** `canonical_base_present` reject; corpus 19 → **20** (`:889`); `expected_reach()` `canonical_bases` 2 → 0 with cause, `reject_classes` nine → ten; `t12_…_0_13_0_rejecting_0_12_0` renamed and rebased (`:969`) | +| 5d | `crates/epiphany-textproj/src/lib.rs` | `COMPANION_VERSION` 0.13.0 → **0.14.0** (`:59`) — **mandatory**, not conditional | +| 5e | `crates/epiphany-textproj/src/parse.rs` | pin 3b's parse-side refusal; `parse_header` (`:397`) accepting only the new version; the test `HEADER` constant at `:658` | +| 5f | *(same file as row 5)* `SerializeError` at `serialize.rs:66` | pin 3b's **new dedicated variant** — an addition, not a retained refusal; the doc comment at `:116`–`:118` enumerates the error set and must gain it | +| 5g | `spec/text_projection.tex` (+ `.pdf`) | `req:textproj:roundtrip` (`:903`ff) amended to state the base-bearing exclusion in its own terms; companion version (`:486`, `:1146` spell `(0 13 0)` literally) and Revision History rows | +| 6 | `spec/binary_format.tex` (+ `.pdf`) | pin 7 | +| 7 | `spec/core_spec.tex` (+ `.pdf`) | pin 7 | +| 8 | `spec/PASS13_CANDIDATES.md` | pin 9 | +| 9 | `spec/CONTRACT_P13S27_REDUCTION_AUTHORITY.md` | **mandatory** — pin 8's major-1 precondition (resolving S27's open pin 2a) **and** M8's deferred laundering demonstration. Still a DRAFT, so editable; **not** one of the ratified contracts that may not be touched | + +**`spec/vectors/decode_vectors.txt` is NOT touched** (§0.3). If a change appears +to require regenerating *that* file, stop and report — it would mean something +reads a header where §0.3 found nothing. +**`spec/vectors/textproj_document_vectors.txt` IS touched**, mandatorily, by +pin 3b's companion bump. + +--- + +## §3. Required tests + +1. **`a_legacy_major_0_bundle_without_a_base_opens`** +2. **`a_legacy_major_0_bundle_with_a_base_is_rejected`** — asserting the row-2 + error specifically. +3. **`adding_a_base_to_a_legacy_bundle_is_rejected_and_names_repack`** — the + row-3 error, distinct from row 2's, asserted by variant **and** message. +4. **`a_major_1_bundle_round_trips_and_refuses_to_introduce_a_base`** — renamed + from `…_validates_its_base`, which pin 3a forbids claiming until P13-S27 + lands. **The name must not promise validation this rung does not perform.** + It MUST assert matrix row 6i's **authority-unavailable** error specifically, + and that **neither legacy error** is produced. An earlier draft left this + test asserting only "some failure", which is what made M11 unfalsifiable. +5. **`an_unknown_major_is_still_unsupported_format_version`** — the third arm of + pin 2, which a two-way test would silently drop. +6. **`text_projection_serialize_produces_a_major_1_container`** — pin 6. +7. **`a_corrupt_base_fails_as_malformed_before_any_epoch_error`** — pin 3's + precedence rule, **covering both epochs in one test or two, but covering + both**. Construct a container whose base version disagrees with its + superblock's, once at major 0 and once at major 1, and assert the + **malformed** error each time — **not** row 2's legacy error, and **not** + row 5i's `ReductionAuthorityUnavailable`. Renamed from + `…_corrupt_legacy_base…`: the earlier name scoped the guarantee to legacy + containers, which is exactly the half that was missing. Without this, S28 + silently erases P13-S27's tamper/staleness distinction and every other test + still passes — **including test 11**, whose base is self-consistent by + construction. +8. **`serializing_a_text_document_with_a_canonical_base_is_refused`** — pin 3b, + asserted against a document built from the existing base-bearing fixture, and + asserting the **dedicated `SerializeError` variant specifically** — not merely + "an error", and **not** `SerializeError::Bundle`. M8 depends on that + distinction being asserted. +9. **`projecting_a_base_bearing_bundle_is_refused`** — pin 3b's projection side. +10. **`parsing_base_bearing_text_is_refused`** — pin 3b's parse side. Without 9 + and 10 both, the companion is left able to produce text it cannot consume. +11. **`opening_a_major_1_bundle_that_already_carries_a_base_is_refused`** — pin + 3a's **read-side** branch (matrix row 5i), the one an earlier draft omitted + entirely. Asserted to return the **authority-unavailable** error and to be + **neither** legacy error. + +Tests 2 and 3 must each assert the **other's** error is not produced; they are +the pair pin 4 exists to separate. Test 7 stands in the same relation to test 2. +Tests 4 and 11 stand in that relation to **both** legacy errors. + +--- + +## §4. Mutation plan + +Applied, **run**, output verbatim, restored **by hand-editing back**. + +**M1 — the legacy classification is real.** Restore the exact-major test; test 1 +must fail (a legacy bundle stops opening at all). + +**M2 — row 2 fires.** Remove the base check for legacy containers; test 2 fails. + +**M3 — row 3 is not row 2.** Make the commit path return row 2's error; test 3 +fails. Signs that the two situations are separately diagnosable. + +**M4 — the epoch is non-inheritable.** Permit a legacy container to gain a base; +test 3 fails. **This is the signing mutation of the whole rung** — it +reintroduces exactly the counterexample that killed `FORMAT_MINOR`, and the +contract's central claim is that a major boundary does not admit it. + +**M5 — the unknown-major arm survives.** Make the classifier treat any non-1 +major as legacy; test 5 fails. + +**M6 — the writer stamps the epoch.** Make `create` stamp major 0; test 6 fails. + +**M7 — corruption precedence holds in both epochs.** Reorder pin 3's checks so +the **epoch** classification runs before the malformed-base check at +`bundle.rs:396`; test 7 fails **on both its major-0 and major-1 halves**. Run it +as a single reorder — one placement decision governs both — but **report both +failures**, because a reorder that only moves the legacy branch would show one +failure and look like a pass of the other. Signs that the ordering is deliberate +rather than incidental to how the code happens to be written, and that a corrupt +major-1 base is never reported as the *temporary* +`ReductionAuthorityUnavailable` — which a user would reasonably retry after +P13-S27 lands, on a container that is in fact tampered with. + +**M8 — the text boundary is closed.** In `serialize_document`, **remove or +bypass the base-bearing early-return branch** that raises pin 3b's dedicated +`SerializeError` variant. **Test 8 must then fail by falling through to pin 3a's +interim bundle error** (`ReductionAuthorityUnavailable`, surfacing as +`SerializeError::Bundle`) instead of the dedicated variant. That fall-through +**is** the signature: it shows the text layer's own refusal is what test 8 was +asserting, not the container guard standing behind it. + +**Mutate the branch, NOT the variant declaration.** Deleting the variant makes +test 8 — which pin 3b requires to name it — fail to **compile**, and a mutation +that does not compile observes nothing at all: no fall-through, no error +identity, no evidence. An earlier draft said "remove the variant," which would +have produced a compile error and invited the executing agent to report it as +the mutation's "failure." **A compile error is not a test failure**, and this +contract does not accept one as mutation evidence anywhere. + +**Do NOT expect a successful serialization here, and an earlier draft did.** +That draft required observing that a base-bearing document "does serialize into +a major-1 container" with the refusal removed — **impossible under pin 3a**, +which refuses every major-1 base commit outright. Removing the text refusal +reaches that guard; it cannot reach success. The mutation as written could not +have produced its promised observation. + +**The laundering demonstration is deferred to P13-S27**, where generic authority +validation exists and a base commit can succeed or fail on its version rather +than being refused categorically. Record that deferral in S27's contract when +this rung lands — the demonstration is still owed, just not performable yet. + +**M9 — the interim write refusal is not vacuous.** Remove pin 3a's temporary +base-introduction refusal for major-1 containers; test 4 fails. + +**M10 — the interim READ refusal is not vacuous.** Remove pin 3a's open-side +branch; test 11 fails. Run this **separately from M9** — a single mutation +covering both would not show that the two branches are independently present, +which is exactly the gap that made the previous draft unsound. + +**M11 — the third error is genuinely distinct.** Make **both** of pin 3a's +major-1 branches return the row-3 repack error instead of +`ReductionAuthorityUnavailable`. **Tests 4 and 11 must both fail; test 3 is a +control and must stay green.** + +An earlier draft said "tests 10 and 3 must both fail" — **impossible as +written.** M11 touches only the major-1 branches; test 3 exercises the untouched +legacy commit path and passes regardless. That draft's M11 could not have +produced the failure it predicted, and would have been reported as an anomaly or +quietly re-specified by the executing agent. The corrected form is what makes it +signing: test 3 staying green proves the mutation was **confined** to the +major-1 branches, and tests 4 and 11 failing proves both of them assert the +third error rather than any repack error. + +--- + +## §5. Gate + +1. `cargo test --workspace` — full pass; report the new total and its delta. +2. `cargo clippy --workspace --all-targets -- -D warnings` → clean. +3. `cargo fmt -p epiphany-bundle -p epiphany-textproj --check` → clean. + **`cargo fmt --all` is forbidden.** +4. `git diff --cached --check` clean; staged list exactly §2. +5. **`spec/vectors/decode_vectors.txt` unmodified** — by `git status`, not + inspection (§0.3). **`spec/vectors/textproj_document_vectors.txt` MUST have + changed**, and the diff must show every edit from touch row 5c: 18 rows to + `(0 14 0)`, `superseded_companion_version` to `(0 13 0)`, both accepts + re-derived base-free, `canonical_base_before_extension` re-expressed, the new + `canonical_base_present` reject, **20 rows total**, and `expected_reach()` at + its pinned counts. An unchanged corpus is a **failure of this gate**, not a + clean result — it would mean pin 3b's text boundary was never exercised. + **A corpus that changed to 20 rows while `canonical_bases` stayed at 2 is + also a failure** — it would mean a base survived on the accept side. +6. No read-only path was added for **any** of pin 4's three errors: + `grep -rn "read_only" crates/epiphany-bundle/src/bundle.rs` reviewed, and all + three new errors shown not to appear in any branch that sets it. +7. `FORMAT_MAJOR == 1` and `FORMAT_MINOR == 0`, asserted in a test, not only by + reading the constants. + +--- + +## §6. Staging and boundary + +Stage only §2's files, by explicit path. **Never `git add -A`.** + +**A concurrent session commits here.** Re-check `HEAD` before staging and before +commit. **Never** `git reset`, `git restore --staged`, `git checkout`, `git +stash`. + +**Out of bounds — MUST NOT be read, written, or staged:** the entire `spikes/` +tree, `spec/PLAN_EDITOR_APP.md`, `spec/CONTRACT_EDITOR_*.md`, +`spec/ANALYSIS_GENESIS_PERSISTENCE.md`, `spec/ANALYSIS_TEXT_RUN_PRIMITIVES.md`, +`spec/DRAFT_T4_FIXTURE_RECIPE.md`, `crates/epiphany-render-svg/**`, +`crates/epiphany-glyphs/**`, `crates/epiphany-editor-gui/**`, +`crates/epiphany-testkit/benches/editor_pipeline.rs`, the root `Cargo.toml`, +`.claude/worktrees/`. + +**Do not implement P13-S27 or P13-S16.** No `BundleCapabilities`, no +`CURRENT_REDUCTION_ALGORITHM_VERSION`, no `create_staff` change. This rung +establishes the container epoch those depend on; it does not begin them. + +**Editing S27's *contract* is in scope and is required** (touch row 9) — pins 8 +and M8 both write to it. That is a document edit, not an implementation of S27. +S16's contract is **not** touched by this rung. + +**Pin 3a's interim refusal is the one exception** and is explicitly in scope: a +major-1 container refuses base introduction outright until S27 replaces that +with validation. It MUST be marked in code as temporary and MUST NOT be built as +a partial capability check. + +**Do not build a repack flow** (pin 5) and **do not add a read-only mode** for +any of pin 4's three errors. + +**The executing agent MUST NOT commit.** Leave the work staged. + +--- + +## §7. Report requirements + +1. The **eleven** mutations, each with verbatim failure output — **M4 identified + as the signing mutation** of the epoch's non-inheritability; **M11's control + result** (test 3 green) reported alongside its two failures; **M7's two + failures** (major-0 and major-1 halves) reported separately; and **M8's + fall-through error named**, confirming it reached pin 3a's interim guard + rather than a successful serialization. +2. The seven gate results, each with its command. +3. The staged file list and the test-count delta with its cause. +4. The **eleven** tests by name, with 2/3, 2/7, and 4/11-vs-both-legacy-errors + each shown to produce **different** errors. +5. `decode_vectors.txt` unchanged. `textproj_document_vectors.txt` **changed to + 20 rows**, with its diff summarized against touch row 5c. `COMPANION_VERSION` + **at 0.14.0**. Every `expected_reach()` count against pin 3b's table, with + `canonical_bases: 0`'s recorded cause. Which round-trip laws now except + base-bearing documents. These are reported as **performed**, not as open + questions. +6. Confirmation that **no repack flow and no read-only path** were added, and + that pin 3a's refusal is marked temporary in code with P13-S27 named. +6b. The two additions to `spec/CONTRACT_P13S27_REDUCTION_AUTHORITY.md` (touch + row 9), quoted: pin 8's major-1 precondition and M8's deferred laundering + demonstration. +7. Anything contradicting this contract. diff --git a/spec/PASS13_CANDIDATES.md b/spec/PASS13_CANDIDATES.md index 7d2bff6..a4d8baa 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 | -| 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 | +| 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. **S27 and S16 remain blocked** until this rung is implemented; S27's contract is a mandatory touch of it (pin 8 resolves S27's open pin 2a: legacy bases are refused by container epoch, never by version arithmetic) |