739 lines
42 KiB
Markdown
739 lines
42 KiB
Markdown
# 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 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), 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.
|