Nothing in the store has ever removed an index run. `seal_index` pushes
one per seal, adoption pushes the runs staging materialized, and each
successor generation carries the predecessor's forward. That was invisible
while nothing checkpointed on a cadence; wiring the P2 harness to
checkpoint inside its measured window made it immediate, and a 125-second
run died with `max_open_index_runs (observed: 33, allowed: 32)`.
The frozen workload is arithmetically unreachable without compaction: 300s
warmup plus 900s measured against 32 open runs allows at most one
checkpoint per ~37 seconds and ends at the ceiling with no headroom for a
fan-out-triggered seal. Raising the ceiling is the disclosed-weakening
pattern retired earlier today, and would make `runs_sealed` describe a run
whose fan-out grows unbounded; shortening the measured window is a Phase 0
contract. Merging runs is the only option that does not trade what the
number means for the ability to produce one.
A merge cannot be written against an API that answers point lookups only,
so `IndexRun::entries` is granted by contract review 2026-08-09-C --
requested rather than emitted, because `index.rs` is a frozen Wave A
interface. It is additive and read-only; no byte of the format moves. The
order is the one already on the device, so a caller that re-encodes what
it reads produces the layout it consumed. It reports no per-entry error:
every value is decoded from bytes the run validated at `open`, and a run
that could not be trusted entry by entry should not have opened.
The test asserts the walk against the run's own `get` rather than against
the delta it was built from. The delta is what the encoder was given; the
question is whether the decoder reads back what was written, and a merge
built on an iterator that disagreed with `get` would relocate entries
silently.
The compaction that consumes this is not here. Its shape is recorded in
the review: partition by generation domain, because a run section is
per-namespace with a 16-bit generation span and adopted-projection
generations sit at `1 << 63`, so journal-backed and staging-owned runs can
never merge into one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGdH5V43XWnj1PdHqiPktQ
Two things stood between the harness and a bundle that could encode a
pass, and neither was a hardware or duration problem.
`run_conditions.checkpointing` had to be "exercised", and no code path
could produce it: `CheckpointProbe` had two variants and the probe mapped
`Ok(_)` to `EnabledNotReached` unconditionally. The scope doc's claim that
"what remains is a run long enough to take a checkpoint, not a mechanism
to build" was wrong -- the harness took no checkpoint inside the measured
window at all.
It does now, on a thread that lives exactly as long as the submitters, so
the cost lands in the reported rate. That is the honest place for it: a
number that excluded index maintenance would not describe a steady state
either. `Exercised` is derived from checkpoints the run took, never from
the probe -- the probe can only establish that checkpointing is possible,
which is what `enabled_not_reached` already says.
Checkpointing made the emitter refuse: sealing had run and left a partial
backlog, and `index_maintenance` requires a drained one for `runs_sealed`.
A steady-state run always has a partial backlog wherever the clock stops,
so the strict check and the periodic checkpoints could not both hold. The
run now takes one closing checkpoint *after* the window, which changes no
reported rate and leaves the strict check intact. The alternative --
accepting a bounded backlog -- would assert a steady state while carrying
a backlog the bundle has no field to report.
`run` was a stub that still said submit "is B1 NamespaceTxn ... cannot
produce a P2 result". It is implemented, sharing one emitter with
`emit-skeleton` so their measurement blocks cannot drift, and differing
only in the window arithmetic and the `skeleton` flag. Commits are counted
per one-minute window at the commit site: a counter per minute is bounded
by the run's length, while a timestamp per commit is bounded by its
throughput -- 22 million of them at target. Whole windows only, and a
measured run with no whole window is refused rather than falling back to
the skeleton's synthesized one. Both halves of section 3 are checked: the
percentage the schema records, and the floor no single window may fall
below, which nothing downstream would otherwise notice.
`scripts/close-phase1.sh` runs the gate, the recovery campaign, and the P2
repetitions, and reports one checklist. It is a reporter and never a
promoter: it exits non-zero unless every criterion passes and never edits
a bundle to make one pass, so it is safe to rehearse on non-reference
hardware -- which is how the `run` stub was found.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGdH5V43XWnj1PdHqiPktQ
`run_conditions.index_run_ceiling` offered one value for a raise, and it
named a reason: `raised_because_index_sealing_unimplemented`. The emitter
never established that reason. It derives the declaration by comparing the
configured ceiling with the store default, and a comparison of two numbers
cannot know why they differ -- so the bundle asserted a cause on the
strength of a subtraction.
That was accurate while the unimplemented seal was the only reason to
raise the ceiling. Sealing landed, the raise was dropped, and the value
became a false explanation waiting for the next run that raises the
ceiling for any other purpose -- which the schema would have called valid.
`raised_above_store_default` is added and is what the emitter now writes.
It states the fact the comparison establishes and stops there.
The old spelling is retained and deprecated rather than removed.
`schema_version` is `const: 1`, so there is no later version to move
archived bundles to, and this repository has never held a bundle to check
against; the reference machine may hold ones that declare it. Invalidating
evidence already produced is worse than carrying a spelling nothing emits.
Deprecated does not mean unchecked, and that was the trap. The `allOf`
rule flooring a declared raise at 65 accepts either spelling, so a bundle
using the old one is still cross-checked against the recorded ceiling. Had
the rule kept keying on the new string alone, the deprecated value would
have skipped the cross-check entirely and been valid while recording 64.
`both_raised_spellings_are_accepted_and_bound_the_same_way` asserts, for
each spelling, that it validates and that it is refused when it records
the store default; narrowing the rule to the new value alone fails it.
The protocol crate's `submit_path_bundle` keeps the deprecated spelling
deliberately, as the standing proof that an archived bundle validates.
Recorded as contract review 2026-08-09-B. The scope doc's carry-forward
closed on the grounds that the enum was untouched -- true then, superseded
now -- so it carries an amendment rather than a rewrite, and its "truthful
values today" table now points at the corrections recorded elsewhere in
the same document.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGdH5V43XWnj1PdHqiPktQ
The B4 harness documented `max_index_runs` as a ceiling reached by
refusal: B1's slice sealed no delta layer into an `IndexRun`, so `submit`
returned `NotImplemented` after exactly that many group publications, and
a driver needing more had to raise the ceiling to run at all. That was
recorded as a disclosed weakening because it configured around a missing
deliverable rather than a tuning choice.
Every part of it is now false. Sealing is implemented, the ceiling is
drained by sealing rather than reached by refusing, and no
`NotImplemented` refusal remains in the engine's production paths. A
45-second soak at the default published 5,192 groups -- 81x the ceiling --
at a flat rate. Leaving the disclosure in place understates the store in
its own evidence.
The same claim appeared in `store-bench.rs`'s `configured_ceilings`
comment and in the plan's note on why `max_index_runs` became a required
bundle member; both now say the run seals against the ceiling.
`scripts/check-phase1.sh` was not prose. Its crash-matrix pending-row
check was guarded on B1 having landed, proxied by `engine.rs` no longer
mentioning `NotImplemented`. B1 landed and the token survived in a module
doc and in two tests asserting an error is *not* one, so the grep matched
and the check silently stopped running. Zero pending rows made that
harmless, but a gate condition that had inverted is worth more than the
row it was guarding. The transitional guard is gone and the check is
unconditional.
`CONTENTION_MAX_INDEX_RUNS` is removed rather than aliased: the driver
reaches a genuinely lost committed-root CAS after 11 and 12 transactions
against a ceiling of 64, reproducibly, and the row's test panics rather
than passes if the failpoint never fires. The old note measured a worst
case that could exceed 64, which no longer refuses either.
`bench/result-schema.json` is untouched. Nothing in-tree emits
`raised_because_index_sealing_unimplemented` now, but it remains a valid
declaration for a bundle that did raise the ceiling, and retiring it is a
compatibility decision of its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGdH5V43XWnj1PdHqiPktQ
Startup states 3 and 4 shared a refusal that named both, because the
recognizer separating them had not been written. That under-classification
was honest but it cost the operator the answer: a legacy instance holding
real data and a directory belonging to something else got the same
"refusing to modify", and only one of them has a next step.
State 3 is the scope's signature and nothing looser: at least one entry
whose name is 64 hexadecimal characters and which holds a `.levcs`
directory. It is recognized, never inferred -- a missing FORMAT is
evidence of nothing, and most roots without one are state 4.
Neither half of the signature may be reached through a symlink. The entry
type comes from the directory entry itself and `.levcs` is checked with
`symlink_metadata`, because the claim being made is about what this root
*holds*, not what a link in it can reach. `Path::is_dir` follows links, so
a `.levcs` pointing at any directory anywhere satisfied it, and a root
that borrowed the shape from elsewhere was answered with a migration
command for repositories it does not have.
The recognizer opens no file and creates nothing, and runs last -- only
for a root already known non-empty and unformatted. A probe that wrote so
much as a directory would destroy the byte-identity guarantee state 4
promises, and destroy it before the refusal that promises it.
State 3 carries the exact command with the source filled in and
`--destination` left as a placeholder: only the operator knows where the
v2 root goes, and inventing one produces a command that runs and writes
somewhere nobody chose. State 4 reports what was observed and suggests
nothing, because the directory may be another application's, a partial
backup, or a root whose FORMAT was deleted, and a migration taken on faith
costs data. A test asserts state 4's message does not mention migration.
`tree_image` no longer follows links either. It records a symlink as its
own kind carrying its target, which is both what let the new tests be
written and strictly more sensitive than before: it now notices a symlink
replaced by a real directory of the same name, the one substitution a
contents-only image called unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGdH5V43XWnj1PdHqiPktQ
A same-ID/same-digest submit arriving while the first is still in flight
was refused. Nothing is wrong with it: it is one request that reached the
store twice, and the durable path cannot answer it because there is no
receipt yet and no terminal entry to read.
The follower reserves nothing and sequences nothing. `accept` removes its
waiter and moves its completion onto the leader's, so from there it is
answered by whatever resolves the leader and by nothing else -- one frame,
one sequence, two callers answered. The tests assert that by requiring the
follower's `repo_sequence` to equal the leader's, because a second
sequence would mean a second frame for one operation.
Followers live on the leader's `Waiter` rather than in a table keyed by
operation, so a follower cannot outlive the request it follows: every path
that resolves a waiter drops it and takes its followers with it. All eight
completion sites now go through `Waiter::resolve`, which answers the
followers and then the leader with one outcome.
That centralization is the point rather than tidiness. Only one of those
sites is the happy path; the rest are pre-append refusal, poison drain,
deadline removal, and panic unwind. A follower any of them forgot would
not fail -- it would hang, with neither receipt nor error, which is the
outcome hardest to notice and hardest to diagnose. So the case worth
proving is a leader taken down inside the poison window, and
`a_follower_is_answered_when_its_leader_is_poisoned` arms one.
The two coalescing tests hold the fault serial even though only one arms a
failpoint. The registry is a one-shot global, and without it the arming
test fired inside its sibling's engine -- a real interference that made
both pass alone and fail together.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGdH5V43XWnj1PdHqiPktQ
A client that submitted, lost the answer, and submitted again was refused.
Retrying is the normal case, and the operation ID exists precisely so the
store can say what happened the first time instead of either appending a
second frame or turning a recoverable disconnect into a failure.
The rule is `oracle::coalescing_decision`'s durable branch applied to a
terminal entry rather than restated beside it: a matching stable digest
returns the durable receipt, and a different one is a conflict. The digest
is what makes the answer safe. An operation ID alone cannot tell a retry
from a different request reusing an identity, and answering the second
with the first's receipt would tell a caller its transaction committed
when another one did.
An expired entry is refused rather than answered. Its tombstone still
binds the ID against reuse, but the receipt is gone and a tombstone is not
a statement about this submit's outcome. The remaining `TransactionStatus`
variants are named and poison: a terminal entry reporting `Pending`,
`Resolving`, or `Unknown` is a contradiction, and a catch-all would answer
it with whatever the last arm happened to be.
The receipt reaches `accept` as a field rather than through `StoreError`,
so `accept` completes the waiter with it. Carrying a success through the
error channel would make every caller of `prepare` responsible for
noticing that one variant means it worked.
Nothing is sequenced and no frame is appended for a retry, and the tests
replay one through a reopen, where the answer can only come from the root
recovery rebuilt.
In-flight coalescing -- a same-digest resubmit attaching to a leader that
has not resolved yet -- is still refused. It changes waiter and completion
ownership and is left to its own change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGdH5V43XWnj1PdHqiPktQ
A journal is preallocated, and a shard that filled it stopped accepting
work. Nothing about the refused transactions was wrong and no amount of
retrying made room, so the shard was simply done -- the one failure mode
a store cannot have.
Rotation cannot happen from inside preparation. The open group's frames
are built but not appended and each carries `journal_id` in its header,
so sealing mid-formation would leave them naming a journal that no longer
takes writes. `accept` therefore publishes the open group, rotates, and
re-prepares, which is the shape the group-full path already used. The
reservation is released first; speculative state is not advanced until
past the fit test, so that release is the whole rollback.
Two triggers, and the second is not implied by the first: a group that no
longer fits must rotate or be refused, and a cursor that has reached
`segment_max_bytes` must rotate so segments stay near their configured
size. With `segment_max_bytes` below `journal_preallocate_bytes` that
boundary arrives first and every time, and checking only the fit let
segments grow to the whole preallocation whatever the ceiling said. The
threshold is deliberately not a per-group cap: it is read before a group
is added rather than inside one, so a segment may overshoot by at most a
group and no committed group is ever split.
A frame no *empty* journal could hold is a ceiling, not a rotation --
sealing would produce a fresh journal that refuses it again, forever. It
is measured against the preallocation less the journal header, because a
new journal's cursor starts past that header; comparing against the whole
preallocation called frames in that gap rotatable and retried them into
the same refusal.
The manifest advances `committed_shard_sequence` to the sealed segment's
last. `validate_manifest_sequence_coverage` requires the final retained
tail range to end exactly there, and sealing does make that prefix
durable under a second, manifest-referenced name. Installing an index run
is the case that differs and correctly carries the field forward: a run
makes no journal frame more durable than it already was.
The successor generation pins the new segment and the fresh tail. The
predecessor's active tail is not carried forward -- that file is the
segment now, and retaining both would leave one logical generation naming
two sources, which `object_source` refuses.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGdH5V43XWnj1PdHqiPktQ
`adopt_projection` reached `build` and stopped there. Wiring it through
`submit` is one change because there is no safe partial: the moment the
builder stops refusing, a submit that ignores the adoption drops a live
pin with no outcome, which is the leak scope 6.5 declares a bug.
The pin cannot live inside the transaction. `Prepared` retains an
`Arc<ValidatedTransaction>` so a group can be re-sequenced when the
pre-mark deadline recheck drops a member, and an `Arc` has no move-out.
It travels beside it in a one-shot `AdoptionSlot` that settles itself
from an append phase rather than from a guess: the phase advances at the
first frame write, not at the fence, so torn and unfenced bytes stay
recovery-owned. That makes the unenumerated routes safe by construction
-- queue rejection, pre-append errors and panics, deadline reforming,
writer unwinding -- instead of correct only where someone remembered.
Two orderings are now statements rather than drop-order accidents. Both
the poison window and every pre-append refusal settle the pin before
resolving the waiter; otherwise a submit could return while staging still
believed the pin was live.
Membership could not travel as an index delta. Adopted generations sit in
a reserved band `1 << 63` away from journal generations, and an index run
packs `segment_generation` as a 16-bit delta from a per-namespace section
base, so one section cannot hold both domains -- the adoption committed
and the next checkpoint poisoned the shard. B3 now materializes
staging-owned runs post-fence, deterministically and idempotently, and
`resolve_committed` only opens, verifies, and pins them. Recovery replays
the same materialization, which is what makes a crash between the fence
and materialization recoverable rather than ambiguous. Runs partition one
per adoption, splitting every 65,536 ordinals, because two adoptions'
sequences differ by more than a section can span.
The run count is therefore knowable before anything is appended, so the
ceiling check is a refusal at revalidation rather than a poison after the
fence.
Payload kind is a matrix axis: all eight Wave B rows drive inline and
staged-projection, and the adoption outcome is read from staging's
counters rather than derived, so the fixture and the run remain two
independent derivations.
Contract review 2026-08-09-A records the interface changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGdH5V43XWnj1PdHqiPktQ
A delta layer's `through_shard_sequence` states that every frame from its
shard through that sequence is accounted for in the index. Skipping the
layer when the delta was empty left a group that introduced no objects
advancing the committed sequence without advancing coverage, so the next
checkpoint refused: it read the highest layer stamp as the reach of the
index and concluded that pruning would drop objects that never existed.
Any transaction introducing no objects reached it. An empty layer is the
smallest honest way to record that the sequence happened and carried
nothing, so `with_subtree` now installs one whenever the publication
advances the shard's committed sequence. A maintenance publication
appends no frame and still owes nothing.
`coverable_through` had the same gap from the other side: it stopped at
the first layer whose entries did not resolve as a segment or an active
tail, which excluded adopted projection artifacts. Those are the most
durable entries in the index -- not in the journal at all, and carried
forward by every successor generation -- and were read as the least.
The empty layer counts toward layer fan-out and may seal into a
zero-entry run. That is the bounded-run model working as designed and is
the failure-safe direction: a stamped empty layer costs a run slot, an
unstamped one costs the ability to checkpoint at all.
Asserted with a plain object-less transaction rather than through the
adopting path that found it, so the regression names what actually broke.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGdH5V43XWnj1PdHqiPktQ
D3 adopts a staged projection through the same eight Wave B locations an
inline transaction reaches, and it fails differently at each. Giving adoption
its own failpoint rows would produce a parallel list that drifts from this one
the first time a location is added to either, so payload kind becomes a third
axis alongside the location the failpoint names and the action the driver
chooses - the same independence `actions` already documents.
The adoption expectation gets the two-derivation treatment the recovery
outcome has. The fixture states it, `PhysicalStateClass::adoption_outcome`
derives it, and the matrix requires them to agree. The rule is physical and
nothing else: whether a frame binding the artifacts exists on the device. No
bytes means the artifacts are unreferenced and reclaiming them is correct; a
fenced frame binds the manifest, so the pin outlives the process; fenced and
published means the writer already settled `Adopted` against a root that
references them.
Note the asymmetry against `required_outcome`. `PartialFrame` and
`WholeFrameUnfenced` are `AbsentRetriable` there and `TransferredToRecovery`
here, because bytes are on the device and only recovery may say what they mean.
A transaction that will not commit and a pin whose artifacts may be referenced
are different questions, and collapsing them is how a retriable refusal would
come to delete content.
Both derivations run on every submit row now, before any row lists
`staged_projection`. That is deliberate: turning the kind on becomes a matter of
listing it rather than of also getting the expectation right in the same commit.
Verified by stating `DefinitivePreAppendFailure` on `DuringCommittedRootBuild`,
which the class derives as `TransferredToRecovery` - the assertion names the row
and says what settling it that way would license.
No row lists `staged_projection` yet and nothing drives one, so this commit adds
no coverage claim. D3 does not merge without the adoption cases running.
`inline` is required on every submit row rather than merely allowed: it is the
payload the eight locations were characterized with, and a row that dropped it
while adding the new kind would move coverage sideways while reading as having
added some.
check-phase1.sh reports GATE_EXIT=0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`store-bench` opened its measured store with `ENGINE_MAX_INDEX_RUNS =
1_000_000` against a store default of 64, carrying the comment "raised because
index-delta sealing is unimplemented". Sealing landed in fef8520, so the reason
had lapsed and two things followed that did not follow before: the emitted
`index_run_ceiling` named a reason that was no longer true, and at an
effectively infinite fan-out ceiling no seal in a measured run was ever
triggered by fan-out pressure, so `index_maintenance: runs_sealed` was earned by
entry pressure alone.
The raise is dropped rather than the enum renamed, so `bench/result-schema.json`
is untouched and no contract review was needed. The store opens at its own
default and the derivation reports `store_default` from the comparison it
already made.
Demonstrated rather than assumed, because the whole question was whether a run
survives the ceiling the raise existed to escape. A 45-second submit-path run at
`max_index_runs = 64` published 5,192 groups - 81x the ceiling - with no
`NotImplemented` refusal, and its rate was flat against a 5-second run at the
same ceiling (113.2/s vs 117.7/s). Both figures are debug-build and diagnostic
hardware and are evidence of survival, not of throughput. The emitted bundle
declares `index_run_ceiling: store_default` against a configured ceiling of 64,
with `index_maintenance: runs_sealed` alongside.
The raised branch of the derivation does not become dead code. The existing
fixture keeps covering it and `a_run_at_the_store_default_declares_store_default`
covers the branch a real run now takes, because a comparison needs both sides
exercised. The constant survives as a test-only value under a name that says so,
rather than being deleted and leaving the branch reachable only from a
configuration nothing produces.
The rationale block above the run is corrected too: it counted two of B1's
deliverables as bounding the bundle when only checkpointing still does, and it
still described the ceiling it no longer raises.
check-phase1.sh reports GATE_EXIT=0; verify-store-recovery.sh --cycles 2 reports
matrix=pass, bundle=schema-valid, acknowledged_loss=0, torn_transactions=0,
VERIFY_EXIT=0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`verify-store-recovery.sh` looked for `store-crash-driver` and `store-bench`
under `$repo_root/target/debug`. `CARGO_TARGET_DIR`, a `build.target-dir` in a
config.toml, and a shared workspace target all move that, and on a host that
sets one the script builds successfully and then exits 70 saying the driver
"was not built" - which reads as a compilation failure and is not one. It asks
cargo now.
The working directory deliberately does not follow. It holds crash roots whose
filesystem is part of what the campaign measures, which is why the script
already refuses tmpfs; a shared target directory may be on a different one. The
distinction is commented so the remaining `$repo_root/target` is not read as a
missed substitution.
Two scope claims were behind the code and one of them undersold the work.
Section 7 said the harness satisfied two of four run conditions. It satisfies
three: index sealing earned `index_maintenance = runs_sealed`, derived from the
`IndexRun` files read back off the device. `checkpointing` has moved from
`unimplemented` to `enabled_not_reached` - the mechanism exists and short runs
do not accumulate enough work to trip it - so what remains is a run long enough
to take a checkpoint, not a mechanism to build.
Section 6.6 item 5 said the emitter wrote a schema-invalid bundle. That closed
with 062797d. The passage is kept rather than deleted because the mechanism is
the point: the gate was red for a schema the store satisfied, since three
`store-bench` unit tests validate the emitted bundle against the schema rather
than against substrings. That is schema conformance living inside the gate,
which is what review 2026-07-24-B was after.
New carry-forward: `ENGINE_MAX_INDEX_RUNS` outlived its reason. It is 1,000,000
against a store default of 64 and still carries the comment "raised because
index-delta sealing is unimplemented", but sealing landed in fef8520. Two
consequences follow that did not before - the emitted `index_run_ceiling` names
a reason that is no longer true, and at that ceiling the fan-out trigger never
fires, so `runs_sealed` above is earned by entry pressure alone. It is left as a
decision rather than taken: dropping the raise needs the measured run to survive
the real ceiling, and renaming the value is an amendment to the lead-owned
schema, whose own rule is to request it rather than emit it.
Verified on this box at b4e4c7e: `check-phase1.sh` reports GATE_EXIT=0, and
`verify-store-recovery.sh --cycles 2` reports matrix=pass, bundle=schema-valid
on both paths, acknowledged_loss=0, torn_transactions=0, VERIFY_EXIT=0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
B1 deliverable 8. `StoreEngine::snapshot` and every `RepoSnapshot` accessor
were the frozen D0 signatures returning `NotImplemented`; they now capture one
committed root and answer from it.
The frozen signature could not express an absent repository, so contract review
2026-08-07-A adds `StoreError::NoSuchRepository`. Reusing `Conflict`, `NotReady`
or `UnrecognizedLayout` would have made the error a false statement about what
happened and left a caller unable to distinguish it from a genuine instance of
that condition. It is an inability to answer and not a lifecycle state, which is
the distinction the taxonomy turns on: a namespace never bound has no
`RepoState`, so there is no genesis authority to report and none can be
manufactured without fabricating a trust root. A namespace that is bound and
retired is the opposite case, and it captures normally - refusing both would
erase a difference the store knows. `RepoSnapshot` therefore gains `lifecycle`
and `storage_mode`, without which a reader cannot tell an active repository from
a deleted one.
Isolation is structural rather than checked. `IndexKey` has no constructor that
omits a namespace, so the only key `locate` can build is one scoped to its own,
and there is no branch a later edit could invert. An undefined object type code
is `Corruption` and not a miss: the entry was written by this store, so a code
no version of it ever assigned means the run behind it is damaged, and reporting
that as absence would hide it.
Capture is two `Arc` clones and a hash lookup, and holding the root is what pins
every generation behind the locations it can return - a reader cannot be handed
an offset into a segment deleted before it reads. `Debug` is hand-written, since
a derived one would render the whole index into any log line that formatted a
snapshot.
Deliverable 8's acceptance is amended, and the reason is that the design already
succeeded. "A test that fails if someone clones" assumes a clone is a copy;
`CommittedRoot` is entirely `im` persistent structures, so `(*root).clone()`
allocates zero bytes and so does cloning the index. Both were tried as the
negative control and both read zero. The test keeps a measured figure asserted
at exactly zero, which catches materialization, and adds `Arc::ptr_eq`, which
catches the copy the figure cannot. A live control proves the meter moves.
Four integration tests assert the same property through `open`, `submit` and
`snapshot` rather than against a hand-built root - charter item 8. Their two
repositories are co-located on one shard deliberately: separate shards write to
separate journals and separate index deltas, so isolation holds there by
construction and a namespace-blind lookup would still pass. Verified by giving
`locate` a namespace-blind fallback, which fails the isolation assertion at both
levels.
Recorded and not acted on: `segment_generation` is per-shard, so the same
generation and offset pair occurs in every shard's journal. Not ambiguity - a
location is only read through a snapshot, whose namespace determines the shard -
but it means a cross-shard location comparison asserts nothing.
Deliverables 1, 3 and 7 remain. The ignored staging test's blocker list is now
stale in B3's file and is left for B3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
B3 deliverables 6, 7 and 8. Each was an explicit NotImplemented stub whose
recorded blocker had lapsed; together they take a sealed session from a pin
nobody could hold to one recovery can resolve after the process that held it
is gone.
Finalize takes an adoption pin, and the state machine the deliverable
described could not be built as written. The pin is taken from Sealed rather
than Open, because the sealed manifest is what an adopter revalidates against.
A definitive pre-append failure returns the session to Sealed rather than
Open, because reconstruction reads that manifest's presence as the seal's own
commit point and would hand back Sealed on the next restart regardless -
returning a session to a state no restart can reproduce is the defect the
private busy/state split exists to prevent. Adopted is durable and is a state
the wording had no name for: adopted artifacts stay in staging/ with a
committed root pointing into them, so dropping the session record would make
the next reconstruction read the directory as an abandoned materialization and
reclaim committed content. The pin is made durable before it is issued; a
handle backed by an in-memory flag is an adoption capability with nothing
behind it.
Cleanup proves absence of reference, and absence is not enough on its own. A
sealed session a client is still finalizing and a pinned one whose frame may
be mid-append are both referenced by nothing, so the candidate set is the
adopted sessions alone. The unit of removal is the session directory rather
than the artifact: a manifest names every chunk and reconstruction refuses a
sealed session missing any ordinal, so removing the unreferenced half of a
directory trades a bounded leak for a root that fails to open. The supplied
root must have reached the adoption - a root captured earlier references none
of these artifacts because it predates them, and acting on that deletes a
directory the current root points into.
Recovery resolves a committed descriptor against the bytes on disk, not
against staging's cached idea of them. Every declared ordinal is read back and
its digest bound to the sealed record, the manifest's ordered list, and the
frame's own artifact-set digest, so a valid replacement chunk of identical
shape is refused rather than indexed. It resolves whole or not at all. A
location names the whole certified record, so every object in a chunk shares
that chunk's location and a reader validates the artifact before extracting
from its decoded vector.
Three frozen seams move, each recorded as a contract review:
ProjectionAdoptionOutcome::Adopted and resolve_committed both gain the
adoption's committed shard sequence, from whichever side made the frame
authoritative; and checkpoint-style proof-bounded replacement earns a third
caller for sys::rename_replace.
Contract reviews 2026-07-31-A through -D. Scope 6.5 and plan 8 are amended
where they specified behaviour that could not be built.
Deliverables 6-8 are implemented and not yet operated: no production path
calls begin, finalize, or adopt_projection, and expire still has no scheduler.
Both carry-forwards stand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKzM69CHmBuDcA3qN1jFdh
Five review findings against 5462952.
Adoption was content-blind: it compared the path and the committed sequence,
so a different checkpoint at the right name was adopted and published. That is
silent logical deletion rather than a wedge — the manifest suppresses replay of
the frames the checkpoint covers, so an empty catalog at the live sequence
makes the namespace disappear at the next open with nothing reporting a fault.
Adoption now requires logical equivalence to the checkpoint this call would
have written, created_at_micros excepted, and refuses anything else without
poisoning, since nothing durable has moved. The test reopens and reads catalog,
ref and receipt back through the public surface.
Checkpointing did not relieve the replay ceiling. replayable_index summed every
sealed-run entry and every namespace regardless of the horizon, so a shard at
the ceiling checkpointed and was then refused its next single-object
transaction. The writer tracks what the newest checkpoint made unreplayable —
from recovery at open, from itself thereafter — and subtracts it. Subtracting
rather than dropping the accounting: runs sealed after a checkpoint cover
frames a reopen does replay. Removing the subtraction reproduces observed: 4,
allowed: 3.
checkpoint::install wrote through symlinks. Survivable while nothing production
could reach it; not once this dispatch gave it a caller. It goes through the
no-follow funnel now, and an occupied final name is type-checked before
adoption.
Checkpoint pins grew by a file per checkpoint while the manifest trimmed to
checkpoint_retain, keeping pruned inodes alive for the life of the process. The
successor pins exactly the rows its manifest publishes.
Refs and receipts are sorted before encoding, which the equivalence guard also
depends on.
Disclosed: the adoption success path has no test. Building an equivalent image
from outside the engine needs a failpoint seam or a test-only accessor, and
neither belongs here. The refusal path, which is the one that can lose data, is
covered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKzM69CHmBuDcA3qN1jFdh
`StoreEngine::checkpoint` was the frozen D0 signature that returned
NotImplemented. It now checkpoints every shard on its own writer thread and
returns a lease that really pins.
The ordering inside it is the correctness. The checkpoint format carries
catalog, refs and receipts and no object index, and advancing
committed_shard_sequence stops recovery replaying the frames it covers — so
entries that live only in the root's delta layers describe objects that are on
disk, named by a segment, and unreachable at the next open. Every layer through
the committed sequence is sealed into a run the checkpoint's own manifest names
and discarded from the root before the sequence moves; coverage short of that
is refused rather than partially applied.
CheckpointLease holds the exact Arc<RetainedGeneration> each shard published,
which pins segment and checkpoint descriptors and index-run mappings. Counting
retained generations cannot stand in for that: two later checkpoints prune the
generation the lease names.
The group boundary is structural rather than a lock. The checkpoint travels the
same channel as submissions and the writer loop publishes any open group before
running one, so it cannot advance past sequenced-but-unfenced frames.
Four edge cases, each tested: an empty shard is skipped rather than
checkpointed at sequence 0; a repeat with no new work reuses instead of
renaming onto its own name; a finalized but unreferenced checkpoint left by a
crash is validated through recovery's reader and adopted rather than wedging
the shard on a permanent EEXIST; and a journal holding no frame seals nothing.
store-bench found that last one by calling the entry point that had always
refused. It now earns index_maintenance from a measurement as well —
index_maintenance().unsealed_delta_layers where it recorded None for "no such
reading exists", which is the interface request its own guard had written down.
Acceptance: write below seal pressure, checkpoint, reopen, resolve every object
with the replay delta excluded as the answer. Both named mutations fail it on
reachability rather than bookkeeping.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKzM69CHmBuDcA3qN1jFdh
The test oracle probed generations 0..16 to discover what a published run
names, which reproduced the defect it was written to catch: it could not report
an identity outside the range it guessed. Review drifted the identity to 101 and
the test died on "a run with entries names some generation" before reaching
either the identity assertion or the damage assertion. It would also have picked
the lowest of several once a run spans generations.
The helper now takes an IndexKey the test knows is covered and reads
`segment_generation` off `IndexRun::get`, which answers exactly and has no
range to outgrow. It fails loudly if the key is absent, since a missing entry
means the test is measuring nothing.
The surviving-journal regression keeps a second orphan at generation 100, so the
fallback it must not take is 101 — a number that cannot be read as an
off-by-one. Verified by reverting the tail fix under it: the assertion now
reports left: 101, right: 1 instead of failing to find a generation at all.
No production change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKzM69CHmBuDcA3qN1jFdh
The sealing fallback was reaching a journal that seals nothing. A recovery
keeping an empty tail gave it the fallback's generation, but no TailRange
recorded the move — nothing was sealed — so the next open derived the identity
the manifest still implied while the session in between had appended frames
under the moved one and sealed a run over them. The guard searched published
runs for the derived generation, found none, and let the store open with the
run unresolvable. Review confirmed it: empty tail plus an orphan at 1, reopen
taking 2, frames and a run at 2, an orphan at 2, then an open at 3 with
`object_source(0, 2) == None`.
`RecoveryGenerations` carries a third number. `tail` is what a surviving
journal keeps and is always the tail's own identity: segment-name occupancy is
a fact about `segments/`, and a journal that seals nothing does not go there.
`logical` remains the sealing identity and still falls back under the guard.
That restores the invariant the split rests on — the active tail's identity is
always derivable from the manifest — so the guard and the frames are always
naming the same generation. A tail may keep a generation an orphan occupies;
the collision is only real when frames are sealed under that name, and it is
refused then, with the run in hand.
The regression follows the reported sequence and reads the run's generation out
of the run rather than assuming it, since which identity the frames ended up
with is the thing under test. Mutation-checked in both places it can fail: the
run drifts to generation 2, and with the mid-assertion relaxed the open
succeeds with None pinned there, reproducing the report exactly. The shared
helper now reports the run's own generation, so all three refusal paths fail
legibly rather than against a hard-coded number.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKzM69CHmBuDcA3qN1jFdh
The displaced-run refusal sat only on the fresh fallback. The resumable branch
returned before it, so an interrupted recovery that had already fallen back to
generation 2 resumed at 2 — correctly, since finishing an interrupted seal must
not orphan its artifact — and stranded a published run naming generation 1
exactly as a fresh fallback would, one crash later. Review reproduced it with a
run at 1, an orphan at 1, and a `.recovery-<journal>-2.prefix`: the store opened
with `object_source(0, 1) == None`.
`preferred` is now computed before either path can return, and both call one
closure keyed on choosing anything other than the identity the frames already
carry. A resumable artifact at `preferred` displaces nothing and the guard is a
no-op on it, which is its own test — a guard keyed on "a resumable artifact
exists" would refuse every interrupted recovery on a root that has ever sealed,
and that test fails rather than letting the over-broad version pass.
Both refusing tests share one assertion helper, so the paths cannot drift in the
tests either. Each guard call is separately mutation-checked: disabling the
resumed one reproduces the reported open with the run's source unpinned.
Contract review 2026-07-30-B amended with the finding and the general form of
it — the check belongs on the outcome, that the frames are being renamed, not
on the branch that produced the rename.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKzM69CHmBuDcA3qN1jFdh
Review of 2026-07-30-A declined the silent state it disclosed. When a `.seg`
occupies the logical generation the active tail carries, the frames must be
renamed, and the two cases part on what that costs:
- orphan alone: nothing names the displaced identity, the seal falls back to
a free generation, and recovery succeeds — an orphan segment is a state in
which the active journal is still the authority, and refusing would turn a
recoverable root into an outage;
- orphan plus a run the manifest names: recovery would publish a manifest
naming a run whose every location resolves to nothing. It refuses with
Corruption instead, naming the run.
Refusing is an outage on a root whose data is all present. It is chosen because
the alternative opens and lies, and because the replay delta above the run hides
that from every lookup until the first consumer that reads runs directly — which
is the checkpointer this precedes.
`IndexRun::references_segment_generation` is the frozen seam, read-only, with
recovery as its one caller (contract review 2026-07-30-B). Exact rather than a
range test over section headers: entries pack a 16-bit delta from the section
base, so the header says only what a section could name, and a `true` it does
not owe refuses a recovery with nothing to lose.
`coverable_through` keeps the coverage 2026-07-30-A widened — the unsound state
is now refused where it arises rather than designed around at every seal — and
its comment, which still described the pre-split world, says so.
Evidence: the refusing test asserts the damage rather than an expectation, so
disabling the guard reports that the reopened root pins None at the generation
the run names. A first draft of it passed for the wrong reason, re-pushing the
genesis object id as a blob so the reopen failed on a duplicate-object Conflict
either way; the mutation exposed that.
Still open and recorded in scope §6.5: recovery discarding a run whose covered
identity was not preserved, which turns this refusal into reclamation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKzM69CHmBuDcA3qN1jFdh
The prerequisite for checkpointing. `recovery_generation` was two numbers
wearing one name: the logical generation of the segment recovery seals, and
the generation of the manifest recovery installs. Every index run publishes a
manifest, so every seal moved the number — and the segment recovery then
wrote took the moved value while the frames inside it were already named by
the old one. Persisted index locations dangled.
Split, each answers its own question. The logical generation is the tail's,
derived from the manifest's committed prefix, so sealing a journal into a
segment changes where the bytes are and not what they are called. The
manifest generation is the next free one, so an index run's manifest and a
recovery's cannot collide. Collision detection moved with them: the old check
compared against a maximum mixing all three namespaces, which an index run
could raise on its own, and only a segment can collide with a segment.
This closes the coverage restriction the previous commit had to impose. A run
may again cover locations naming the active tail, because the tail's identity
now survives being sealed away — so sealing covers the frames of the session
that wrote them instead of lagging one behind. Mutation-checked by putting the
segment's identity back on the manifest counter, which reproduces the original
defect exactly: a recovered run pointing at a logical generation nothing pins.
One case is not closed, and a test found it rather than review. An orphan
`.seg` from an interrupted seal occupies a logical generation whether or not
it is a readable segment, and it may hold exactly the identity the tail wants.
`an_orphan_segment_leaves_the_active_journal_the_authority` is also the test
documenting why recovery must not refuse there — the active journal is still
the authority and an outage would be the wrong answer — so the seal falls back
to a free generation and renames the frames, as it did before. An index run
against the old identity then dangles. Confined to roots carrying an orphan
segment, where it was previously universal; the closure is for recovery to
discard runs whose identity was not preserved, recorded in scope §6.5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKzM69CHmBuDcA3qN1jFdh
Two findings against the previous commit.
The replayability check sat behind an early return for "no delta layers", so
the admission right after a seal skipped it entirely — and that is exactly the
admission where everything a reopen must rebuild has just moved into the run.
Seal three entries at a ceiling of three and the next submission was accepted;
the reopen then failed with `LimitExceeded`. Only the seal attempt depends on
there being a backlog now; the ceilings are checked every time.
The open-group test did not exercise an open group. `submit` is an `async fn`,
so its send happens on first poll, and awaiting one submission before starting
the next is two groups rather than one — the refusal followed from the
published root alone, and the test passed with the pending-group projection
removed. Both submissions are now in flight: the first is polled once so it
reaches `pending`, and the second is decided while it is still there. Verified
by mutation — zeroing only the `open_group` term now fails that test and
nothing else.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKzM69CHmBuDcA3qN1jFdh
Four findings against the previous commit. Each fix carries a regression that
fails against the landed code, and each was mutation-checked back to it.
Admission projected nothing. The replay-ceiling check read the published root,
so it decided about a transaction it had not counted: one object present, a
two-object transaction against a ceiling of two committed and the store then
failed to reopen. The open group was the same hole one step along. Admission
now projects sealed runs, unsealed layers, the open group, and the incoming
transaction.
The byte ceiling was unguarded. Recovery rebuilds into one delta that refuses
on either ceiling, so narrow frames across namespaces passed admission and
failed to reopen on `max_active_index_bytes`. Both are checked, and the
projection counts namespaces because the encoding pays a section header per
namespace. `index::encoded_bytes_for` is that arithmetic extracted, so this
file does not carry a copy of the encoding's shape.
A recovered run's locations did not resolve, and this reshaped the slice. An
`IndexLocation` names a logical generation, and a run is the first thing here
that persists one across a session — sound only for a generation that is
stable, which is a segment's alone. The active tail's is assigned from
`max(manifest, .seg, .idx) + 1`, so it moves whenever any artifact appears
(the run's own manifest suffices), and recovery seals a journal holding
frames at that counter rather than at the generation the tail had. The
previous reopen test could not see it: its lookups were answered by the
replay delta shadowing the run. Coverage is now an oldest-first prefix of
layers whose every entry is segment-backed, which makes the broken run
unwritable rather than untested. The cost — sealing lags one session behind
until frames leave `active/` — is recorded in scope §6.5.
Preserving the tail's generation across the seal was attempted and withdrawn.
`recovery_generation` is at once the new manifest's generation and the sealed
segment's logical generation, so the real fix separates those two numbers in
A2's recovery core, and that belongs with checkpointing rather than inside a
B1 integration commit. The first attempt also targeted the wrong branch: a
journal holding frames is replaced, not kept. `active_tail_logical_generation`
is left extracted at the one path that already used that formula so the two
ways of numbering an active tail are visible together.
The run ceilings counted every shard, where recovery enforces them against one
shard's manifest — a four-shard root with `max_index_runs = 1` refused the
second shard its first run. Counted per shard now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKzM69CHmBuDcA3qN1jFdh
A shard now seals its accumulated delta into an `IndexRun`, publishes it
through the manifest, and discards exactly the layers that run covers in one
committed-root CAS. This replaces the `NotImplemented` that refused a shard
once it had accumulated `max_index_runs` delta layers.
The seal runs at admission, before anything is reserved or sequenced, because
a seal that fails partway has to poison and poisoning a shard that has just
accepted a transaction owes that caller an answer it can no longer give. Two
triggers: `DeltaPressure::SealRequired` over the accumulated entries and
bytes, and the layer count, which is the fan-out every lookup pays before it
reaches a run and which entry pressure alone does not bound.
Everything up to `install_index_run` is pre-durable and fails as an ordinary
error; from there the shard poisons on any failure, including one the call
may have made before writing. The caller cannot distinguish those, and the
conservative direction is refusing to keep writing against a root that may no
longer describe the device. `poison_now` latches it — `poison_error` only
built the error, which is right inside the publication window whose caller
latches for the whole group, and wrong here.
Four frozen amendments, all recorded as contract review 2026-07-29-C:
`segment::install_index_run` holds the whole durability sequence so no
durability operation lives in `engine.rs`; `index::delta_pressure` becomes a
free function so the writer's multi-layer backlog asks the same watermark
rather than restating it; `CommittedRoot::merge` recognizes a publication
that appends no frame, without which the run reaches the manifest and never
the root; and a manifest may have an empty retained tail when it commits
through zero, which is every shard that has sealed an index but not yet
rotated its journal.
A finding, recorded in scope §6.5 rather than papered over. Sealing moves
entries out of the layers but no frame out of `active/`, and the committed
prefix advances only on a checkpoint or a rotation — so recovery still
replays everything into one ceiling-bounded delta. The writer therefore
refuses once the replayable set reaches `max_active_index_entries`, closing a
hole that pre-dates this change: the old layer cap never bounded the summed
entries behind it. The consequence is that an entry-pressure seal lands
exactly on that ceiling and the next admission is refused; only a fan-out
seal leaves the shard able to continue. Entry-pressure sealing becomes useful
when `checkpoint()` can advance the prefix.
Mutation-checked in both directions. Reverting the discard reports one run
beside a three-layer backlog where the test requires zero, so the frozen
`IndexMaintenanceSnapshot` is load-bearing. Reverting the `merge` amendment
reports zero runs beside a three-layer backlog while `CURRENT` names the run:
the silent divergence, arriving quietly.
`store-bench` is untouched and stays preliminary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKzM69CHmBuDcA3qN1jFdh
The last three hazards of contract review 2026-07-28-D, closed together
because they are one invariant: no name the store *invents* beneath a root
may be reached through a link or resolve to an object of the wrong type. The
flags and descriptor checks are the safety property, so the mechanics go in
the `sys.rs` funnel where the next author looking for how this crate opens
files will find them.
Refusal semantics differ per name, because the names mean different things.
`write_fenced` adopts and empties an existing regular `.tmp` — that is
residue from an interrupted attempt at this exact write, and reusing the name
is how a retry works — which is why the open and the truncation had to be
separated: no flag combination truncates only regular files, so the type
check needs the descriptor first and `O_TRUNC` cannot be in the open.
`read_format` keeps three answers apart: absent stays `Io(NotFound)` because
startup states 1 and 3 depend on it, non-regular is `UnrecognizedLayout`, and
a corrupt regular marker keeps its decode error. `initialize_root` leaves the
caller's root and ancestors alone, adopts existing directories beneath it, and
classifies every planned entry before creating any missing one so a refusal
cannot half-extend the tree it refused.
Directory fences now go to descriptors already validated rather than
re-resolving the name, which would hand the fence to whatever the name
resolves to now instead of what was checked. The fence sequence is otherwise
identical on purpose: `engine.rs` asserts the count exactly.
Each protection was reverted independently and the witnesses observed:
- `write_fenced` — a 4096-byte file outside the root truncated and
rewritten through a live link, a file created outside the root through a
dangling one, and a fifo at the name blocking the open for the full
ten-second deadline: an unbounded startup hang from one `mkfifo`.
- `read_format` — a foreign `FORMAT` read in full, its `shard_count` and
`root_uuid` returned as this root's, so every file in the tree would then
be validated against a marker the store never wrote. Same hang on the
read side.
- `initialize_root` — returned `Ok(FormatMarker)`, reporting a working
store with its shard tree built outside the root. The preflight has its
own witness: `shards/00/active` left behind by a refusal that named
`shards/00/segments`.
The witnesses sit on the `segment` entry points. `StoreEngine::open`'s
classifier refuses a redirected root before any of this is reached, so a test
entering that way passes whether or not the protection exists — and
`RecoverySession::open`, `drive.rs` and `store-bench` all arrive without it.
Disclosed: the device-node residual now covers `FORMAT.tmp` too — one
`O_NONBLOCK` open before the `fstat` refuses it, still gated behind `mknod`
privilege inside a configured root. `rename_noreplace` needed no change;
`RENAME_NOREPLACE` fails `EEXIST` on an occupied target whether or not it is
a link.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKzM69CHmBuDcA3qN1jFdh
Lead fix, folded in. The previous repair accepted any `#[cfg(test)]` item whose
first line ended in `{`, which is the same latch in another spelling: a
semicolon-terminated `static` or `const` can open a block initializer there and
close with `};`, so the exemption ran past it into the next function. Verified
by reverting the condition — the scanner returned no offender at all for a
`std::fs::write` in the function following a `LazyLock` initializer.
Only `mod` and `impl` are accepted as braced shapes, being the two the crate
actually uses. Everything else fails the guard by name rather than being
bounded by a brace that may not be the item's own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKzM69CHmBuDcA3qN1jFdh
Two findings against the previous commit, both upheld.
The funnel guard's test exemption could still latch. Ending it at the next
column-zero `}` is right for a braced item and wrong for every other shape:
after `#[cfg(test)] use crate::test_support;` the first such brace belongs to
the *next* function, so all of it went unscanned. The scanner now reads the
attributed item's shape — braced items are exempt to their closing brace,
semicolon-terminated items exempt only themselves, and any third shape,
including an item header rustfmt split across lines, fails the guard. A shape
it cannot bound is not a shape it may assume is harmless.
It is now a function over `&str` with synthetic tests, which is the more
important half. Mutating real sources only probes the shapes those sources
happen to contain: no file in this crate has a semicolon-terminated
`#[cfg(test)]` item followed by production code, so no mutation of a real
file could have produced this defect. Charter item 8's analogue for tooling.
The two-owner claim was overstated. The regression arranges its wrong-typed
name by replacing `LOCK` under a live holder — and replacing it with a fresh
*regular* file succeeds just as well, since both opens are then of a regular
file at the right name with nothing to tell them apart. The type check closes
"the name already resolves to the wrong kind of object", the operator-error
and stale-state case; it does not close "the name is replaced under a
holder", and no check at this layer can.
So scope 3.1 now separates the two, says which is in scope, and states the
replacement case as an explicit deployment assumption rather than leaving it
implied: anything able to replace `LOCK` can equally unlink a journal, so
advisory locking was never the boundary that would stop it. The assumption is
pinned by a test asserting the current behavior on purpose — if a stable
locking object is ever adopted, that test is meant to fail, and the failure
is the signal that the documented assumption changed. §3.1 records locking
the root directory as the candidate and what it would cost.
Also exact rather than caveated: a Unix socket fails `open(2)` with `ENXIO`
before any `fstat`, so it surfaced as `Io` while the documentation promised
`UnrecognizedLayout`. `ENXIO` and `EISDIR` both now mean "not a regular
file", and the socket is one of four occupants the test loop covers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKzM69CHmBuDcA3qN1jFdh
`--fail-ack-append-after N` states that the run met an acknowledgment-journal
failure at append N. A run that ended before reaching N — too short a window,
too few groups, a store that refused every submit — met none of it, and still
emitted an ordinary bundle: the flag was on the command line, the failure
never happened, and nothing in the output said so. Same class of defect as
the ack write failure this flag exists to induce, one level up.
So the arming is treated as a claim and `fired` as the counter, compared at
run completion on both seams, before any total is read. The negative control
is the load-bearing half of the test: a check that refused every armed run
would satisfy the refusal assertion, so the same seam runs with a reachable
target and must still report the *injected* failure rather than the arming
one. The two refusals are asserted to stay distinguishable.
The usage statement said "emits no bundle by construction", which was only
true when the target was reachable. It now says so unconditionally, which is
what the code does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKzM69CHmBuDcA3qN1jFdh
`flock` locks the inode a descriptor reached, not the name that was asked
for, and `lock_root` opened `<root>/LOCK` with `create(true)` — a
follow-through open. A symlink at that name therefore put the root lock on
a foreign inode and left the root's own lock file unlocked, so a second
process arriving at the same root took the lock as well: two owners, each
believing it held one root exclusively. Nothing is destroyed and everything
downstream is permitted to race, which is why this is scheduled on its own
rather than folded into the remaining symlink work.
`classify_root` already refused a non-regular `LOCK`, but only on the
`StoreEngine::open` path. `RecoverySession::open` and `drive.rs` reach
`lock_root` directly with no classification ahead of them, so the guarantee
had to move down to the open itself.
`sys.rs` gains a third no-follow primitive for the shape the other two do
not cover — a name the store must own and keep, adopting an existing regular
file and creating an absent one, never truncating. The type is established
by `fstat` on the descriptor already held, before `flock` is attempted, so a
refused name is never locked even momentarily. A non-regular occupant is
`UnrecognizedLayout`, not `AlreadyLocked`: the root is malformed, not busy.
Measured on a reverted copy, three distinct failures rather than one:
- two owners of one root, with the first lock still held;
- a dangling link at `LOCK` created a file outside the root;
- a fifo at `LOCK` returned `Ok(RootLock)`, the store reporting that it
held the root lock on a pipe. That one was found by writing the test
for the type check, not predicted.
The funnel guard needed amending to accept these tests, and the reason it
did is a defect in the guard: it exempted test code by matching the literal
name `mod tests`, so the two modules named otherwise were scanned as
production code while a file could have evaded the guard entirely by naming
a module `tests`. It now keys on the `#[cfg(test)]` attribute and, unlike
before, the exemption ends at the module's closing brace — code appended
after a test module used to be unscanned. Both directions mutation-checked.
Disclosed, not closed: a device node at `LOCK` still receives one
`O_NONBLOCK` open before the `fstat` refuses it. The three remaining
symlink hazards in `segment.rs` stand unfixed; contract review 2026-07-29-A
records both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKzM69CHmBuDcA3qN1jFdh
A bundle's verification flags were assertions about methodology that
nothing checked. This makes them conditional on machine-readable
declarations of what the run actually did, and fixes five cases where the
emitter stated something it had not established.
bench/result-schema.json gains a required run_conditions block of ten
closed enumerations -- initialization and mutation path, checkpoint and
index state, index-run ceiling, receipt reconciliation, objects_new
source, commit-id uniqueness, build profile, environment fidelity. It is
not disclosure beside the claims; it is what the claims are conditioned
on, so a harness can only assert what its declaration permits. A prose
caveat field was rejected: free text is not a condition a consumer can
check, and a bundle whose caveats live only in a report reads as
unconditional to everyone who receives it.
The branch conditional forbids the three newly earnable claims on the
journal-drive path and pins its four provenance declarations to the only
values that seam can make. Forbidding the claims alone left the hole one
field over -- a drive bundle could otherwise declare exact receipt
reconciliation it has no receipts to perform.
Emitter defects, each found by reading the schema against the code:
- Setup traffic was inside the measured interval. Both counter baselines
were read only at the end, so repository creation -- which goes through
submit, and therefore fences and signs -- was counted as measured work
while the bundle asserted setup_traffic_excluded. A false exclusion
claim is worse than a wrong number: a wrong number invites scrutiny
and this deflects it.
- A zero-work run produced a schema-valid bundle asserting uniqueness
over zero ids and three-objects-per-commit over zero commits. Both are
vacuously true, which is why they must not be earnable that way: the
result is indistinguishable from a measured run by the consumer the
schema exists to serve. Refused by name at two altitudes.
- An ACK-journal write failure ended the run quietly. It set a stop flag
without recording a refusal, so neither the fatal guard nor the
zero-work guard saw it, and the bundle omitted a committed transaction
while still counting its fence and its signature -- one counted
transaction against two fences and two signings. It is now a fatal
incomplete-accounting refusal carrying the original errno, because the
commit happened: folding it into the refused count would report a
transaction the store committed as one it declined.
- Widening that class to "a failure that produces a value nobody read"
found three more. A shard with no counters summed to zero fences,
silently shrinking the total that bounds every durability claim. A
digest of an unreadable file returned the digest of empty input -- a
well-formed 64-hex value indistinguishable from a real one, feeding
five attested fields. An unreadable /proc/meminfo published one byte
of RAM. All three refuse now.
- Index steady state was inferred from any directory entry, so one stray
file declared the index sealed. Entries are parsed back as index runs
against the root's own uuid; an unparseable entry is reported as
unvalidatable rather than lowering a count, and a backlog is refused
because neither named value describes sealing that did not keep up.
deployment.tmpfs, persistent_data_mount, and hardware.filesystem were
constants -- the emitter could assert deployment facts it had never
checked. They are read from /proc/mounts now. Both deployment fields relax
to booleans so a diagnostic run is representable at all: it was previously
not disqualified but unencodable, and a schema that can only express
successful runs is not a record of what was measured. outcome=pass
requires reference fidelity at every gate, and a diagnostic run may never
carry a pass verdict.
Hardware profiles are derived, never accepted. store-bench parses the
whole frozen profile tables and names a profile only by exact comparison,
iterating every pinned fact rather than every supplied one -- so a fact
the emitter does not model eliminates the profile instead of being
invisible. That took the honest unobserved list on this host from 7 facts
to 24, which is the inversion working. A deployed-node harness supplies
privileged facts as evidence to compare, never as a label. The outcome
derivation now also requires the checkpoint and index conditions, because
fidelity was the only thing preventing a pass and would have stopped being
so the moment profile recognition started working, at which point the
emitter would have produced a pass its own validator rejects.
operation_receipts_reconciled is expressible and deliberately not emitted:
the bench accepts any Committed status without comparing the payload, and
the digest it records is of the operation id rather than the receipt.
Contract review 2026-07-28-C records the amendment and the ceiling it does
not close: run_conditions is self-reported, and only the index-run ceiling
is cross-checked against an independent value. Scope 5.1 records why a
full filesystem is indistinguishable from a concurrency flake by symptom,
and that an I/O error must reach a report with its errno intact -- the
same requirement as the incomplete-accounting refusal above.
scripts/check-phase1.sh GATE_EXIT=0; verify-store-recovery.sh reports
bundle=schema-valid on both paths, zero_work_run=refused, and
unaccounted_ack_run=refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Scope 6.4 deliverable 1, startup state 1. StoreEngine::open now creates a
root rather than refusing to, and every harness that measured a store
production did not build is re-pointed at it. States 3 and 4 still refuse.
The four startup states are distinguished by one read-only classification
that creates nothing. A FORMAT entry means state 2 on the strength of the
name alone, because a FORMAT that does not decode is still FORMAT and
treating an unreadable marker as "no marker, therefore empty" would
authorize building a fresh tree over a populated root. A root holding only
LOCK is empty, since lock_root creates that file as a side effect of asking
whether the root is busy. The signer check moved above every root access,
because once an absent root is initialized rather than refused, a signerless
configuration would otherwise create a tree, write FORMAT and fsync the
parent before failing -- a configuration error must not leave a root behind.
Three defects found in review, all fixed here rather than deferred.
A deeply absent path was created with create_dir_all and only its immediate
parent fenced, so open could report success over ancestors a power loss
could take. Ancestors are now created one at a time -- so what this call
created is exactly what it fences, and a racing creator surfaces as
AlreadyExists rather than being absorbed -- and fenced deepest-first, since
a directory entry lives in the parent that names it and the reverse order
can leave a fenced parent naming an unfenced child.
An interrupted initialization was unrecoverable: any tree residue classified
the root as non-empty-without-FORMAT and it was refused forever, with a
message about legacy layouts that had nothing to do with what happened. A
root being built now carries an INITIALIZING marker, installed by rename as
the first durable act and removed as the last, so a durable partial tree
always has a durable marker beside it and every crash window is resumable.
Recognizing it requires both a byte-exact marker this crate alone writes and
every entry in the root drawn from a closed set of names this store
invented, so a foreign layout cannot be mistaken for abandoned
initialization and overwritten -- the direction that matters, since refusing
a resumable root costs an operator time and overwriting a real one costs
their data. Sibling staging with atomic installation was the alternative and
is structurally blocked: LOCK lives inside the root, so the root must exist
before any mutation can be serialized, and renaming a tree onto a directory
containing LOCK fails ENOTEMPTY.
The classifier then ignored INITIALIZING.tmp by name regardless of type or
contents, and initialization opened that name with create plus truncate. An
operator's file there was destroyed silently, and a symlink there truncated
a file outside the root to 27 bytes and then removed the link -- destroying
data the store never owned and erasing the evidence, while open returned Ok
and reported a working store. The justification for ignoring the name was
that only this path could have written it, which is circular: that is the
claim the classifier runs in order to establish. Every entry is now judged
by lstat type before anything opens it, the temporary marker is validated as
an exact regular marker or refused, and installation is create-new rather
than create-truncate. Contract review 2026-07-28-D records the two no-follow
open primitives this added to the frozen sys.rs, and the four further
symlink hazards in segment.rs that are recorded rather than fixed -- the
first of which lets two processes believe they hold one root lock.
A fifo at that name made the pre-fix open block forever: one mkfifo in a
configured root was an unbounded startup hang, not only a data hazard.
The engine and the drive seam are now asserted to recover one crash image
identically, closing a gap that was true by construction and untested.
Charter item 8 applied to the harness: the in-crate test helper no longer
calls segment::initialize_root, so every writer test builds its root through
open; the ROOT_SEEDED_BY_NON_PRODUCTION_PATH disclosure is retired; and the
fixture's root_seeded_by becomes a stable token matched by exact equality,
with the history moved to an adjacent reason field -- a substring match
passes on a value that has drifted to mean something else.
The d0 contract test asserting open returns NotImplemented for any valid
configuration is obsoleted by this deliverable and replaced with the
stronger property: a signerless configuration is refused and leaves no root
behind. It moves off a fixed /tmp path, which under the old check ordering
would have created a real store root on every gate run on every machine.
scripts/check-phase1.sh GATE_EXIT=0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
B4's harness measured intermittent AlreadyLocked on reopening a store it
had just dropped -- up to 29 retries over 147ms, 7 failures in 40 runs,
reproducing on a single-shard engine. StoreEngine::drop was not the cause:
it closes every channel and joins every writer, and looked correct in
every single-process test.
An flock is held by the open file description, not by a descriptor. A
concurrently forked child transiently inherits that description, and
FD_CLOEXEC closes it at exec, not at fork -- so during that window the
parent closing its own descriptor releases nothing. Scope 3.1 makes
AlreadyLocked a refusal and never a wait, so every consumer that closes
and reopens a root -- a recovery drill, an in-place restart, the Phase 2
migrator -- could be refused with no defined retry.
Releasing must therefore be an explicit act. lock_root returns an RAII
RootLock that issues LOCK_UN in Drop before the file closes, and both
owners -- RecoverySession and ShardDrive -- hold that guard. The unlock
lives in sys.rs beside try_lock_exclusive so the locking syscalls stay in
the one funnel. StoreEngine needs no change: it holds a RecoverySession,
never a File, so it inherits the fix.
Drop is the only release path. A release() and a file() accessor were
written and deleted before landing: neither had a caller, and an uncalled
second way to release a lock is exactly the decoy the charter names. A
failed LOCK_UN in Drop cannot be returned and must not be swallowed, so it
increments a counter the regression asserts unchanged.
The rejected alternative was fixing this in StoreEngine::drop alone. That
leaves ShardDrive and the drive's one-shot session exposed and makes
correctness depend on a descriptor lifetime that fork can extend.
The regression is synchronized rather than timed: the child forks while
the lock is held, signals ready on one pipe, and blocks on a second until
after the parent has released and attempted its reopen, so the inherited
descriptor is provably open across the whole window and the reopen is
asserted on its first attempt. With the explicit unlock removed it fails
10/10; as landed it passes 40/40. Measured under load -- 200 close-reopen
cycles against 72,255 concurrent forks -- 0 refusals, worst case 1 attempt
and 10.8ms; the same load kills the pre-fix behaviour within 0.05s, so the
load reproduces the defect rather than merely being weak.
Contract review 2026-07-28-B records the amendment. Two carry-forwards are
recorded in scope 6.6: the verify-store-recovery SIGKILL cycles still drive
the journal seam rather than submit, so kill -9 never lands inside a real
publication and the acknowledged-crash-recovery criterion is only partly
earned; and B4's bounded reopen retry must become a one-attempt assertion
now that the defect it compensates for is gone.
scripts/check-phase1.sh GATE_EXIT=0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Scope 6.6 deliverables 1-3, partial. This is evidence, not a freeze: no
Wave B deliverable set is complete and the throughput figure below is not
a publishable bundle.
The revert-and-observe-red acceptance for cd37f8b's two generators was
never actually performed -- the tests asserted it in their doc comments,
which is a claim. It is performed now, in a throwaway copy, one mutation
at a time, and each reddens exactly one test for the right reason. With
segment frame validation reverted to pushing footer offsets straight into
the adopted set, the store still opens and recovery still completes: it
reports recovery_ok=true and adopts the corrupted frame as authority, so
the red is exit 0 where 65 is required rather than a store that failed to
open. With the shard-index check removed from journal binding, the moved
journal is adopted whole and root_uuid alone does not catch it -- the Wave
A blocker reproduced.
The eight Wave B failpoint rows now drive StoreEngine::submit in-process
with the full six-field expectation asserted against the frozen oracle,
field by field rather than by struct comparison. No pending-wave-b row
remains. The four fields Wave A could not reach come from four independent
observations: the store naming itself poisoned, the two-root status read,
a receipt obtainable at all, and a different transaction submitted to the
same shard before any reopen. The last two are not the same question -- a
shard can be unpoisoned and still refuse a later append because its writer
thread died, which is exactly what phase-aware panic ownership fixed and
what this now checks from outside. AfterRootCasBeforeWaiterWake is
observed as a genuinely hung submit whose receipt is still retrievable,
so a waiter that never wakes is proved to be a hung request rather than an
absent transaction. Every row also asserts its group's fence count against
the public durability snapshot.
Two flake campaigns were measured rather than rerun: 4 failures in 40, then
7 in 40, from two distinct causes. One is a finding -- publishing a group
adds an index delta layer and none are sealed, so submit refuses after
exactly max_index_runs publications for the life of an engine. Both fixed
structurally; 200/200 and 40/40 after.
store-bench emit-skeleton now defaults to the submit path, with the journal
seam retained under --path drive for comparison. The signer is real, the
ref CAS is evaluated by the sequencer, and objects_new is summed from the
store's own receipts rather than multiplied out of the transaction count.
Explicit blockers, retained rather than worked around:
- StoreEngine::open still refuses startup state 1, so the benchmark seeds
its root by a non-production path. Seeding a store off the production
path in order to measure the production path is the charter item 8
smell; the disclosure is recorded in the fixture, a const doc, and the
module docs, and a test asserts open still refuses so it cannot go
stale in the safe direction.
- P2 is blocked three ways -- checkpointing disabled, no steady state
under the index-run ceiling, and no warmup/repetition/trim protocol.
The rate emitted is a debug build on tmpfs, marked preliminary.
- The 100 SIGKILL cycles still drive the journal seam, so kill -9 never
lands inside a real publication.
- Four schema claims became earnable and are requested, not emitted;
bench/result-schema.json is lead-owned.
- Reopen after close needs a bounded, measured, reported wait, because
the root LOCK outlives StoreEngine::drop. Diagnosed since as fork/exec
inheritance of the lock file description; the fix belongs in the lock
primitive, and this wait is removed when it lands.
scripts/check-phase1.sh GATE_EXIT=0; verify-store-recovery.sh 100 cycles,
recovery_failures=0, acknowledged_loss=0, torn_transactions=0,
repeated_adoptions=0, bundle=schema-valid.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A narrow vertical slice of scope 6.4: StoreEngine::open through one
retained RecoverySession, every shard recovered into the initial
CommittedRoot, and one shard writer wired GroupBuilder -> append_group_and_fence
-> ShardSubtree -> CAS publication -> completion. This closes the Wave A
carry-forward by giving GroupBuilder a production caller; the group bounds
are asserted against the sequencer as fdatasync counts on the device, not
against the builder in isolation.
§6.3's two load-bearing orderings are enforced and asserted, not assumed.
Step 8 follows step 7 with an explicit release fence, so a reader observing
the status root without an entry cannot then read a committed root older
than the publication -- verified by mutation in both directions. Waiters
are woken only after publication, and failure at steps 9-10 leaves a
queryable receipt, because a fence that succeeded and a root that published
is committed.
Three P1 findings from the B2 review are fixed here.
The committed event took its actor from the destination signer rather than
the evidence, so a frame could be signed, fenced, and published and then
fail frozen mirror verification -- first observed by another instance, long
after the bytes were durable. The test could not see it because the signer
key and the evidence actor were the same constant; they are now
deliberately different.
The deadline was rechecked in prepare, before signing and before the group
idle wait, so a slow signer could cross the retry deadline and still
append. The rule is now evaluated again over the whole group immediately
before anything is marked Resolving. The structural half matters more than
the recheck: shard_sequence is no longer consumed at prepare time, so a
dropped member leaves no hole by construction. Repair is re-derivation from
a recorded pre-image through the one sequencing function -- re-sequenced,
re-chained, re-signed -- because signing covers a digest that chains
previous_event_digest, and patching a suffix produces a durable, correctly
fenced frame whose signature verifies against nothing. The test reads every
frame back and checks both the chain and the signature; receipts alone
would not catch a partial repair.
Panic recovery had one catch around the whole writer loop, so every panic
poisoned the shard and reported every waiter as poisoned. Waiters now carry
an explicit phase and an owned reservation. A pre-append panic is
definitively absent and leaves the writer alive, since a dead writer makes
later_append_allowed_before_recovery false whatever the error says; a
request that overflows a publishing group is rolled back before that group
publishes rather than reported as its member; and a panic after publication
still delivers every receipt.
Also: the duplicated object-type and ref-kind tables are deleted in favour
of the amended shared helpers; max_objects_per_transaction and
max_refs_per_transaction are enforced at submit, the entry point a consumer
calls; status occupancy records the newly published root rather than the
one it replaced; and durability_counters/operation_status_metrics return
snapshots only, never the live counters, since a durability claim whose
auditor can write to it is not evidence.
StoreEngine::open constructs the single ProjectionStaging under the held
session before recovery -- it is recovery's projection resolver -- and
holds it exactly as long as the lock.
The remaining deliverables refuse by name: startup states 1/3/4,
coalescing, terminal retention, index sealing, journal rotation,
RepoSnapshot, and checkpoint. This is not a completed B1 deliverable set
and not freeze evidence; the gate's pending-row check stays inert while
engine.rs still returns NotImplemented.
138 library tests, 27 staging tests, 1 intentionally ignored.
scripts/check-phase1.sh GATE_EXIT=0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Scope 6.5 deliverables 1-5: what a staging session is on disk, what it
costs, when it dies. The instance-layer half of plan §8 — identity,
policy, ProjectionCore, the v2 routes — is deliberately absent; B3 binds
and exposes the fields those checks key on and evaluates none of them.
Two findings from the B2 review shaped the result more than the original
deliverables did.
Restart was in-memory handle reuse. open() built a fresh registry and
never read the filesystem, so after a real reopen an existing session ID
was admitted as new -- and if materialization then found the old
directory, error cleanup could unlink a durable session. That is data loss
reachable from an ordinary restart plus one error. open() now scans,
validates, and reconstructs sessions, chunk indexes, and the whole
occupancy account before returning, so a reconstructed ID is occupied and
refused as a conflict long before the error path; and that path no longer
calls remove_dir_all. Either change alone closes the loss.
A session is final iff its directory holds a valid session record for its
own ID, installed by rename_noreplace over fenced, digest-checked bytes,
so the final name only ever appears atomically over complete data. A
directory without one is abandoned materialization -- a crash between
mkdir and that rename, unaccounted and unreferenceable. The two states
share no code path, and reclaimed abandonments count on their own counter
so they can never be read as aborts or expiries.
Global quotas were per-handle. Every open() built an independent registry
outside the root LOCK, so two handles admitted twice the global limit and
the atomic-insertion work bought nothing across them. Construction now
requires proof of the held root lock and refuses a second in-process
instance, making two accountants on one root inexpressible rather than
discouraged.
Also: bounds are enforced atomically with insertion under one mutex with
no read-then-decide path, refused as typed LimitExceeded or Overloaded and
never by eviction; the directory-sync test pinned two syncs when the first
session in a shard needs three, a counter assertion that encoded the bug;
cleanup now validates a whole directory before unlinking anything, rather
than discovering a surprise midway through destroying a live session; and
artifact I/O moved off the registry mutex onto maintenance workers, with
the calling thread asserted to hold no guard rather than documented not to.
Deliverables 6-8 are explicit NotImplemented naming themselves.
StagedSessionState omits Finalizing, so deliverable 6 will fail to compile
at exactly the expiry and abort sites that must learn about a pin.
Carry-forwards recorded in §6.5, not closed: no production path begins a
session, so the sealed-invisibility acceptance stays ignored with both
blockers named; and expire() has no scheduler, so session age is a bound
enforced when asked and never asked.
116 library tests, 27 staging tests, 1 intentionally ignored.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The B2 review of the first B1/B3 slice found five defects whose fixes were
not available to the packages that had to make them: each needed a change
to a surface those packages do not own. Rather than let them restate a
frozen fact locally, the lead amends the surfaces and the packages consume
them. Contract review 2026-07-28-A records all five with the weaker
alternative that was rejected for each.
- CommitEvidenceSigner::sign_event's parameter becomes signing_digest.
The message is SignedCommittedTransactionV1::signing_digest, which
binds key epoch and durability result around the canonical event; the
bare event digest is chain identity, not signature material. A doc line
was not enough: misuse is undetectable until after durability, and is
first observed by a mirror on another instance.
- TransactionEvidenceV1::actor() is new, exhaustive over all six
variants. A destination event's actor must restate the evidence's
source instance, and deriving it a second way store-side is what
produced the defect it closes.
- format::object_type_code becomes pub(crate) and recovery.rs's private
twin is deleted, so one table exists where three did.
- RefRecord gains from_target/target() in terms of RefTarget, with the
code table stated once per direction and an unknown kind refused by
value as CheckpointError::RefKind. Defaulting an unknown kind would
launder it into the next checkpoint within one interval.
- max_projection_objects is capped at MAX_CANONICAL_ITEMS and its default
lowered to it; max_projection_chunks likewise, and max_projection_bytes
against the transitive per-chunk allowance. The old default described a
projection no manifest could encode. Supporting a hundred million
objects requires a versioned chunked or indexed manifest design, not a
larger hostile-decode ceiling.
Both governing documents are updated where they now misstate a frozen
fact, including §6.4's claim that signing covers the event digest. §6.5
records the two carry-forwards from B3's first slice — production staging
use is incomplete, and expire() has no scheduler — and records that the
requested StoreEngine staging accessor is a pending amendment which must
not be a bare Arc<ProjectionStaging>, since a clone could outlive
EngineShared, survive release of the root LOCK, and keep serving a root
this process no longer holds.
No package file is touched: engine.rs, transaction.rs, and staging.rs
consume these in their own commits.
levcs-protocol 6 consumer tests, levcs-store 109 library tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Wave A review recorded that the crash matrix structurally could not
express its own first blocker: no failpoint corrupts a frame inside a
sealed segment, and none moves a journal between shards of one root. A
green matrix on a defect it cannot represent is the same trap as a helper
nothing calls, so the finding was carried forward rather than closed.
Add the two physical crash-image generators as a `damage` subcommand on
the crash driver, and two tests that drive them through the same
production `reconcile` path the matrix and the recovery script use.
- sealed-frame-corruption flips one payload byte in the first frame of
a segment the drive actually sealed and installed. The footer stays
structurally valid, so only frame verification can reject it.
- cross-shard-journal-movement relocates shard 1's active journal under
shard 0 of the same root. Root UUID validation cannot see this; the
journal header's shard index must be bound to the directory being
opened.
Both generators mutate a production-written image and fence the mutation;
neither synthesizes a footer, frame, manifest, or checksum, so a passing
test cannot be an artifact of the harness agreeing with itself. Each
source image is required to be unambiguous — exactly one segment or
journal — so the result does not depend on directory iteration order.
The assertions pin the refusal to its own cause rather than to any
non-zero exit: the frame case must fail on the digest recomputation and
the movement case on the shard binding, and neither may publish a partial
adoption result.
crash_matrix: 26 passed. scripts/check-phase1.sh green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The freeze commit cannot contain its own hash, so it is recorded here.
Wave A is frozen at 5ee9c6b78b with the gate
evidence measured at that commit, in both the plan and the scope document.
Restates what the freeze does and does not cover: the two carry-forwards are
B-wave work and outside it, and doc/swarm-fabric-roadmap-exploration.md
(e6a058d) stays non-binding and outside it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QNy4Ve7mogg4X1ezJTnFxG
Deliberately a separate commit, outside the Wave A freeze (5ee9c6b). This
document decides nothing and is not part of any frozen contract: it is a place
to hold the thinking on whether LeVCS should become a change fabric for
multi-agent work, pending its own review.
Records where the proposal maps onto the existing plan, where it cuts against
it -- levcsd must not own ref transactions, the ephemeral stratum invariant,
delegation being identity-shaped rather than protocol-shaped, attestations not
fitting TransactionEvidenceV1, and the canonical benchmark workload being the
wrong shape for swarm traffic -- and where the proposal is simply wrong.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QNy4Ve7mogg4X1ezJTnFxG
Freezes the Wave A interfaces, frame format, durability ordering, and
crash/fault fixtures per doc/phase1-storage-spine-scope.md section 5. Wave B
may now start against these.
D0 lands crates/levcs-store: the sealed public API, the file-ownership split,
the single durability syscall funnel with its counters and fault hooks, the
17-entry failpoint registry in compiler-enforced correspondence with
oracle::AppendFailpoint, and the journal-level drive seam. Wave A lands the
frame codec and journal/segment lifecycle (A1), the recovery index and
checkpoints (A2), and the durable-ingest benchmark and crash harness (A3).
The adversarial review found five defects behind a green gate, three of them
blockers, all closed here. Two were the same shape: drive::reopen_through_
recovery had reimplemented a simplified recovery and called none of recovery.rs
-- so it adopted segment footer sequences without validating frame bytes,
journal_id, or root_uuid, and it double-adopted interrupted seals. The seam
between two packages was untested precisely because each package's own tests
passed. reopen_through_recovery now delegates rather than decides, and
DriveRecovery carries the recovery report verbatim so tests assert the
disposition and not merely its effect: a double adoption and a correct replay
produce the same adopted set, which is how the defect stayed invisible.
Also closed: store-bench now schema-validates its own emitted artifact with a
five-mutation negative control instead of matching JSON substrings; the ACK
reconciler distinguishes duplicates and regressions from forward gaps;
checkpoint writes and a journal truncation are routed through the durability
funnel, whose guard now covers writes and truncations rather than only sync,
rename, and unlink.
bench/result-schema.json is amended (contract review 2026-07-24-B, second and
third amendments): per-gate latency ceilings conditional on outcome so a failed
run is representable, and the verification claims split per gate so a storage
run cannot certify an object graph it never touches. Not-applicable claims are
forbidden rather than falsified; applicable-but-not-performed report false.
Every relaxation is re-pinned in the else branch and asserted member by member,
after an edit in this series silently un-pinned all eleven validation flags and
was caught only by revalidating against constructed bundles.
Arming the fault registry now requires a FaultSerial token, so the invariant is
a compile error rather than a comment. The file where this was diagnosed
carried a header saying it was deliberately the only test in it, and a second
test had been added under that comment anyway -- an 8-in-40 failure rate that
read as flakiness.
Evidence at this commit: check-phase1.sh GATE_EXIT=0 across all four feature
configurations, 124 test binaries, zero failures; verify-store-recovery.sh
--cycles 100 with recovery_failures=0, acknowledged_loss=0,
torn_transactions=0, repeated_adoptions=0, bundle=schema-valid; recovery_eio
40/40 at four test threads; golden corpus byte-stable; fmt clean.
Carry-forwards, explicitly not Wave A blockers and recorded in scope section 5:
extend the crash matrix to generate sealed-frame corruption and cross-shard
journal movement, since it structurally cannot express the class the first
blocker belonged to; and wire GroupBuilder through B1's production path, since
deliverable 4-A1.2 is presently asserted only over a type nothing calls.
Charter item 9 -- ask every package what of its work is correct but uncalled --
is accepted for every subsequent wave. It, not the review, found the class.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QNy4Ve7mogg4X1ezJTnFxG