Skip to content

Integrate backlog step 4: one storage format, macOS port, and expected state on open (#204) - #220

Draft
flyingrobots wants to merge 37 commits into
mainfrom
integrate/open-prs
Draft

flyingrobots wants to merge 37 commits into
mainfrom
integrate/open-prs

Conversation

@flyingrobots

@flyingrobots flyingrobots commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

Backlog step 4 of landing the open PRs: merges #204, which contains #200–#203, onto main with a real merge commit, and adds what ADR-0010 and #216 require on top. Backlog steps 1 to 3 were #208, #211 and #215. ("Backlog step" is this landing sequence. ADR-0010 has its own numbered steps, which are a different list.)

Problem

Main still has two formats, version-2 stores are created only by migration, and a migrated store cannot ingest. #204's stack fixes that, but it was written before two later changes:

  • Admit macOS APFS as a production storage platform #216 admitted macOS APFS, including the default case-insensitive variant, by checking the exact spelling of every protocol name it looks up.
  • ADR-0010's 2026-11-04 amendment removed the binding of a store to its directory and required callers to state what they expect when they open one.

A plain merge would have bypassed the exact-name check on the new store paths, and removing the binding would have left wrong-store and rollback detection with no replacement.

Before you merge

Invariant affected

  • One writable format, created by a recoverable direct initialization.
  • A protocol lookup on default APFS never accepts a spelling other than the exact protocol name, on every store path that admits a platform profile.
  • A store persists no device, inode, mount or path.
  • Every normal open states what the caller expects, and a mismatch refuses with expected and observed values. Recovery tools, verification loads, initialization and the repository-task reopen take no expectation.
  • A copy that mixes two quiescent published states, or holds one half-length file over the newer state, refuses or returns exactly the bytes an identity names.

Approach

  • The merge: the stack's single-format design and Admit macOS APFS as a production storage platform #216's platform adapter are both kept. filesystem_platform_profile selects the native adapter over the nine protocol directories.
  • Coordinates: FilesystemRootIdentity becomes FilesystemDirectoryCoordinate, holding device and mount only, used within one operation and never persisted. Darwin's restart coordinate domain and the lenient Linux mount policy go away with the binding.
  • macOS port: all store code uses FilesystemDirectory. Nested initialization lookups traverse one entry at a time.
  • StoreExpectation: five opens require it, with no default: FilesystemStoreAdmission::reopen, FilesystemCatalogSnapshot::load, FilesystemRetentionSnapshot::load, DurableSnapshot::open and DurableStore::open, which checks when a snapshot is pinned. Catalog names an exact generation and digest, and NoExpectation states that there is no check.
  • Keeping an expectation: ExpectedCatalog is the caller's claim. from_recorded rebuilds it in a later process from a generation and 32 digest bytes, and digest_bytes returns the bytes to record. Keep stores nothing for the caller. The observed side of a mismatch is ObservedCatalog.
  • Commit point: initialization delegates its root synchronizations to one synchronizer, so laws can witness the order and a failed synchronization.
  • Requirements: adds KEEP-STORE-022 and -023.

Alternatives rejected

  • Keeping the root binding on macOS only. It never detected a swap or rollback, and it refused valid restored backups.
  • A default of NoExpectation. Callers would lose the check by omission, which the owner rejected.
  • Exempting FilesystemCatalogSnapshot::load as an "observational" read. Cheaper, but it concedes that a public read can skip the check, which is the thing the owner rejected.
  • A public CatalogDigest constructor for recorded values. Its type means a verified digest, so a recorded claim stays inside ExpectedCatalog.
  • Silently accepting a successor catalog after a crash. It would change what Catalog promises. A variant that accepts the direct successor is a possible follow-up in Follow-ups from the review of PR #220 (single storage format) #222.
  • DurableView as the expected state. It also covers retention, but it would make writer admission depend on the durable read layer.
  • A threaded concurrent copy for the torn-copy law. It depends on timing.

Failure modes

  • Stale expectation. An expected catalog goes stale on any new publication, so a long-lived DurableStore holding one refuses after the store moves on.
  • Crash between publication and the caller's record. The store holds the successor and every normal open refuses the caller. Keep cannot tell that successor from another store, so the caller decides whether to trust what the mismatch reports. The retained-catalog crash sequence pins the refusal.
  • Catalog-only coverage. Catalog does not detect a rollback that leaves the catalog unchanged, such as restored retention state alone. A law pins this.
  • Routes without the check. Recovery can finalize against a rolled-back store, because recovery takes no expectation.
  • Torn copies. With no expectation, a torn copy may silently be the older state. The laws do not cover a copy holding a writer's in-flight stage, other truncation lengths, or a tear over the older state.
  • Durability of the commit point. The laws witness that the synchronization is requested in order. Nothing tests that it reaches stable storage, and no power-loss test exists.

Tests

Native Rust 1.96 on macOS at 0ab39da6, through scripts/native/check.sh:

Check Default APFS Case-sensitive APFS
Tests (--all-features, root package) 946 passed, 0 failed 946 passed, 0 failed
Crash matrix exit 0 at bea75ae9's source; retained-catalog sequence exit 0 at this head exit 0 at bea75ae9's source
Clippy, workspace passes not repeated

The crash matrix is silent on success, so it reports no row count. The full local matrix last ran before the final four commits, which changed only the retained-catalog restart check among crash-matrix code; that sequence was re-run locally, and CI runs the full matrix at this head.

CI at this head: run 42005696619 passes all five jobs: Rust quality gates on Linux, native macOS storage, documentation and workflow integrity, runtime fuzz smoke, and dependency policy. CodeRabbit skips drafts.

Mutants, run by hand and never committed (docs/testing-evidence/single-format-mutants.md):

  • At fb355d66: a no-op commit synchronization survived the whole suite, and so did removing the lock replacement from the portability law. Removing the gate's re-entry at HEAD replacement was killed.
  • After the repairs: skipping the synchronizer, making the root lock shared, and ignoring the expectation in load are each killed by the law that names that behavior. The last also fails the retained-catalog crash sequence at its first case.
  • Still surviving: a no-op production synchronizer. See "Durability of the commit point" above.

cargo test -p xtask locally: 419 passed, 7 failed. The seven failures are caused by the local environment, and none has been re-run in a clean one:

  • 1 b3sum_oracle law: b3sum is not installed.
  • 5 cli_contract laws: the runner sets ambient CARGO_* build settings that the tool refuses. One message says so; the other four say only that a command failed.
  • 1 documentation_cli_contract law: PermissionDenied under the runner's sandbox.

Two process_group laws that failed here before now pass. Their Unix socket path exceeded macOS's 104-byte sun_path bound under the runner's long scratch path. Linux CI passing never cleared that, and ProcessGroup was not at fault.

Also worth knowing:

  • The acceptance laws in tests/single_format/ compile only with the repository-tasks feature, so a plain cargo test skips them silently.
  • The initialization boundary laws interrupt by failing an observer, not by killing a process. The crash matrix does the process kills.
  • The retired descriptor-exhaustion law has no successor.

Not run locally: documentation-integrity-check, conformance-check and golden-file-worldline-check (they need pinned tools or b3sum), Linux compile and tests, and the reattach law, which needs CI's images. Markdown lint and relative links were checked by hand on the changed files.

Benchmark impact

None measured. NoExpectation adds no I/O. With Catalog, the readers compare the view they already collected and load compares the HEAD it already read, so neither adds I/O. The writer's reopen adds a directory clone, a stat and open of HEAD and its 128-byte read, plus one listing scan of the root on macOS for the exact-name check.

Format/API compatibility

Breaking API:

  • The five opens take a required StoreExpectation.
  • ExpectedCatalog::digest is replaced by digest_bytes, and StoreExpectationMismatch::observed returns Option<ObservedCatalog>.
  • CatalogRestartError gains Expectation.

This PR also carries the stack's format change: the definition digest changes, and version 1 and migration are deleted. Nothing is released, and the owner confirmed no stores of value. The expectation adds nothing to any durable format.

Recovery implications

Removal of migration recovery and the moved stage recovery are the stack's, described in #203 and #204. The expected-state check adds no write protocol. KEEP-CRASH-079 (stage completion under recovery admission) and -080 (gated finalization under recovery admission) are not implemented. The plan placed them before version 1's deletion, and the owner accepted on 2026-11-05 that they follow it in #221. Until then, process death during recovery is untested, and so is the gate running again when a finalization is retried after a crash.

Security implications

The exact-name port prevents a foreign spelling from answering for a protocol name on default APFS. Removing the binding widens no filesystem authority. Rollback and wrong-store protection exist only for callers that pass Catalog, only as far as the catalog, and only as well as the caller protects its own record: a record an attacker can rewrite protects nothing.

Extract the pure candidate and snapshot admission from
publish_catalog_generation, and split head execution into staging and
replacement, so a gated publication route can run the same protocol and
insert a check before HEAD replacement. Version-1 behavior is unchanged.
Add publish_retained_catalog_generation over the
RetainedCatalogPublicationStorage port. Under writer authority it admits
the current retention head, its manifest, and every manifest root,
verifies each root closure against the candidate before the first
catalog storage call, and rereads the retention head after head.next
verification, refusing HEAD replacement if it changed. The receipt binds
the preserved manifest generation and digest. Catalog stages are reachable
only with a gate-issued RetainedClosureAdmission.

Record the decision, crash states, and recovery obligation in the v2
format rationale; update closure, retention-publication, README, ledger,
and changelog.
…198)

Move the pinned staging, segment-pool and catalog-pool directories, the
retained writable stages and the stage-authority token into one
FilesystemCatalogStages value. It is the single CatalogPublicationStorage
implementation; FilesystemCatalogPublisher owns it beside its writer lock
and forwards every transition unchanged. Segment stage creation and
selection move with it. No behavior change: this lets a version-two
writer authority run the same stage code.
…ity (#198)

The gate's evidence admission now reads through a crate-internal
RetainedEvidenceSource. Forward publication adapts its
RetainedCatalogPublicationStorage to it without exposing catalog stages,
so recovery finalization can run the same admission later. No behavior
change.
FilesystemVersionTwoCatalogPublisher implements
RetainedCatalogPublicationStorage under FilesystemVersionTwoAdmission.
Its Stages type, FilesystemVersionTwoCatalogStages, wraps the shared
FilesystemCatalogStages and has no public constructor, so it is
reachable only through the retained-closure gate. Retention evidence
is read no-follow and bounded; an absent head is reported only when no
fixed retention stage and no root or manifest pool entry contradicts
it. Publication takes no reader fence; readers now admit a retained
catalog head.next, which version 2 preserves and readers never select.

FilesystemVersionTwoRecoveryAdmission admits a migrated root that may
retain head.next. The inventory reader, discarder, completer, segment
resumer and next-head finalizer gain version-two constructors over the
same stage code, classify_version_two_recovery_names classifies every
version-two root entry as known, and version-two next-head
finalization runs the forward gate's retained-closure admission
immediately before every candidate verification that can precede
HEAD replacement. Version-one entry points keep refusing version-two
roots and version-two entry points refuse version-one roots.

Supported envelope: one staged segment per publication.
…198)

Add the version-two-catalog crash sequence, KEEP-CRASH-074 through 077:
segment link, catalog-stage creation, head-stage synchronization and
HEAD replacement of the production version-two writer, each at before,
during and after. The child installs an observer on the writer's own
stage type and blocks there for process death. Restart requires the
exact stage residue, recovers it with the public version-two recovery
tools (segment completion, catalog discard, gated head finalization),
republishes where the candidate never reached HEAD, retains the
ingested blob and reads both blobs through DurableStore.
)

Interrupt staging after one record, resume through
FilesystemRecoverySegmentResumer::open_version_two, seal, complete the
stage into the pool, republish through the gate and read both blobs.
Add the catalog-publication page: supported envelope (one staged segment
per publication, no rollover), authority, the ten-step forward
protocol, every crash state with its recovery, version-two recovery
admission and classification, gated next-head finalization, typed
cross-version refusal, and the KEEP-CRASH-074..077 rows with 078..080
planned. Route the version-two README, namespace, retention-publication
and rationale pages to it, record the shared-stage decision, add
KEEP-RETENTION-012..015 and update 011.
The library's own interruption laws use the observer without the
repository-tasks feature, so the hidden types must be reachable there
too; clippy --no-default-features --all-targets otherwise denies them as
unreachable pub items.
…#198)

Move populated-store construction out of the golden worldline and
version-two writer fixtures into tests/support/populated_store.rs and
tests/support/successor_catalog.rs. The builder creates a store through
the public writer (version-one initialization, the first catalog through
FilesystemCatalogPublisher, migration), publishes retention roots, and
publishes successor catalogs through FilesystemVersionTwoCatalogPublisher
and the retained-closure gate. Admission is a typed route, so unit tests
outside the platform profile reuse the same steps.

durable_fixture and writer_fixture keep their names and signatures and
delegate, so every law's construction and assertions are unchanged. When
direct initialization replaces migration, only populated_store::create
changes.
…198)

The src retention fixture and the tmpfs reader-profile fixture wrote the
frozen one-zero bundle segment, catalog, and HEAD by hand into an
initialized version-one root and then migrated it. Both now ingest the
one-zero blob through the shared builder under an unchecked test
admission route.

The retention laws prepare against the frozen bundle snapshot and root, so
populate_bundle requires each pool to hold exactly the frozen entry and
HEAD to equal the frozen head; a drifting writer refuses at construction.
Staging the layout record before the chunk makes it refuse with "writer-built
segments pool is not exactly the expected entry".
The retention and version-two catalog crash rows wrote the frozen bundle
bytes by hand into an initialized root and migrated it. A new
production_protocol/populated_store.rs creates the bundle store through
FilesystemCatalogPublisher and migration, then requires the pools to equal
the frozen bundle before any crash row runs. The crash rows, their
coordinates, and restart verifiers are unchanged.

xtask cannot include integration-test support, so this module is the
matrix's counterpart of tests/support/populated_store.rs over the same
public entry points. The version-two crash blob becomes CrashBlob, which
also identifies the one-zero bundle blob; the unused initialized-lock
helper is removed.
…ormat (#198)

ADR-0010 makes keep.segment-store/v2 the only format and requires
initialization to replace migration in the same change that deletes it.

FilesystemVersionTwoAdmission::initialize creates a store under exclusive
writer authority as a recoverable state machine: it classifies the root
before creating writer.lock and again under the lock, makes FORMAT.next
durable before any directory exists, creates the reader fence and the nine
protocol directories with a parent synchronization each, links FORMAT
without replacement, and commits with the root synchronization after the
link. Every admission refuses FORMAT.next, and the stage is removed only
after the commit synchronization, so a store never admits before its
commit point. Interrupted states resume idempotently; every other nonempty
root, and a committed one, refuses with typed evidence before mutation.

An initialized store has no catalog HEAD: writers admit it, readers refuse
it as Unpublished, version-2 recovery classification no longer requires
HEAD, and the version-2 writer publishes the first catalog through the
retained-closure gate. The test builders and the reader-fence fixture now
initialize and publish through that writer.

Migration is deleted: src/adapters/store_migration, its public API, tests,
fuzz target, crash rows KEEP-CRASH-053..073, golden records and source
tables, and format pages. The persisted root device-and-inode binding goes
with it; live lock, fence, and directory checks remain. The FORMAT codec
moves to src/adapters/store_format. Removing the migration rows from
definition.tsv changes the definition digest to f1a0ce10...361d; the marker
golden is regenerated from the handwritten oracle.

The initialization crash matrix runs 41 cases at KEEP-CRASH-081..091 and
recovers, publishes, retains, and reconstructs after each. Retired
identifiers are mapped in docs/formats/segment-store-v2/retired-identifiers.md.
Retires #108.
…acceptance (#198)

tests/single_format.rs drives the production entry points under the admitted
ext4 profile. Initialization laws: an initialized store reopens but refuses
readers as Unpublished, version-1 admission refuses it, a committed store
refuses re-initialization unchanged, an uncommitted root refuses writers and
readers, and a first publication refuses a stray retention root without a
head, leaving the store unchanged.

The five ADR-0010 portability laws copy a quiescent store byte for byte into
a new directory: the copy opens and reads; corrupt and incomplete copies
refuse exactly as the same damage to the original; a replaced writer lock
and retention directory still refuse; a copy and its original take
independent locks, diverge, and keep the shared segment bytes; and a backup
reopens with the original gone, then recovers an interrupted write.

The acceptance sequence (ADR-0010 step 4) initializes, ingests, reopens,
reconstructs exact bytes, retains, publishes a second catalog, interrupts a
third publication before HEAD replacement, recovers through gated
finalization, reads both retained blobs, and refuses a lossy candidate with
every byte intact. Requirement rows KEEP-INITIALIZATION-006 and
KEEP-PORTABILITY-001..005 record the evidence.
KEEP-CRASH-001..030 name the segment, catalog, head, and recovery-discard
boundaries of the shared catalog stages, which the single-format writer
executes unchanged. Their crash children now create the store by direct
initialization and publish generation one through
FilesystemVersionTwoCatalogPublisher and the retained-closure gate instead
of the version-1 publisher. The driver observes the writer's own
segment-stage port and catalog transitions; the version-1
CatalogPublicationStorage decorator is deleted. A `during` catalog or head
write writes the same strict prefix into the stage the writer created and
dies before the writer's own write.

The recovery-discard rows open the discarder through version-2 recovery
admission. Their head precondition is now a writer publication interrupted
at the head write, so both pools hold the published segment and catalog.

Restart inspection expects the committed single-format inventory, including
the corpus FORMAT bytes, and admits every residue through version-2
recovery admission in place of a bare writer-lock reacquire. The rows keep
their identifiers because each boundary is the same code.
…tores (#198)

Every law below constrains code the single format runs unchanged, so it
moves rather than retires. Each fixture now creates its store through
direct initialization instead of hand-writing writer.lock, staging,
segments, and catalogs:

- the four recovery fixtures (stage completion, stage discard, segment
  continuation, next-head finalization) open their tools through
  FilesystemVersionTwoRecoveryAdmission; the planted crash residue
  (stages, pool entries, head.next) is unchanged. Writer exclusion is now
  observed where single-format recovery takes the lock: a second
  recovery admission refuses with WriterLock Busy;
- the recovery-inventory and stage-fingerprint laws open the reader
  through version-2 admission; the symlinked-staging law now refuses at
  admission with WrongKind { Directory, Symlink };
- catalog_filesystem_publication runs through
  FilesystemVersionTwoCatalogPublisher and the retained-closure gate.
  A leftover head.next and a non-directory or linked protocol directory
  refuse at writer admission with exact namespace refusals; two new laws
  keep the stages' own no-follow checks for a namespace replaced after
  admission;
- segment-stage, observed-stage, and catalog-restart laws use the
  single-format writer; catalog_restart's one-zero store is now
  published by the writer and required to equal the frozen corpus bytes;
- name-classification laws classify the single-format root.

Laws that plant a link target in the store root plant it after admission,
which refuses unknown root entries, so each tool's own no-follow open
remains the boundary under test. Test-only version-1 openers left
without callers are removed.
ADR-0010 step 5. The single format now does everything version 1 did, so
version 1 is deleted and every open path refuses a root without the
format's FORMAT marker.

Deleted: FilesystemPlatformAdmission, the version-1
FilesystemCatalogPublisher and its ungated port impl, the version-1
initializer (initialize_store, StoreInitialization{Error,Phase,Receipt,
Storage}, RepositoryInitializationStorage, the filesystem initializer and
its storage), the version-1 namespace laws (admit, admit_published,
admit_recoverable), the version-1 platform-profile open, the version-1
recovery openers (open(store_root) on the completer, discarder, resumer,
finalizer, and inventory reader, and the unchecked variants),
classify_recovery_names, and the Platform and WriterLock variants of the
four recovery open errors, which no remaining constructor produces.
Next-head finalization always runs the retained-closure gate; the
version-1 branch that skipped it is gone.

FilesystemCatalogSnapshot::load now admits the exact namespace (an
in-flight head.next allowed) and the FORMAT marker before opening any
catalog artifact, refusing with the new CatalogRestartError::Admission.

Tests: the version-1 initializer, platform-admission, and cross-version
laws are deleted with their subject. format_marker_laws requires every
open path (writer, recovery, inventory, catalog restart, retention and
durable readers) to refuse a published, retained store whose FORMAT was
removed as MissingFormatMarker, unchanged; a new single-format law refuses
initialization on tmpfs before mutation (the obligation of the deleted
catalog_platform_admission law); compile_fail doctests now refuse a bare
FilesystemWriterLock instead of the deleted type. The replaced-retention
portability law displaces the directory outside the root, since catalog
restart now refuses unknown root entries.

Crash matrix: the version-1 initialization sequence and KEEP-CRASH-031..035
are retired; the retention restart model derives its phase from
WriteRootStage's position instead of a hard-coded index. KEEP-STORE-020,
KEEP-RECOVERY-002/003, and KEEP-RETENTION-014 are retired in
docs/formats/segment-store-v2/retired-identifiers.md with their last
evidence and successors; format and corpus pages describe the single
format's admission.
With version 1 deleted there is one format, so "VersionTwo" no longer
distinguishes anything and reads as if a second format existed. Refactor
only: no behavior or test law changes; Display messages that said
"version-two" now name the store instead.

Public API:
- FilesystemVersionTwoAdmission -> FilesystemStoreAdmission
- FilesystemVersionTwoRecoveryAdmission -> FilesystemRecoveryAdmission
- FilesystemVersionTwoCatalogPublisher -> FilesystemCatalogPublisher
- FilesystemVersionTwoCatalogStages -> FilesystemRetainedCatalogStages
- VersionTwoInitialization{,Boundary,Directory,Error,Observer,Phase,Prefix,
  Refusal} -> StoreInitialization{...}
- VersionTwoRecordRefusal -> StoreRecordRefusal
- FilesystemPlatformAdmissionError -> FilesystemStoreAdmissionError
- classify_version_two_recovery_names -> classify_recovery_names
- open_version_two{,_unchecked_for_repository_tasks} -> open{,...} on the
  recovery tools and inventory reader

Kept: StoreFormatDefinitionDigest::VERSION_TWO and
CanonicalStoreFormatMarker::version_two(), which name the format's own
version number (keep.segment-store/v2, marker version 2), and the "v2"
record-grammar and corpus names.

Modules and files follow the types (filesystem_store_admission,
filesystem_recovery_admission, filesystem_catalog_publisher,
filesystem_retained_catalog_stages, filesystem_store_namespace,
store_initialization/, tests/catalog_writer, tests/store_admission_contract).
Crash-matrix sequences version-two-initialization and version-two-catalog
become initialization and retained-catalog, and points VersionTwo* become
Retained*; their KEEP-CRASH identifiers are unchanged. Comments and living
docs drop "version-two" where it meant "the store"; changelog, ADRs, and
testing-evidence records keep their historical names.
ADR-0010 asks for one normative set of pages for the one store format.
docs/formats/segment-store-v1 and segment-store-v2 become
docs/formats/store:

- segment.md, catalog.md, and publication.md move unchanged in grammar;
  their intros name the store format and the record-grammar version their
  own magic fields carry;
- the version-1 recovery page becomes stage-recovery.md, its rationale
  catalog-protocol-rationale.md, and its ledger
  catalog-protocol-requirements.md, beside the store's own recovery.md,
  rationale.md, and requirements.md (merging the ledgers would exceed the
  300-line page threshold);
- one README replaces the two indexes and keeps both core-law sections and
  every route; the format registry has one row and states that
  keep.segment-store/v1 is retired as a store format.

Every heading other pages link to is kept, including
recovery.md#root-identity-across-restart. Links across the repository,
including historical evidence pages, point to the new paths; the
retirement ledger keeps the old paths it records as history. The
conformance corpora keep their v1/v2 directory names, which are the
record-grammar versions in the artifacts' own bytes, so definition.tsv
and the definition digest are unchanged.

Prose that used "version 2" to mean "the store beside version 1" now names
the store. The compatibility section follows ADR-0010 (no in-place
conversion; an upgrade tool after the first release) and states that
definition.tsv does not yet bind the segment, catalog, and head grammars.
KEEP-CRASH-078, reserved for discard under version-2 recovery admission and
never implemented, is retired: KEEP-CRASH-027..030 now run under recovery
admission. The changelog records the unit.
The deleted cross-version law showed that classification refuses a former
version-1 inventory as missing reader.lock. Generalize it: removing any one
of the nine root entries of an initialized store refuses as exactly that
missing required entry, so a classifier that stops requiring FORMAT or the
reader fence is caught.
Resolves the stack against macOS support (#216). Every store path goes through
the native platform adapter and FilesystemDirectory, including the new
initialization, namespace, marker and admission code, so the exact-name check
covers them. Nested initialization lookups now traverse one entry at a time.

The persisted root binding is gone, so the live directory coordinate keeps only
device and mount, and Darwin's restart coordinate domain is removed. A reattached
volume must still open; that law now uses an initialized store. Platform-neutral
laws from the stack also run on macOS.
ADR-0010 removes the persisted root binding, which never detected a swapped or
rolled-back store. Each public open now takes a StoreExpectation with no default:
an exact catalog refuses anything else with expected and observed values, and
NoExpectation states the absence of a check at the call site.

Enumerated laws cover the two copies ADR-0010 requires before migration deletion:
every mix of two published states and every partly copied file refuses or returns
exact bytes, and a restored older snapshot refuses newer expected state.
The namespace check refuses a foreign spelling first, so the public laws do not
witness the lookup check. These laws call the marker, head and protocol-directory
lookups directly, and the public laws add the writer and initialization routes.
@coderabbitai

coderabbitai Bot commented Nov 5, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Initialization delegates every root synchronization to one synchronizer, which production binds to the real call. Two laws substitute it: one requires the commit synchronization after the marker link and before stage removal, while both names exist; the other fails it and requires an unadmitted, resumable store.

The law previously named for a failed commit synchronization failed its observer before the synchronization ran. It is renamed for what it does. A mutant that made the synchronization a no-op passed the complete suite before this change.

These laws witness that the protocol requests the synchronization. That the production call reaches stable storage is still established by reading it.
The module was compiled only on Linux. Its three laws pass natively on default and case-sensitive APFS, so the gate goes.
The guards could pass on the two whole states, any error counted as a refusal, and the names claimed more than the laws enumerate. Refusals must now be a typed absent-or-malformed cause, the guards count only real mixes, empty files are skipped as untorn, and the names and header say what is and is not covered.
Two process-group laws failed under a long temporary directory because their Unix socket path exceeded macOS's 104-byte sun_path bound, and the child panicked before readiness. Their sibling already used short names. With short names both pass natively; ProcessGroup itself was not at fault.
FilesystemCatalogSnapshot::load was a public open that took no StoreExpectation, so a caller could lose the check by using it. It now takes one and refuses with CatalogRestartError::Expectation. Verification loads still name none.

An expectation can now be kept across processes. ExpectedCatalog is the caller's claim: from_recorded rebuilds it from a generation and 32 digest bytes, and digest_bytes returns the bytes to record. It no longer hands out a CatalogDigest, which would have let a claim pass as a verified digest. The observed side of a mismatch is the new ObservedCatalog, and the mismatch's Display reports both.

The retained-catalog crash sequence now requires the catalog recorded before an interrupted publication to admit while generation one is published and to refuse, reporting generation two, once HEAD moves. Keep cannot tell that successor from another store, so accepting it stays the caller's decision.

BREAKING CHANGE: FilesystemCatalogSnapshot::load takes a StoreExpectation. ExpectedCatalog::digest is replaced by digest_bytes, and StoreExpectationMismatch::observed returns Option<ObservedCatalog>.
The review of PR #220 found statements that claimed more than the code and laws establish. This corrects them: which opens take an expectation and which routes take none, how an expectation is kept and what a crash does to it, what the torn-copy and commit-point laws witness, the current crash-row counts, the retirement ledger, the changelog, and the platform pages.

It records two owner decisions of 2026-11-05: KEEP-CRASH-079 and -080 follow version 1's deletion and block the first release in #221, and the GC salvage renumbers from KEEP-CRASH-092.

The mutation record holds the six hand-run mutants and what they showed. The protocol contract law that pinned the old case-sensitivity wording now pins the rule that replaced it.
FilesystemCatalogSnapshot::load compared after loading the whole store, so a store that published another catalog and was also damaged, or over the caller's limits, reported that instead of the mismatch. The writer's reopen reported the mismatch for the same store. The load now compares what the checksummed HEAD names before it reads the catalog or any segment, and a law pins that order.

Two assertions in the expectation laws were looser than their comments: one did not pin the head phase, and one flipped a bit in a digest that would have refused unflipped.
)

The refusal list accepted every segment error, including policy and resource limits, which its own comment excluded. It now accepts only errors about the segment's bytes.

A mix that behaves as the older whole state could satisfy both guards, because a blob that state never held refuses there too. The laws now also require a refusal at an absent or malformed artifact.
…198)

The recorded generation-one catalog was only shown to admit on the republish path, after recovery had cleaned the stage, which is six of the twelve retained-catalog cases. Restart now requires it to admit before any recovery in every case that left HEAD at generation one, beside whatever stage the publication left.
)

An independent read of the repair commits found wording that claimed slightly more than the code. Only the lock phase and the three root-synchronization phases go through the injected synchronizer; the fence, the directory admissions and the stage discard synchronize their parent directly. Five repository-task entry points take no expectation, not one. One of the six mutants was a test edit. The docs also follow the three preceding commits.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant