Contract for P13-S27, tracked as DRAFT and blocked
The reduction-version check is a tautology: reduction_version_for seeds a superblock from the canonical base's self-report, and open compares the base against that superblock. Both operands descend from the same source. It catches a corrupt base disagreeing with its superblock, and necessarily passes for a conformingly propagated stale one. Rulings: a typed BundleCapabilities required at both constructors and carried on the Bundle, with no default; outright rejection on mismatch via CanonicalBaseRequiresRebuild, not read-only and not an anomaly. Storing the capability keeps all 57 commit sites unchanged; only open (57) and create (32) move. Pin 2a stays an open question. Baseline zero does not preserve the corpus, and after S16 moves the authority to one, a pre-S27 base that happens to carry one is indistinguishable from a legitimately rebuilt S16 base. A raw u32 has no provenance. Four candidate dispositions are recorded, including FORMAT_MINOR as a carrier, which is rejected here for two independent reasons: the header never changes after creation (core_spec.tex:10799) so a legacy bundle that commits a validated base keeps its old minor forever, and a minor change may only append append-safe discriminants (core_spec.tex:12258), not alter acceptance semantics — while current readers ignore minor entirely, so the boundary would bind only readers that already comply. What survives: provenance must be carried by a container property old readers cannot silently accept and a later commit cannot inherit unchanged. That is a format-epoch design, filed next as P13-S28. Three claims of mine died while scoping this, all recorded in the contract: a constructor-grep that could not see a propagated value, a head -14 truncation behind a universal negative, and reading `format_major != FORMAT_MAJOR` for the half that supported the proposal. A contract about unobservable defects should carry its own. Docs only. Not dispatchable. S16 and S27 both remain blocked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
This commit is contained in:
parent
381c49849e
commit
79a2507d47
|
|
@ -0,0 +1,505 @@
|
|||
# Contract — P13-S27: the reduction version gets an outside witness
|
||||
|
||||
**Status:** DRAFT — **BLOCKED on P13-S28** (the format-epoch rung). Pins 1 and
|
||||
3–10 are settled, internally consistent, and ratifiable as a plan. **Pin 2a is
|
||||
an open question this contract states rather than answers**, and it is not
|
||||
answerable inside this rung: the provenance carrier it needs is a format-epoch
|
||||
design, which P13-S28 owns.
|
||||
|
||||
**This is bounded analysis, not an abandoned implementation plan.** It may not be
|
||||
dispatched. Pin 2a MUST NOT be treated as a sub-pin that drifts into this rung's
|
||||
scope — see its own prohibition.
|
||||
|
||||
**Rung type:** **capability + API change.** No wire bytes move and no schema
|
||||
major or minor changes — `BundleError` has no discriminant and no encoder
|
||||
(`bundle/src/error.rs`), so a new variant is a pure Rust API change. What does
|
||||
change is `Bundle::open`'s **and `Bundle::create`'s** signatures, at 57 and 32
|
||||
call sites.
|
||||
|
||||
**Does NOT by itself unblock P13-S16.** This rung installs the authority and
|
||||
validates both read and write paths. S16 additionally requires the **legacy-base
|
||||
disposition of pin 2a**, which is an open question this contract states rather
|
||||
than answers.
|
||||
|
||||
**Rulings already made (2026-07-31), not re-opened here:**
|
||||
|
||||
- **Fork 1 → B, typed injected capability.** A required capability carrying the
|
||||
current reduction version, with **no bundle-local default**: every caller
|
||||
consciously supplies the semantics it implements. Broadened from
|
||||
`OpenCapabilities` to **`BundleCapabilities`** in this revision, because it now
|
||||
governs creation and commit as well as opening.
|
||||
- **Fork 2 → outright rejection.** A dedicated error, not read-only and not an
|
||||
integrity anomaly.
|
||||
|
||||
---
|
||||
|
||||
## §0. What was verified before drafting
|
||||
|
||||
Read out of the working tree at `381c498`. Every line number confirmed by
|
||||
reading the line.
|
||||
|
||||
### 0.1 The defect is a tautology, not an absence
|
||||
|
||||
`reduction_version_for` (`bundle.rs:989`) sets a new superblock's version from
|
||||
**the canonical base's own self-report**, `unwrap_or_default()` when there is no
|
||||
base. `open` (`bundle.rs:396`–`:399`) then rejects a bundle whose base's version
|
||||
**disagrees with that superblock's**. Both operands descend from the same
|
||||
source, so for any conformingly-written document the comparison is a tautology.
|
||||
|
||||
**It is not vacuous** — it catches a corrupt or tampered base whose version
|
||||
disagrees with its superblock, and that behaviour is preserved by pin 6. What it
|
||||
cannot catch is a *valid stale base*: one whose version was conformingly
|
||||
propagated from an earlier implementation. That is exactly the case
|
||||
`core_spec.tex:11614`–`:11617` exists to prevent.
|
||||
|
||||
### 0.2 The project has this pattern twice, and both instances are the wrong shape here
|
||||
|
||||
- `SUPPORTED_PROFILE_MAJOR` (`bundle.rs:87`) → `profile_is_understood`
|
||||
(`:939`) → read-only + `IntegrityAnomaly::UnsupportedProfile` (`:389`).
|
||||
- `IntegrityAnomaly::UnsupportedCanonicalChunkMajor { schema_major }`
|
||||
(`error.rs:253`ff).
|
||||
|
||||
Both are **bundle-crate constants**, and `open(store: S)` (`bundle.rs:301`)
|
||||
takes no capability parameter. Fork 1's ruling deliberately departs from this
|
||||
precedent, because a profile major and a schema major are *visible in the
|
||||
container* while reduction semantics are not: a bundle-crate constant would be a
|
||||
number the container crate cannot verify and a rung must remember to bump —
|
||||
the hand-maintained-parallel-table shape that produced P13-S15 and the four
|
||||
Push-4a literal sites.
|
||||
|
||||
### 0.3 The layering forces the split, and makes `ids.rs:288` repairable
|
||||
|
||||
`epiphany-bundle`'s only workspace dependency is `epiphany-determinism`.
|
||||
It cannot read a constant in `epiphany-ops`. So `ids.rs:288`–`:289`'s claim that
|
||||
*"the algorithm catalog itself lives in `epiphany-ops`"* is not merely false —
|
||||
as written it is **unimplementable from where the check must run**.
|
||||
|
||||
Fork 1's ruling makes it true: the authoritative number lives in
|
||||
`epiphany-ops`, and the composing layer wraps it. **`epiphany-ops` must NOT gain
|
||||
a dependency on `epiphany-bundle`** merely to use the wrapper type — hence a
|
||||
plain `u32` in `epiphany-ops` and the `ReductionAlgorithmVersion` wrapper
|
||||
constructed at the composition boundary.
|
||||
|
||||
### 0.4 The surface, and a correction to this contract's own first draft
|
||||
|
||||
**The writer path is production, not test-only. An earlier draft of this
|
||||
section asserted the opposite and was wrong.**
|
||||
|
||||
`epiphany-textproj` serializes a `TextDocument` into a bundle in production
|
||||
code: `serialize_document` (`serialize.rs:119`) creates a bundle and commits a
|
||||
manifest built by `build_manifest` (`serialize.rs:212`), which **copies
|
||||
`base.reduction_algorithm_version` verbatim** into a fresh `SnapshotRef`
|
||||
(`:219`–`:222`). `commit_versioned` then stamps the superblock from that same
|
||||
carried value via `reduction_version_for` (`bundle.rs:798`). `project.rs:936` is
|
||||
a **second** production write path of the same shape.
|
||||
|
||||
**So production code mints a self-consistent stale document without ever calling
|
||||
`open`.** An authority enforced only at read time would leave that entirely
|
||||
open, which is the gap this revision closes (pin 3a, pin 4a).
|
||||
|
||||
> **Method note, recorded because it is this rung's own subject matter.** The
|
||||
> false claim came from `grep … canonical_base | head -14`. The `textproj` hits
|
||||
> were below the cut. A universal negative — "no production code *anywhere*" —
|
||||
> was asserted from deliberately truncated evidence. This is the second
|
||||
> instrument failure in this rung: the first searched for
|
||||
> `ReductionAlgorithmVersion(` constructor calls and so could not see a path
|
||||
> that *propagates* a value without constructing one. Both are the defect S27
|
||||
> exists to fix, committed while scoping it: **an observation that cannot
|
||||
> support the claim drawn from it.**
|
||||
|
||||
**Reader surface — 57 `Bundle::open(` sites across 10 files:**
|
||||
|
||||
| Crate | Sites |
|
||||
|---|---|
|
||||
| `epiphany-bundle` (`bundle.rs` 20, `fuzz.rs` 15) | 35 |
|
||||
| `epiphany-testkit` (`bundle_harness.rs` 11, `roundtrip.rs` 4, `benches/bundle.rs` 2, `tests/bundle_reopen.rs` 1) | 18 |
|
||||
| `epiphany-textproj` (`serialize.rs`, `project.rs`) | 2 |
|
||||
| `epiphany-bundle/tests/` | 2 |
|
||||
|
||||
**Writer surface — 32 `Bundle::create(` sites:** `bundle.rs` 11,
|
||||
`testkit/roundtrip.rs` 6, `testkit/bundle_harness.rs` 6, `textproj/project.rs` 2,
|
||||
`bundle/tests/crash_recovery.rs` 2, `textproj/serialize.rs` 1,
|
||||
`testkit/tests/bundle_reopen.rs` 1, `testkit/benches/bundle.rs` 1,
|
||||
`bundle/tests/manifest_selection.rs` 1, `bundle/fuzz.rs` 1.
|
||||
|
||||
**`commit` has 57 sites** (including 2 in `epiphany-editor-core`) — pin 3's
|
||||
design keeps every one of them unchanged.
|
||||
|
||||
**Which capability each site supplies is NOT decided by crate dependency.**
|
||||
`epiphany-testkit` and `epiphany-textproj` depend on `epiphany-ops`, but that
|
||||
does not mean every call site there should pass the real constant. Format and
|
||||
container fixtures that deliberately exercise arbitrary wire values — e.g.
|
||||
`generators.rs:1628`/`:1651`'s `rng.range(0, 8)`, and the committed
|
||||
text-projection vectors — MUST pass an **explicitly named synthetic capability
|
||||
matching the fixture**, so the test keeps testing what it was written to test.
|
||||
Only **production composition paths** wrap
|
||||
`CURRENT_REDUCTION_ALGORITHM_VERSION`. Pin 3b makes this a naming rule rather
|
||||
than a judgement call.
|
||||
|
||||
---
|
||||
|
||||
## §1. Pins
|
||||
|
||||
**Pin 1 — the authority is a plain `u32` in `epiphany-ops`, with its discipline
|
||||
beside it.**
|
||||
|
||||
```rust
|
||||
pub const CURRENT_REDUCTION_ALGORITHM_VERSION: u32 = 0;
|
||||
```
|
||||
|
||||
**`epiphany-ops` MUST NOT gain a dependency on `epiphany-bundle`.** The constant
|
||||
is a bare `u32`; the `ReductionAlgorithmVersion` wrapper is constructed by the
|
||||
composing layer (pin 3).
|
||||
|
||||
Its doc comment carries the **bump discipline**, in the shape of
|
||||
`PLAN_GMINOR_SCHEMA_MINOR.md`'s epoch rules: *any change to a canonical
|
||||
reduction verdict or to canonical reduced state MUST bump this constant and
|
||||
record the change here*, with a dated list of bumps. State plainly that **no
|
||||
mechanism can detect a semantics change** — a golden test over reduction outputs
|
||||
can prompt the question, never answer it — so the discipline is the guarantee.
|
||||
|
||||
**Pin 2 — the initial value is 0, and this is a decision, not a placeholder.**
|
||||
Bundles written to date carry `0` when they have no base, and bases self-report
|
||||
whatever they were stamped with. Starting the constant at anything but `0` would
|
||||
make every existing base-bearing document fail to open **without any semantics
|
||||
having changed** — the rung would manufacture the breakage it exists to detect.
|
||||
The first real bump is **P13-S16's**, to `1`.
|
||||
|
||||
The doc comment must say this, so `0` is not later read as "unset."
|
||||
|
||||
**Pin 2a — the legacy-base disposition is OPEN, and S16 is blocked on it.**
|
||||
Baseline `0` does **not** preserve every current base-bearing document, and this
|
||||
contract's first draft claimed it would. Verified: `serialize.rs:327`–`:331`
|
||||
builds a canonical base stamped `ReductionAlgorithmVersion(1)` and
|
||||
`serialize_and_reopen` (`:347`) round-trips it; `vectors.rs:353`/`:363` are
|
||||
committed text-projection vectors carrying the same; `generators.rs:1628`/`:1651`
|
||||
emit `rng.range(0, 8)`. Injecting the real authority `0` at every site would
|
||||
reject part of the present corpus.
|
||||
|
||||
Pin 3b handles the *fixtures* — they take named synthetic capabilities. It does
|
||||
**not** handle the real problem:
|
||||
|
||||
> **After S16 moves the authority to `1`, a pre-S27 base that happens to carry
|
||||
> `1` is indistinguishable from a legitimately rebuilt S16 base. A raw `u32`
|
||||
> carries no provenance, so no check can tell a conforming new base from a
|
||||
> coincidental legacy one.**
|
||||
|
||||
`0` may remain the baseline, but this rung MUST settle **one** of:
|
||||
|
||||
- **(i) Normalize before S16** — a migration that rebuilds or re-stamps every
|
||||
pre-authority nonzero base, so the space below the first real bump is empty by
|
||||
construction. Requires locating every such artifact, including committed
|
||||
vectors.
|
||||
- **(ii) Choose a non-colliding epoch** — start the authority at a value no
|
||||
pre-authority artifact can hold (the observed range is `0..8`, so e.g. `1000`),
|
||||
making legacy values structurally distinguishable, and define what an
|
||||
implementation does on encountering one.
|
||||
- **(iii) Carry provenance** — a wider or tagged authority type. The largest
|
||||
change; it makes `ReductionAlgorithmVersion`'s wire meaning richer and is
|
||||
likely a schema question, which the other two are not.
|
||||
|
||||
- **(iv) `FORMAT_MINOR` as a provenance carrier — PROPOSED AND REJECTED
|
||||
2026-07-31.** Recorded here so it is not re-proposed. The idea was to bump
|
||||
`FORMAT_MINOR` (`header.rs:42`) so pre-S27 bundles are structurally
|
||||
identifiable, since `decode` gates on **major** only (`:119`). Two independent
|
||||
objections, either one fatal:
|
||||
|
||||
1. **The header is immutable and commit cannot touch it.**
|
||||
`core_spec.tex:10799`–`:10800`: *"The header never changes after the file is
|
||||
created."* `commit_versioned` (`bundle.rs:791`) computes and publishes only
|
||||
a new **superblock**. So: open a legacy minor-1 bundle with no base; under
|
||||
S27 commit a base that writer validation just accepted; the header is still
|
||||
minor 1. If minor ≤ 1 bases are rejected, a base S27 itself validated
|
||||
becomes permanently unusable. If they are accepted, S16 still cannot tell an
|
||||
old arbitrary `1` from a real S16 `1`. **Both branches fail**, which is the
|
||||
whole question 2a exists to settle.
|
||||
2. **A minor bump may not mean this.** `core_spec.tex:12258`–`:12262`: minor
|
||||
changes are backward-compatible and *"MUST only append discriminants to the
|
||||
append-safe"* set. Making a previously-valid canonical base newly rejectable
|
||||
is a **semantic acceptance change**, not an append. And because current
|
||||
readers ignore minor entirely, they would open minor-2 bundles and skip the
|
||||
check — an enforcement boundary that binds only the readers that already
|
||||
comply.
|
||||
|
||||
The proposal also mis-stated its own corpus result: after the bump,
|
||||
`serialize_document` writes a **minor-2** header, so its base-`1` fixture must
|
||||
fail writer validation under real authority `0`. A synthetic capability
|
||||
(pin 3b) can preserve that as a *format* test, but it cannot make the fixture
|
||||
evidence about a legacy minor-1 bundle — the two are different documents.
|
||||
|
||||
**The insight worth keeping from (iv), stated so a later rung can use it:**
|
||||
|
||||
> Provenance MUST be carried by a container property that **old readers cannot
|
||||
> silently accept** and that **cannot be inherited unchanged by a later commit**.
|
||||
> `FORMAT_MINOR` fails both halves; any candidate carrier must be checked against
|
||||
> both before it is proposed.
|
||||
|
||||
That points at a real format-epoch design — most plausibly a **major**-version
|
||||
boundary with explicit legacy-read and rebuild handling, or a
|
||||
generation-scoped attestation paired with an incompatibility boundary for old
|
||||
readers. Either is a **format-design rung of its own**, with an honest
|
||||
rebuild/repack policy for legacy bases; neither is a clause this contract can
|
||||
absorb.
|
||||
|
||||
**Until a disposition is ratified AND tested, P13-S16 does not become
|
||||
dispatchable merely because S27 lands.** This pin deliberately remains an open
|
||||
question. It is not to be amended into a disposition without its own ratification
|
||||
round.
|
||||
|
||||
**Pin 3 — `BundleCapabilities`, required at both constructors, carried on the
|
||||
`Bundle`.**
|
||||
|
||||
```rust
|
||||
pub struct BundleCapabilities {
|
||||
pub current_reduction_version: ReductionAlgorithmVersion,
|
||||
}
|
||||
```
|
||||
|
||||
**It MUST NOT implement `Default`, and MUST NOT expose a bundle-local
|
||||
constant.** Both would let a caller use a bundle without stating what semantics
|
||||
it implements, which is the defect this rung exists to remove.
|
||||
|
||||
`Bundle::open(store, caps)` **and `Bundle::create(store, …, caps)`** both take
|
||||
it; the `Bundle` **stores it**. `commit` and `commit_versioned` then validate
|
||||
against `self`'s copy and **their 57 call sites are unchanged** — the capability
|
||||
is a property of the session, not of each call. Naming it
|
||||
`BundleCapabilities` rather than `OpenCapabilities` follows from its now
|
||||
governing creation and commit.
|
||||
|
||||
The struct is a struct rather than a bare parameter so later capabilities append
|
||||
without another signature break — say so in its doc, and do not add speculative
|
||||
fields now.
|
||||
|
||||
**Pin 3a — the writer is validated, not only the reader.** §0.4 shows production
|
||||
code minting a stale document without ever calling `open`. Therefore
|
||||
`commit`/`commit_versioned` MUST reject a manifest whose **newly emitted or
|
||||
replaced** canonical base carries a version differing from
|
||||
`self.caps.current_reduction_version`, with the same error as pin 4.
|
||||
|
||||
"Newly emitted or replaced" is the operative scope: a commit that does not touch
|
||||
`canonical_base` MUST NOT be refused merely because an inherited base is stale —
|
||||
that document could not have been opened in the first place, and refusing here
|
||||
would make an unrelated commit the site of the diagnosis.
|
||||
|
||||
**Pin 3b — synthetic capabilities are named, so the choice is not a judgement
|
||||
call.** Provide a clearly-named constructor for fixture use, e.g.
|
||||
`BundleCapabilities::synthetic_for_fixture(v: u32)`, whose doc states it is for
|
||||
format and container fixtures deliberately exercising arbitrary wire values and
|
||||
**must never appear in a production composition path**. Production paths wrap
|
||||
`epiphany_ops::CURRENT_REDUCTION_ALGORITHM_VERSION`.
|
||||
|
||||
Every call site converted by this rung uses one or the other **explicitly**;
|
||||
none may take a value that merely happens to be in scope.
|
||||
|
||||
**Pin 4 — the mismatch is a hard error.**
|
||||
|
||||
```rust
|
||||
BundleError::CanonicalBaseRequiresRebuild {
|
||||
base: ReductionAlgorithmVersion,
|
||||
current: ReductionAlgorithmVersion,
|
||||
}
|
||||
```
|
||||
|
||||
A `BundleError`, **not** an `IntegrityAnomaly`, and **not** a read-only
|
||||
degradation. Read-only is wrong on the merits: a stale base is not a
|
||||
restricted-but-correct view, it is **the wrong materialization**, and exposing it
|
||||
read-only would serve incorrect canonical state confidently.
|
||||
|
||||
Its doc must record why `open` cannot recover: **`open` cannot rebuild**, and
|
||||
drop-and-replay is unsound once pruning exists (`core_spec.tex:12207`, `:14701`
|
||||
— pruning is specified and **not implemented**; no `prune` appears anywhere in
|
||||
`epiphany-bundle`). A higher-level rebuild path may be authorized later **only
|
||||
where full pre-base history is demonstrably available**; this rung authorizes
|
||||
none.
|
||||
|
||||
**Pin 5 — the check runs where the current check runs, and only for a base.**
|
||||
In `open`, at `bundle.rs:396`. A bundle with **no canonical base is openable
|
||||
regardless** of `caps.current_reduction_version` — there is no reduced state to
|
||||
be stale. Order the two checks so the corrupt case (pin 6) is distinguishable.
|
||||
|
||||
**Pin 6 — the existing corrupt-disagreement failure is preserved, distinctly.**
|
||||
The current base-vs-superblock disagreement check must keep returning its
|
||||
existing malformed-bundle `DecodeError`. It is **not** replaced by pin 4 and
|
||||
**not** merged with it: they detect different things — tampering versus valid
|
||||
staleness — and collapsing them would lose the distinction §0.1 rests on.
|
||||
|
||||
**Pin 7 — `reduction_version_for` does not change, and the reasoning is
|
||||
recorded.**
|
||||
It keeps sourcing from the base's self-report — but its justification now rests
|
||||
on **both** validation points, and the first draft's single-sided reasoning was
|
||||
insufficient:
|
||||
|
||||
- **read-time (pin 5):** an opened bundle has proved `base == current`;
|
||||
- **write-time (pin 3a):** a newly emitted or replaced base has proved the same.
|
||||
|
||||
Only together do these make propagation equal to propagating the current
|
||||
version. A doc line MUST state both, naming pin 5 and pin 3a — otherwise a later
|
||||
reader sees an unguarded self-report and either "fixes" it or trusts it in a
|
||||
context where neither check has run. §0.4's correction is exactly what happens
|
||||
when that reasoning is done one-sided.
|
||||
|
||||
**Pin 8 — `ids.rs:288`–`:289` becomes true.**
|
||||
Repair the catalog claim to name the real location and the real mechanism: the
|
||||
authoritative version lives in `epiphany-ops` as a plain `u32`; the wrapper type
|
||||
is constructed at the composition boundary; `epiphany-bundle` deliberately does
|
||||
not depend on `epiphany-ops`. This is the rung that earns the sentence.
|
||||
|
||||
**Pin 9 — normative specification text.**
|
||||
`core_spec.tex` Chapter 8 §"Canonical Document Identity" currently states the
|
||||
rebuild requirement (`:11614`) with no error surface. Add the rejection
|
||||
behaviour normatively, and a Revision History row. There is **no** existing
|
||||
"requires rebuild" error language in either document — verified — so this is new
|
||||
prose, not an amendment.
|
||||
|
||||
**Pin 10 — the ledger.**
|
||||
`spec/PASS13_CANDIDATES.md`: P13-S27 → RESOLVED, recording both rulings, the
|
||||
baseline-0 decision, the writer-path correction of §0.4, and **whichever
|
||||
legacy-base disposition pin 2a settles on**.
|
||||
|
||||
**P13-S16's row does NOT become dispatchable on this rung alone.** It moves from
|
||||
"blocked on P13-S27" to "blocked on the pin-2a legacy disposition" unless 2a is
|
||||
ratified and tested within this rung, in which case it opens and its row records
|
||||
that its first act is bumping the authority past the baseline.
|
||||
|
||||
---
|
||||
|
||||
## §2. Touch table
|
||||
|
||||
| # | File | Change |
|
||||
|---|---|---|
|
||||
| 1 | `crates/epiphany-ops/src/lib.rs` (or a new `reduction.rs`) | pins 1, 2 |
|
||||
| 2 | `crates/epiphany-bundle/src/bundle.rs` | pins 3, 3a, 3b, 5, 6, 7 + 35 in-crate `open` sites + 11 in-crate `create` sites |
|
||||
| 3 | `crates/epiphany-bundle/src/error.rs` | pin 4 |
|
||||
| 4 | `crates/epiphany-bundle/src/ids.rs` | pin 8 |
|
||||
| 5 | `crates/epiphany-bundle/src/fuzz.rs` | call sites |
|
||||
| 6 | `crates/epiphany-bundle/tests/{crash_recovery,manifest_selection}.rs` | call sites |
|
||||
| 7 | `crates/epiphany-testkit/src/{bundle_harness,roundtrip,generators}.rs` | call sites, real authority |
|
||||
| 8 | `crates/epiphany-testkit/tests/bundle_reopen.rs`, `benches/bundle.rs` | call sites |
|
||||
| 9 | `crates/epiphany-textproj/src/{serialize,project}.rs` | call sites, real authority |
|
||||
| 10 | `spec/core_spec.tex` (+ `.pdf`) | pin 9 |
|
||||
| 11 | `spec/PASS13_CANDIDATES.md` | pin 10 |
|
||||
|
||||
---
|
||||
|
||||
## §3. Required tests (pin 4's ruling names all four)
|
||||
|
||||
Named, permanent, in `epiphany-bundle`:
|
||||
|
||||
1. **`open_succeeds_when_base_and_current_reduction_versions_match`**
|
||||
2. **`open_rejects_a_valid_stale_canonical_base`** — base and superblock agree
|
||||
with each other, `caps.current` differs. Must return
|
||||
`CanonicalBaseRequiresRebuild { base, current }` with **both fields
|
||||
asserted**, not merely the variant.
|
||||
3. **`a_corrupt_base_superblock_disagreement_still_fails_as_malformed`** — the
|
||||
pin-6 path, asserted to be the *existing* malformed error and **not**
|
||||
`CanonicalBaseRequiresRebuild`.
|
||||
4. **`a_bundle_with_no_canonical_base_opens_at_any_reduction_version`** — run at
|
||||
two different `caps` values.
|
||||
5. **`committing_a_stale_canonical_base_fails_and_leaves_the_prior_generation_reopenable`**
|
||||
— the pin-3a writer test, and the one this contract's first draft omitted
|
||||
entirely. Create a bundle, commit a good generation, then attempt a commit
|
||||
whose newly emitted canonical base carries a version differing from `caps`.
|
||||
Assert **both**: the commit fails with `CanonicalBaseRequiresRebuild`, **and**
|
||||
the bundle reopens at the prior active generation with its earlier content
|
||||
intact. A writer check that corrupts the document while refusing is worse than
|
||||
no check.
|
||||
|
||||
Tests 2 and 3 must be **paired in review**: each asserts the other's error is
|
||||
*not* produced. A test that only checks its own variant cannot show the two
|
||||
paths are distinguishable, which is the whole point of pin 6.
|
||||
|
||||
---
|
||||
|
||||
## §4. Mutation plan
|
||||
|
||||
Applied, **run**, output recorded verbatim, restored **by hand-editing back**.
|
||||
|
||||
**M1 — the new check fires.** Delete pin 5's comparison; test 2 must fail.
|
||||
|
||||
**M2 — the paths are distinguishable.** Make pin 4's error subsume pin 6's
|
||||
(return `CanonicalBaseRequiresRebuild` for the corrupt case too); test 3 must
|
||||
fail. Signs that §0.1's distinction is real in the code, not only in prose.
|
||||
|
||||
**M3 — the no-base exemption is deliberate.** Make the check run when
|
||||
`canonical_base` is `None`; test 4 must fail.
|
||||
|
||||
**M4 — the capability is genuinely required.** Add `impl Default for
|
||||
BundleCapabilities` and a call site using it. **This must be observed to
|
||||
compile**, then reverted — it demonstrates what pin 3 forbids and why the
|
||||
prohibition needs to be a review rule, since no test can catch a `Default` that
|
||||
callers then use. Report it as a *prohibition recorded*, not a guard.
|
||||
|
||||
**M5 — the authority is the one consulted, and by a production path.** Change
|
||||
`CURRENT_REDUCTION_ALGORITHM_VERSION` to a different value. The failing test
|
||||
MUST be one exercising a **production composition path** — `textproj`'s
|
||||
`serialize_document` / `project` round trip — **not** a fixture using
|
||||
`synthetic_for_fixture`, which by design would not move. Report which test
|
||||
failed; if only fixture tests fail, pin 3b has been applied backwards and that
|
||||
is a finding.
|
||||
|
||||
**M6 — the writer check fires.** Remove pin 3a's commit-side validation; test 5
|
||||
must fail. Then narrow it to refuse *any* stale inherited base rather than only a
|
||||
newly emitted or replaced one, and confirm an unrelated commit on an
|
||||
already-open bundle starts failing — signing that pin 3a's scope is deliberate.
|
||||
|
||||
---
|
||||
|
||||
## §5. Gate
|
||||
|
||||
1. `cargo test --workspace` — full pass; report the new total and the delta with
|
||||
its cause (four tests added).
|
||||
2. `cargo clippy --workspace --all-targets -- -D warnings` → clean.
|
||||
3. `cargo fmt -p epiphany-ops -p epiphany-bundle -p epiphany-testkit -p
|
||||
epiphany-textproj --check` → clean. **`cargo fmt --all` is forbidden.**
|
||||
4. `git diff --cached --check` clean after staging; staged list exactly §2.
|
||||
5. **`epiphany-ops` has NOT gained an `epiphany-bundle` dependency** — check
|
||||
`crates/epiphany-ops/Cargo.toml` directly.
|
||||
6. **`BundleCapabilities` implements no `Default`**, by exact query with an
|
||||
expected count of **zero**:
|
||||
`grep -rnE "impl +Default +for +BundleCapabilities|derive\\([^)]*\\bDefault\\b[^)]*\\)[[:space:]]*(pub )?struct BundleCapabilities" crates/epiphany-bundle/src/`
|
||||
→ **0 matches.** Run it against production source only; report the count, not
|
||||
a verdict.
|
||||
6a. **No production composition path uses the fixture constructor:**
|
||||
`grep -rn "synthetic_for_fixture" crates/epiphany-textproj/src/` shows matches
|
||||
**only** inside `#[cfg(test)]` modules. Report each match with its enclosing
|
||||
item.
|
||||
7. `spec/vectors/decode_vectors.txt` unmodified; no schema major/minor moved.
|
||||
|
||||
---
|
||||
|
||||
## §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 any part of P13-S16.** This rung unblocks it; it does not
|
||||
begin it. No `create_staff`, `create_staff_group`, or invariant change.
|
||||
|
||||
**Do not bump `CURRENT_REDUCTION_ALGORITHM_VERSION` past 0** — pin 2. The bump
|
||||
to 1 belongs to S16.
|
||||
|
||||
**The executing agent MUST NOT commit.** Leave the work staged.
|
||||
|
||||
---
|
||||
|
||||
## §7. Report requirements
|
||||
|
||||
1. The five mutations, each with verbatim output (M4 as a recorded prohibition).
|
||||
2. The seven gate results, each with its command.
|
||||
3. The staged file list and the test-count delta with its cause.
|
||||
4. The four required tests by name, each passing, with tests 2 and 3 shown to
|
||||
produce *different* errors.
|
||||
5. A count of call sites updated per crate, against §0.4's table — any
|
||||
discrepancy is a finding.
|
||||
6. Anything contradicting this contract.
|
||||
Loading…
Reference in New Issue