Skip to content

Implement retention namespace generation transitions - #78

Merged
flyingrobots merged 112 commits into
mainfrom
feature/retention-namespace-transitions
Sep 8, 2026
Merged

Implement retention namespace generation transitions#78
flyingrobots merged 112 commits into
mainfrom
feature/retention-namespace-transitions

Conversation

@flyingrobots

@flyingrobots flyingrobots commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Goal

Implement the accepted keep.segment-store/v2 retention and one-way migration campaign tracked by #19 without weakening Keep's exact-byte, fail-closed storage law.

The affected invariant is that retention authority must be generation-checked, canonical, deterministic, bounded, and durable before any reader or writer may treat a version-2 state as admitted. This branch preserves every version-1 segment, catalog, and publication-head byte.

Plan

  • Freeze the version-2 format, golden corpus, typed coordinates, and canonical retention and migration records.
  • Implement deterministic closure verification, generation transition planning, publication preparation, ordered storage ports, and receipts.
  • Implement bounded migration inventory, migration intent and receipt construction, verification-first execution, and filesystem migration authority observation.
  • Implement one fresh writer-locked filesystem migration through all 21 ordered phases with exact record and namespace verification.
  • Complete production filesystem retention publication and exact idempotent retry behavior.
  • Complete restart recovery, immutable reader fencing, and model-based namespace transition evidence.
  • Complete migration partial-prefix recovery, corruption coverage, and KEEP-CRASH-053..=073 process-death evidence.

Current status

This is a Draft-turned-ready PR for review of the landed slices; it does not
yet close #19. Fresh migration and fresh retention publication both have
production filesystem adapters. FilesystemStoreMigrationAuthority executes
the 21-phase forward migration; FilesystemRetentionPublicationAuthority
executes the 17-phase forward retention publication against a completely
migrated version-2 root, for both initial and successor generations. Both stage
exclusively, verify device-and-inode identity at every transition, hard-link
immutable pool entries without replacement, and leave every admitted version-1
byte unchanged. observe_current returns the published head and its
cross-verified manifest; verification admits a successor only when the prepared
head names that manifest as its exact predecessor, returns an exact
already-committed candidate with zero mutation, and refuses a superseded one.
Version-1 reopen refuses a migrated root; admit_version_two and
reopen_version_two own the version-2 namespace boundary.

Both adapters are intentionally one-shot forward writers, not restart engines.
A process that opens a retained stage prefix has no lawful continuation
implementation and refuses; the crash matrix covers KEEP-CRASH-001..=035,
not the retention boundaries 036..=052 or migration boundaries 053..=073.
Version 1 therefore remains the only admitted restart-safe production store.

The README was rewritten as a front door (855db515, d7518a8e, 06072f44):
it routes requirement status to the segment-store-v2 ledger instead of
restating it, and carries a Mermaid architecture diagram.

The branch contains 57 commits spanning the #19 campaign and is 0 commits
behind origin/main; no rebase or history rewrite has been performed.

Failure modes

  • Stale or mismatched generations refuse before mutation.
  • Missing, corrupt, excessive, changed, substituted, or noncanonical closure and migration evidence has typed refusal boundaries.
  • Existing stages are never truncated; byte-equal substituted canonical files refuse by identity.
  • Unknown or out-of-order version-2 namespace entries refuse before repair.
  • Storage failures stop at the named publication or migration phase; partial-prefix recovery remains an explicit nonclaim.

Validation

  • cargo fmt --all --check
  • cargo check --workspace --all-targets --all-features --locked
  • cargo check --workspace --all-targets --no-default-features --locked
  • cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
  • cargo clippy --workspace --all-targets --no-default-features --locked -- -D warnings
  • cargo test --workspace --all-features --locked
  • cargo test --workspace --all-features --release --locked
  • cargo test --workspace --doc --locked
  • cargo +1.96.0 check --workspace --all-targets --all-features --locked
  • cargo xtask golden-file-worldline-check
  • cargo xtask conformance-check
  • cargo xtask source-structure-check
  • Debug and optimized cargo xtask durability-crash-matrix
  • cargo xtask documentation-refusal-check
  • cargo xtask documentation-integrity-check
  • cargo deny check
  • cargo audit
  • Fuzz workspace format, check, and strict clippy gates
  • Focused fresh-filesystem migration laws: 8 passed
  • Focused filesystem retention publication and successor laws: 8 passed

Compatibility, recovery, performance, and security

  • Format/API compatibility: version 2 preserves admitted version-1 immutable bytes; migration is one-way and does not yet authorize production admission.
  • Recovery: both fresh forward writers are implemented; partial-prefix classification, continuation, and retention and migration process-death evidence remain open.
  • Benchmark impact: no performance claim is made.
  • Security: capability-relative, no-follow filesystem admission, same-mount namespace creation, and repeated evidence revalidation are preserved. This work does not claim secure deletion or semantic/causal authority.

Alternatives rejected

  • Inferring retention or migration completeness from path existence.
  • Re-encoding version-1 immutable data in place.
  • Treating a successful fresh execution as restart or crash-recovery evidence.
  • Admitting byte-equal canonical substitutes without retained file identity.

Open questions

Peer review is most useful on partial-prefix recovery classification, retention publication ordering, and the immutable reader-fence aperture before those irreversible protocol slices are implemented.

Closes #19 only when every unchecked Plan item and the colocated requirements ledger are complete.

reject_trailing_bytes reported LengthChanged.observed as the expected length
plus the single byte that detected the overrun, so a 6-byte stage declared at
3 bytes was reported as 4. Two unreachable fallback arms reported the expected
length itself as the observation.

The three disagreement sites now report the file's metadata length through
one helper, so observed names what the filesystem holds. The pinned trailing-
bytes law expects observed: 6 for the 6-byte fixture; it failed before this
change and passes now.

Self-review finding A8 (P3).
Refs #78
FilesystemPlatformAdmissionError gained MigrationRecord and RootIdentityChanged
on this branch, and every future admission boundary (recovery, GC) will add a
refusal of its own. Without #[non_exhaustive] each addition would be a breaking
change for any caller that matched the enum exhaustively. Inside the crate
only matches! uses exist, so nothing changes here.

A source contract in tests/version_two_admission_contract.rs pins the
attribute; it failed before this change and passes now.

Self-review finding X1 (P3).
Refs #78
The formats registry described keep.segment-store/v2 as "retention transition
implementation planned", and the version-two overview said no version-2 writer
was available and version 1 remained the only admitted production store. Both
predate the migration authority, FilesystemVersionTwoAdmission, and
FilesystemRetentionPublicationAuthority on this branch.

Both pages now name what is implemented with executable evidence and what
remains planned in issue #19, and the overview points at the requirements
ledger as the authority on which requirements are proven. Nonclaims in
retention.md and requirements.md are unchanged.

Self-review finding X2 (P3).
Refs #78
The retention FIFO laws fell back to spawning mkfifo(1) on hosts where rustix
compiles mknodat out. A spawned child briefly holds copies of every open
descriptor in the process, and a flock lives as long as any copy of its
descriptor, so while one FIFO law spawned, another law's writer lock stayed
held across its drop-and-reopen and refused with WriterLock { source: Busy }.
Measured locally: 3 of 6 parallel lib runs failed, 0 of 4 single-threaded,
every failure a FIFO law.

The fallback is deleted and the module is compiled only on Linux, where CI
runs it through mknodat. A contract test walks src/ and refuses any module
that names process::Command or Command::new; it failed before this change and
passes now. No production path ever spawned a process.

Self-review finding R18 (P4), pulled forward because it explained the flake.
Refs #78
@flyingrobots

Copy link
Copy Markdown
Owner Author

Self-Code Review — resolution progress, 21 of 37

Continuing the approved order from the court-date comment. Same discipline as the first report: one Red → Green → Commit per finding under all gates.

Finding Severity Commit What changed
R3 P3 b3d66805 Refusal Display arms rewrapped to 100 columns; contract law pins the width.
R4+R5+R6+R15 P3 de3b2c23 PublicationAttempt owns all between-phase state; refused verification admits no phase; NamespaceExpectationViolated.
R7 P3 2c2f1ff1 Typed namespace refusals; the capacity law was found to never reach capacity and now does.
R8 P3 885db63b Record lengths from decoder constants; contract keeps readers free of bare literals.
R9+R12 P3 ac8c22a6 Observed state decoded once; typed disposition removes the unreachable arm.
R10 P3 ccf64232 VersionTwoRecordRefusal and CatalogHeadRefused { source } carry decode errors.
A8 P3 b38e4168 LengthChanged.observed reports the real on-disk length.
X1 P3 c95a0946 FilesystemPlatformAdmissionError is #[non_exhaustive].
X2 P3 47e137e4 Formats index states what is implemented versus planned in #19.
R18 P4 8ed1b4e2 Pulled forward: mkfifo(1) fallback deleted, FIFO laws Linux-only, no process spawn in src/.

Flake root cause. The intermittent WriterLock { source: Busy } seen in earlier local runs was measured at 3/6 parallel runs versus 0/4 single-threaded, always a FIFO law. The mkfifo(1) fallback's spawned child briefly held copies of other tests' lock descriptors, keeping a flock alive across a drop-and-reopen. R18 removes the spawn; a contract test now forbids process spawns anywhere under src/. CI (Linux, mknodat) was never affected.

Remaining: 16 findings — D1 (six commits), D2, R13, R14+R16+R22+R23, R17, R19, R20+X3, R21, R24. Next push after slice 30 with the final Activity Summary and a re-review request.

…stage

The retention stage, the migration fixed-record stage, the retention
current-state reader, and the version-two record reader each carried their
own copy of the same primitive: open read-only without following links and
without blocking, check kind and length, read exactly the expected bytes,
refuse trailing bytes, and for stages reverify device and inode identity on
both sides of the read. Four copies of a security-relevant read path drift
independently; the FIFO refusal, for one, had to be added to each.

filesystem_exact_record now owns EntryIdentity, a typed ExactRecordRefusal,
verify_named, require_absent, and link_without_replacement; the bounded
readers arrive with their consumers in later commits, since deny(warnings)
refuses a primitive with no consumer. Refusals carry no protocol vocabulary;
each consumer maps them onto its own exact messages or typed variants, so no
observable refusal changes. The retention stage is ported first, its messages
preserved byte for byte. Two laws pin the primitives (different bytes and a
byte-equal substitute refuse; absence and no-replacement linking hold), and a
contract test keeps ported modules from reopening records themselves.

Self-review finding D1 (P3), commit 1 of 6.
Refs #78
…-record module

The migration fixed-record stage carried the second copy of the stage
primitives: its own device-and-inode identity type, its own no-follow
non-blocking exact read with double metadata reverification, its own absence
check, and its own no-replacement hard link.

It now consumes filesystem_exact_record like the retention stage, mapping each
shared refusal onto its existing messages byte for byte, so no observable
refusal changes. The module is added to the exact-record consumer contract,
which refuses any reimplementation of the primitives. The migration laws and
the crash matrix's migration paths cover the port.

Self-review finding D1 (P3), commit 2 of 6.
Refs #78
…cord module

The retention current-state reader carried the third copy of the bounded
read: open without following links or blocking, refuse a non-regular entry or
a length disagreement, read exactly the declared bytes, refuse trailing bytes.
The head, manifest, committed-root, and predecessor-root reads all go through
it, as does the catalog-head binding.

filesystem_exact_record gains read_exact_optional and the KindOrLength refusal,
pinned by two laws (absent reads as None while an exact record reads its
bytes; a short file or a directory refuses before any byte is read). The
retention reader is now a wrapper that maps shared refusals onto
RecordLengthOverflow, RecordTrailingBytes, and RecordKindOrLength, so every
typed refusal callers already downcast to is unchanged. Both stage mappers
fold the new variant into their kind-or-length message. The module joins the
consumer contract.

Self-review finding D1 (P3), commit 3 of 6.
Refs #78
…dule

The version-two record reader carried the fourth copy of the bounded read
for FORMAT, migration.intent, and migration.receipt. It now consumes
filesystem_exact_record's new read_exact_regular, which keeps absence as the
filesystem's own NotFound error because those records are required, and maps
each shared refusal onto VersionTwoRecordRefusal by record name: LengthOverflow,
KindOrLength, and TrailingBytes are unchanged for callers. One law pins the
required read (an exact record reads its bytes; an absent one is NotFound), and
the module joins the consumer contract, so all four readers and stages now
share one no-follow, non-blocking, exact-length implementation.

Self-review finding D1 (P3), commit 4 of 6.
Refs #78
…rd module

The version-one segment and catalog publishers linked their sealed stages into
the immutable pools through filesystem_catalog_artifact's own copy of the
no-replacement hard link. The retention and migration stages already use the
shared primitive, so the copy is deleted and both publishers call
filesystem_exact_record::link_without_replacement directly. Behaviour is
identical: an existing target is left for post-link verification, never
replaced. The crash matrix's segment and catalog publication protocols cover
the repoint.

Self-review finding D1 (P3), commit 5 of 6.
Refs #78
…ryIdentity

The recovery namespace, the migration inventory directory, and the migration
namespace directory each declared an identical private DirectoryIdentity
(device and inode read from a pinned Dir) to detect a replaced protocol
directory between phases. The shared exact-record module's EntryIdentity is
the same pair, so it gains of_directory and the three copies are deleted;
every comparison site is unchanged in meaning. Recovery and migration laws
and the crash matrix cover the identity checks.

Self-review finding D1 (P3), commit 6 of 6.
Refs #78
The version-1 physical pool names and the version-2 retention pool and
namespace names each carried an identical private DigestHex renderer for the
64-lowercase-hex digest component that namespace and pool admission later
parse. Two copies of the emitter for one on-disk grammar can drift.

adapters::digest_hex now owns the renderer and both pool-name modules consume
it. Filename output is unchanged; the golden worldline, conformance, and
retention namespace laws pin the grammar.

Self-review finding D2 (P3).
Refs #78
…espace

link_root and synchronize_root_namespace receive the candidate root from the
orchestrator but used whatever namespace directory the attempt held, so a root
from another namespace handed to either phase would have been linked into, or
synchronized as, the admitted directory.

The attempt now records the admitted namespace's name beside its capability,
and both phases require the root they receive to name exactly that namespace,
refusing with the new RetentionCurrentStateRefusal::AttemptNamespaceDisagreed
otherwise. A law drives synchronize_root_namespace with a root from a second
namespace after admission; it failed before this change and passes now. The
Display impl is split into a short fmt over a const message table so it stays
under clippy's line ceiling; every message is unchanged.

Self-review finding R13 (P4).
Refs #78
When no retention head is published and the preparation carries no forward
publication (the byte-identical already-committed shape), the initial-path
check refused as StaleCommittedRetry, a name that means "another successor is
current". Nothing is current in that state; the retry claims a commit that
cannot have happened. The refusal is now CommittedRetryOverAbsentHead, with
its own description.

The successor-over-absent-head law now downcasts to the refusal it actually
receives, ExpectedCurrentOverAbsentHead, instead of asserting only the error
kind. Of the grouped findings, R14 (the authority doc listing the pinned root),
R16 (the LivenessGeneration import), and R22 (the stale identity-probe comment)
were resolved by the PublicationAttempt and identity-policy commits earlier in
this pass; this commit closes the group.

Self-review findings R14, R16, R22, R23 (P4).
Refs #78
… onto it

Every filesystem law ended with a manual drop(authority); sandbox.remove()?
pair, and a law that returned early on a failed assertion left its sandbox
behind in the scratch root. Two test modules also carried their own private
TestDirectory with a different scratch location.

The shared sandbox now removes itself on drop; remove(self) stays for laws
that want to observe the removal error. A law pins the behaviour (a dropped
sandbox no longer exists); it failed before this change and passes now. The
recovery-stage materialization and writer-lock laws use the shared sandbox,
and the 43 manual teardown lines in the retention laws are gone; locals drop
in reverse declaration order, so the authority releases its handles before
the sandbox removes the directory.

Self-review finding R17 (P4).
Refs #78
tests/version_two_admission_contract.rs pinned the exact text of two function
signatures to show that retention publication accepts only version-two
authority and the version-one publishers only version-one authority. A
signature contract breaks on any harmless reformatting and proves nothing the
compiler does not already enforce.

The doc of FilesystemRetentionPublicationAuthority::open now carries a
compile_fail doctest that hands it a FilesystemPlatformAdmission; the doctest
passes only while that call fails to type-check, which is the actual boundary.
The source contract keeps per-file markers (each publisher names its own
admission type and never the other) without pinning a signature.

Self-review finding R19 (P4).
Refs #78
…he census

The namespace census carried its own copies of the pool-name grammar
(is_pool_name, is_lower_hex, the suffixes and hex widths) apart from the
module that emits those names, and classified every entry through a full
metadata call when the directory listing already reports the file type.

The predicates and constants now live in filesystem_retention_pool_name next
to root(), manifest(), and namespace(), so emission and admission cannot
drift. The census reads DirEntry::file_type, and its doc states the bound: one
visit per roots entry, root pool, and manifest pool, two counters, no entry
bytes, no link following, work proportional to the entry count under the
4,096-namespace ceiling. Behaviour and refusals are unchanged; the namespace,
capacity, and expectation laws cover the census.

Self-review findings R20 and X3 (P4).
Refs #78
Two README lines ran past 80 columns because a Markdown URL cannot wrap. Both
now use reference-style links with the targets collected at the end of the
file; the rendered text and link targets are unchanged, and the phrases the
README contract tests pin are untouched.

Self-review finding R21 (P5).
Refs #78
retention.md and recovery.md both sat at exactly the 300-line review
threshold, so every later sentence had to be paid for by rewrapping an
earlier one. Closure admission and the generation transition now live in
retention-publication.md; the ordered one-way migration protocol and partial
migration recovery live in migration-recovery.md. Each old page keeps its
record grammars and routes to its new companion, and the version-two overview
routes to all four. No sentence changed meaning; no section-anchor link pointed
into the moved sections.

The protocol contract pins the moved phrases on their new pages, reads the
recovery pair as the one contract they state, and adds both new pages to the
review-threshold list. The pages now stand at 242, 65, 242, and 67 lines.

Self-review finding R24 (P5).
Refs #78
@flyingrobots

Copy link
Copy Markdown
Owner Author

Self-Code Review — Activity Summary: 37 of 37 resolved

All findings from the court-date comment are closed on abb5cf65. Earlier progress: 11 of 37, 21 of 37. Every commit ran the full gate chain (fmt, clippy pedantic in both feature sets, both test suites, doctests, cargo doc, documentation refusal and integrity, source structure, conformance, golden worldline; Linux-target clippy for cfg(linux) touches). Where a deterministic RED existed it was observed before GREEN; the commits say where it did not.

Finding Commit What changed
D1 (6 commits) ba10582e 7be0b7d7 80908e38 d7510eee 41e680f8 39014f14 filesystem_exact_record owns the no-follow, non-blocking, exact-length read, identity reverification, absence check, and no-replacement link; retention stage, migration fixed stage, retention current-state reader, version-two record reader, and both version-one pool linkers consume it; three DirectoryIdentity copies collapse onto EntryIdentity. Contract keeps consumers from reimplementing the primitives.
D2 aaf7ccc3 One DigestHex for version-1 and version-2 pool names.
R13 fb538e8b Root-link and namespace-sync phases require the root to name the admitted namespace (AttemptNamespaceDisagreed).
R14 R16 R22 R23 6ce8393a CommittedRetryOverAbsentHead replaces the misnamed refusal; the absent-head law downcasts; the other three were resolved by earlier slices.
R17 a6b61204 Sandbox removes itself on drop; private copies folded; 43 manual teardowns gone.
R19 d647261a compile_fail doctest proves the admission type boundary; contract keeps markers, not signatures.
R20 X3 400ba69f Pool-name predicates beside their emitters; census uses DirEntry::file_type and documents its bound.
R21 ffba9a79 Reference-style links for the two overlong README lines.
R24 abb5cf65 retention-publication.md and migration-recovery.md split off; pages at 242/65/242/67 lines; protocol contract updated.

Two things worth a reviewer's eye.

  • The Display impl of RetentionCurrentStateRefusal is now a short fmt over a const fn message table to stay under clippy's line ceiling; every message string is unchanged.
  • The migration protocol contract reads recovery.md and migration-recovery.md together as one contract, since the split moved sections, not meaning.

Process notes. Gate runs on the local machine were repeatedly killed for memory during this pass; the chain was run one heavy step at a time at two build jobs, and no commit landed without every gate green. The WriterLock { Busy } flake was root-caused and removed (R18, 8ed1b4e2).

@codex the branch is ready for your re-review at abb5cf65. CodeRabbit skips this PR (381 files over its 100-file limit), so a human or Codex pass is the second review here.

The exact-record law for byte-equal substitution deleted the record and wrote
a new file at the same path. On APFS the new file always receives a fresh
inode number, so the law passed locally; on ext4 the kernel may hand the new
file the inode number just freed, the identity matched, and CI refused the
law with "byte-equal substitute admitted" on abb5cf6.

The law now creates the substitute beside the original and renames it over
the entry. Two files that exist at the same time cannot share an inode, so
the substitute's identity differs on every filesystem, which is the situation
the primitive exists to refuse. The retention and migration substitution laws
already used rename and passed on Linux; only this one was platform-bound.

Refs #78

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: abb5cf6591

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/adapters/retention/filesystem_retention_catalog.rs Outdated
Comment thread src/adapters/retention/filesystem_retention_namespace.rs
Comment thread src/adapters/retention/filesystem_retention_pool_name.rs Outdated
Comment thread src/adapters/store_migration/filesystem_migration_authority.rs Outdated
is_pool_name accepted any 16 lowercase hex digits as the generation component,
so an entry such as 0000000000000000-<digest>.root passed the census although
root and liveness generations are positive by definition and the emitters can
never produce that name. The census then treated an impossible coordinate as
canonical and publication continued over it.

The predicate now parses the generation component and requires a positive
value, refusing zero as NoncanonicalPoolEntry like any other malformed name.
A law plants a zero-generation root pool entry and requires the refusal; it
failed before this change and passes now.

Codex review, third pass (filesystem_retention_pool_name.rs).
Refs #78
admit_capacity bounded only the creation of a new namespace directory: a
candidate whose namespace already existed returned Ok without comparing the
census against RetentionManifest::MAXIMUM_ENTRY_COUNT. A store that had
somehow accumulated more than 4,096 namespace directories therefore kept
publishing successors although no manifest can describe it.

The census is now compared against the ceiling before the existing-namespace
exception applies; an over-full store refuses as NamespaceCapacity for every
candidate until recovery reduces it. A law fills the pool to 4,097 namespaces
after publishing generation one and requires a successor in the existing
namespace to refuse with an unchanged witness; it failed before this change
and passes now. Exactly 4,096 still admits a successor (existing law).

Codex review, third pass (filesystem_retention_namespace.rs).
Refs #78
FilesystemStoreMigrationAuthority admitted the published version-one root
shape but never looked inside staging. A store carrying current.seg or
current.cat from an interrupted version-one publication could therefore
publish its migration intent, marker, and receipt with that recovery evidence
still in place; afterwards the version-one recovery constructors refuse the
version-two markers and version-two stage recovery does not yet exist, so a
recoverable crash state became stranded.

verify_namespace now opens staging without following links and requires it to
be empty before the intent is observed, refusing at the Namespace boundary
with the reason. A law plants a retained stage and requires observe_intent to
refuse with the version-one witness unchanged and no intent or intent stage
written; it failed before this change and passes now. The retention fixtures,
which migrate a clean store, are unaffected.

Codex review, third pass (filesystem_migration_authority.rs).
Refs #78
… construction

FilesystemVersionTwoAdmission::reopen admitted every version-two directory and
record, then returned a value holding only the writer lock. The publication
authority reopened retention, roots, and manifests by name, so a directory
renamed and replaced between reopen and open was opened without repeating
admission; the advisory lock does not prevent such a replacement.

The admission now retains the three capabilities it admitted and hands them to
the authority through into_parts, so the authority never resolves those names
itself. Current-state verification additionally requires each name to still
resolve to the pinned directory, refusing with the new
ProtocolDirectoryReplaced when the namespace no longer describes the admitted
state, so a swapped-in directory is neither opened nor published into. A law
publishes generation one, reopens, swaps retention out from under the
admission, and requires open to succeed against the pinned directory and
verification to refuse; it failed before this change and passes now.

Codex review, second pass (filesystem_version_two_admission.rs), P1.
Refs #78
require_current_catalog bound this store's catalog HEAD to the generation and
digest the closure was verified against, but it reopened only HEAD. A
preparation verified from a byte-identical store, or before this store lost
its catalog pool entry, still published: the head agreed while the catalog it
named was absent or corrupt, and the receipt cited evidence these pools could
not reproduce.

Publication now also reopens the catalog pool entry HEAD selects, bounded by
the head's declared length through the shared exact-record reader, decodes it,
and requires its generation and digest to equal the head's, refusing with
CatalogAbsent, CatalogRefused { source }, or CatalogChanged. A law deletes the
selected catalog after the snapshot and requires CatalogAbsent; it failed
before this change and passes now.

Closure-member segments are deliberately not re-read at publication: every
read authenticates them, and their re-verification under authority belongs to
retention recovery. requirements.md records the nonclaim.

Codex review, third pass (filesystem_retention_catalog.rs), P1.
Refs #78
@flyingrobots
flyingrobots merged commit 80ef9a1 into main Sep 8, 2026
5 checks passed
@flyingrobots
flyingrobots deleted the feature/retention-namespace-transitions branch September 8, 2026 06:18
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.

Implement retention namespace generation transitions

1 participant