42 KiB
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:
- 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.
- Legacy resolves to hard rejection, not read-only. A pre-authority base is not safe materialized state.
- 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:393ff) 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:74ff) has no container-major or epoch
field, and the projector deliberately drops physical layout
(project.rs:29ff, 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:903ff) 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
SerializeErrorvariant, as defence for a directly constructedTextDocument.
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:889asserts 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, assertingCOMPANION_VERSION == (0, 13, 0)at:971) — both its body and its name move to 0.14.0/0.13.0;parse.rs:658's testHEADERconstant, andtext_projection.tex:486and: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:
verify_canonical_chunks's base branch (bundle.rs:613–:621), including thebase.hash != base.root.hashcross-check;- 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
(:10796ff) 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:
- this pin's major-1 precondition, resolving S27's open pin 2a;
- 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 (:903ff) 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
a_legacy_major_0_bundle_without_a_base_opensa_legacy_major_0_bundle_with_a_base_is_rejected— asserting the row-2 error specifically.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.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.an_unknown_major_is_still_unsupported_format_version— the third arm of pin 2, which a two-way test would silently drop.text_projection_serialize_produces_a_major_1_container— pin 6.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'sReductionAuthorityUnavailable. 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.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 dedicatedSerializeErrorvariant specifically — not merely "an error", and notSerializeError::Bundle. M8 depends on that distinction being asserted.projecting_a_base_bearing_bundle_is_refused— pin 3b's projection side.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.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
cargo test --workspace— full pass; report the new total and its delta.cargo clippy --workspace --all-targets -- -D warnings→ clean.cargo fmt -p epiphany-bundle -p epiphany-textproj --check→ clean.cargo fmt --allis forbidden.git diff --cached --checkclean; staged list exactly §2.spec/vectors/decode_vectors.txtunmodified — bygit status, not inspection (§0.3).spec/vectors/textproj_document_vectors.txtMUST have changed, and the diff must show every edit from touch row 5c: 18 rows to(0 14 0),superseded_companion_versionto(0 13 0), both accepts re-derived base-free,canonical_base_before_extensionre-expressed, the newcanonical_base_presentreject, 20 rows total, andexpected_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 whilecanonical_basesstayed at 2 is also a failure — it would mean a base survived on the accept side.- No read-only path was added for any of pin 4's three errors:
grep -rn "read_only" crates/epiphany-bundle/src/bundle.rsreviewed, and all three new errors shown not to appear in any branch that sets it. FORMAT_MAJOR == 1andFORMAT_MINOR == 0, asserted in a test, not only by reading the constants.- 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 livecommitpath 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
- 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.
- The eight gate results, each with its command — gate 8's hits classified one by one, not summarized.
- The staged file list and the test-count delta with its cause.
- The eleven tests by name, with 2/3, 2/7, and 4/11-vs-both-legacy-errors each shown to produce different errors.
decode_vectors.txtunchanged.textproj_document_vectors.txtchanged to 20 rows, with its diff summarized against touch row 5c.COMPANION_VERSIONat 0.14.0. Everyexpected_reach()count against pin 3b's table, withcanonical_bases: 0's recorded cause. Which round-trip laws now except base-bearing documents. These are reported as performed, not as open questions.- 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 directChunkRefread — and thatacceleration_snapshotswas not used. - Anything contradicting this contract.