epiphany/spec/CONTRACT_FORMAT_EPOCH_MAJOR...

739 lines
42 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Contract — Format epoch: container major 1
**Status:** **RATIFIED** after four adversarial review rounds; **AMENDED
2026-08-07** in a ratified forward amendment of its own — pin 3c, touch rows 10
and 11, gate 8 — closing a gap found by reconnaissance before dispatch: pin 3a's
refusals reach a conformance-suite criterion through a file the touch table did
not carry. Not yet implemented. **The pins are otherwise frozen** — they may be
executed, not edited. A defect found during execution is **reported, not patched
in place**; that is how pin 3c came to exist rather than being absorbed silently
into the work.
**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 3c — the interval's conformance cost is bounded, marked, and owed back.
AMENDED 2026-08-07, after ratification, on a finding from execution
reconnaissance.**
Pin 3a's refusals close **every** path to a base-bearing container, which the
pins did not say out loud: `create` already rejects a base-bearing manifest
(`bundle.rs:233``:240`), row 3 refuses committing one into major 0 and row 6i
into major 1, and rows 2 and 5i refuse *opening* one in either epoch. So for the
S28 → P13-S27 interval **no bundle anywhere may carry a canonical base** — in
production, in tests, or in the conformance suite.
That reaches one file outside the original touch table and one criterion:
`roundtrip::assert_reduction_serialization_stable` (`roundtrip.rs:241`, base
declared at `:270`) is criterion 4's bookkeeping-projection counterpart
(`testkit/src/lib.rs:86`), driven from `tests/acceptance.rs:135` and
`examples/conformance_suite.rs:60`. It commits the canonical state **as the
canonical base** and reopens the image; pin 3a refuses both halves. Gate 1 could
not have passed, and §6 would have forbidden staging the fix.
**Only the canonical-base wiring is suspended. Criterion 4's cycle keeps
running.** The finding was first reported as "the assertion can only be
suspended" — **too strong, and verified false before this pin was written.** The
serialize → load → decode → reserialize cycle does not depend on the snapshot
being the *canonical base*: `read_chunk` (`bundle.rs:509``:511`) hash-verifies
any `ChunkRef` through `read_and_verify_chunk` (`:1002`). The harness therefore
keeps committing the snapshot chunk and reads it back by its ref; the criterion
survives intact.
**Exactly two assertions lapse, both canonical-base-specific:**
1. `verify_canonical_chunks`'s base branch (`bundle.rs:613``:621`), including
the `base.hash != base.root.hash` cross-check;
2. the reopened manifest actually carrying the base (`roundtrip.rs:293``:297`).
**Do NOT re-home the snapshot to `acceleration_snapshots`.** That field appears
**nowhere** in `bundle.rs` — not in `open`, not in `verify_canonical_chunks`
so the reference would verify nothing while looking like preserved coverage.
This is the single most tempting wrong repair here, and it is forbidden.
**The lapse is marked, never absorbed:**
- at the point the base declaration is removed, a comment naming **P13-S27** and
this pin;
- the harness's doc paragraph (`roundtrip.rs:228``:240`) describing the
canonical base as the snapshot's "correct semantic home" is **amended to state
the suspension, not deleted** — a deleted paragraph leaves S27 nothing to
restore against;
- both lapsed assertions are recorded as **owed** in S27's contract (touch
row 9), beside pin 8 and M8's deferred demonstration.
`testkit/benches/bundle.rs:104` (`build_fixture`, base at `:133`) takes the same
treatment. It is the `[[bench]] name = "bundle"` target and **not** the
out-of-bounds `editor_pipeline.rs`.
**One trap, named because it will be met.** `bundle.rs:1487`
(`a_canonical_base_stamped_above_major_0_opens_read_only`) both commits a base —
so it breaks — and asserts **read-only + anomaly** for a base fault. It is on a
different axis (data-model schema major, not container format major) and does
**not** contradict pin 4. Do not "harmonize" pin 4's errors toward it; pin 4
forbids read-only for all three of its errors.
**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 14 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), M8's deferred laundering demonstration, **and pin 3c's two owed-back conformance assertions**. Still a DRAFT, so editable; **not** one of the ratified contracts that may not be touched |
| 10 | `crates/epiphany-testkit/src/roundtrip.rs` | **pin 3c** — criterion 4's canonical-base wiring suspended and marked; the cycle itself preserved via a direct `ChunkRef` read |
| 11 | `crates/epiphany-testkit/benches/bundle.rs` | **pin 3c**`build_fixture` (`:104`) stops declaring a canonical base at `:133`. **Not** `benches/editor_pipeline.rs`, which is out of bounds |
**`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.
8. **No surviving base-declaring site outside a crafted-image fixture or a
refusal test** (pin 3c): `grep -rn "canonical_base = Some\|canonical_base:
Some" crates/ --include='*.rs'` reviewed hit by hit, each remaining one
classified as (a) a hand-built image fixture, (b) a test asserting a refusal,
or (c) the text corpus under pin 3b. **Any hit that is a live `commit` path
is a failure** — it means a base-bearing container is still being minted.
---
## §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 **eight** gate results, each with its command — gate 8's hits classified
one by one, not summarized.
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 **three** additions to `spec/CONTRACT_P13S27_REDUCTION_AUTHORITY.md`
(touch row 9), quoted: pin 8's major-1 precondition, M8's deferred laundering
demonstration, and pin 3c's two owed-back conformance assertions.
6c. Pin 3c's suspension, shown in the diff: the marker naming P13-S27, the
**amended** (not deleted) doc paragraph, and confirmation that criterion 4's
serialize → load → decode → reserialize cycle still runs via a direct
`ChunkRef` read — **and that `acceleration_snapshots` was not used**.
7. Anything contradicting this contract.