From da0b113fc5ba42c38c8278eaaa44f8922269d5c0 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 2 Sep 2026 08:07:13 +0200 Subject: [PATCH] docs(core): admit S5-B2 race-free AuthoritySnapshot acquisition Closes the exact gap PR #564's S5-A baseline left as an explicit blocker: the reader algorithm's snapshot-capture and retention- reference-registration steps were not specified as one atomic operation, so a reader descheduled between them could have its retention reference arrive after the generation it named was already reclaimed. Admits AuthoritySnapshotGuard: acquire_authority_snapshot_guard() reads the current committed-root generation handle and increments its reference count as one operation indivisible with respect to a concurrent root commit's replacement of that handle. Specifies the publication ordering for the in-memory current cell relative to the parent contract's step-F durable commit, and that it holds no state across a restart (repopulated from the durable committed_root at startup, before any guard can be acquired). Extends reclamation eligibility transitively: a guard-pinned root generation stays retained regardless of how many further commits have occurred since acquisition, and catalog/marker/data generations reachable from it inherit retention through the parent contract's existing "referenced by a retained root" rule - no new per-child pin accounting is introduced. Makes explicit that all three reclamation conditions (durable retention, zero guard references, and any other admitted recovery reason) must hold together, including after a restart. Updates the parent contract's reader-algorithm steps 2/7, S5-B2 blocker paragraph, header status flags, and section 21 to record S5_B2_ADMITTED = YES, and the migration ledger's row 10 accordingly. S5-B1 and S5-B3 remain open. --- docs/native/CORE-MIGRATION-LEDGER.md | 2 +- docs/native/R15-SECURE-STORAGE-CONTRACT.md | 17 +- .../native/r15/AUTHORITY-SNAPSHOT-LIFETIME.md | 246 ++++++++++++++++++ 3 files changed, 255 insertions(+), 10 deletions(-) create mode 100644 docs/native/r15/AUTHORITY-SNAPSHOT-LIFETIME.md diff --git a/docs/native/CORE-MIGRATION-LEDGER.md b/docs/native/CORE-MIGRATION-LEDGER.md index 35298c038..f459bfc52 100644 --- a/docs/native/CORE-MIGRATION-LEDGER.md +++ b/docs/native/CORE-MIGRATION-LEDGER.md @@ -17,7 +17,7 @@ scope shifts — it is a living decision record, not a one-time snapshot. | 7 | `features/project/` domain logic | TS, `features/project/` (24 files, 2,114 lines) — real logic concentrated in `thunks/` + `projectSelectors.ts` (~450-500 lines); `reducers/` (11 files) is CRUD bookkeeping | High — Redux-store-shape/dispatch bound; `reducers/` stays TS-side permanently | Low | Medium (import/restore orchestration) | Low-medium | Medium (only the thunks/selectors subset) | Deferred | Candidate after the schema crate is proven; only thunks/selectors, never `reducers/` | Not started | | 8 | AI services | TS, `services/ai/` (44 files, 5,401 lines), mixed portability (retry/routing/error-taxonomy renderer-neutral vs. `computeShaderFactory.ts`/`webGpuDetectorService.ts`/`.wgsl` inherently WebGPU-coupled) | Mixed | Medium-high (API keys) | Low-medium | Medium | Uncertain — too large/mixed to assess narrowly | **Out of scope for all of Wave 2** | None proposed | None | | 9 | Project state-shape compatibility adapter | TS, `features/project/coreBoundaryAdapter.ts` at the Core boundary + Rust, `crates/worldscript-project` schema | High at the boundary — production Redux `EntityState` must be translated without importing Redux into Core | Low | High — ID/order preservation is part of project identity | Medium | High — every native renderer needs the same conversion contract | **2 — Wave 2 prerequisite before G1 evaluation** | **In progress — typed adapter, fixtures, and the first observation-only desktop shadow caller are locally proven; no authority switch**; normalizes array or Redux `EntityState` to renderer-neutral arrays and reconstructs the TS-side shape only at the integration boundary. The Rust verdict is partial because unknown fields are not rejected, and the envelope's `schemaVersion` is synthesized rather than persisted. | `tests/unit/features/project/coreBoundaryAdapter.test.ts` covers array and `EntityState` inputs, round-trip ID/order preservation, and rejection of duplicate IDs, missing references, and orphaned entities for both characters and worlds; the envelope fixture is accepted by Rust after migration and validation | -| 10 | R-15 protected desktop storage contract | **Design only (S5-A baseline)**, `docs/native/R15-SECURE-STORAGE-CONTRACT.md`; current desktop records remain TS/Tauri filesystem authority | High — future Core must serve Tauri and Qt without renderer-private crypto semantics | High | High — durability, migration, and identity binding protect user data | High | **Highest — cross-renderer security/durability contract** | **3 — S5-A design admitted; three blocking child contracts (S5-B1, S5-B2, S5-B3) and implementation not started** | **S5_A_ADMITTED=YES / S5_IMPLEMENTATION_READY=NO / S5_TERMINAL=NO**; inventory, identity/AAD envelope, key epochs, fail-closed reads, durable replacement, crash-resumable migration, and unified admission are specified. Canonical Migration Source & Payload Evidence (S5-B1), race-free `AuthoritySnapshot` acquisition (S5-B2), and the chunked large-object envelope for records above 64 MiB (S5-B3) remain explicit fail-closed gates, not designed here. No production authority switch or plaintext migration is claimed. | S5-B1, S5-B2, and S5-B3 admitted and merged; then headless Core vectors, fault-injection tests, per-record migration tests, packaged durability evidence, and explicit #357/#359/#360/#361 reconciliation required before implementation gates can close | +| 10 | R-15 protected desktop storage contract | **Design only (S5-A baseline)**, `docs/native/R15-SECURE-STORAGE-CONTRACT.md`; current desktop records remain TS/Tauri filesystem authority | High — future Core must serve Tauri and Qt without renderer-private crypto semantics | High | High — durability, migration, and identity binding protect user data | High | **Highest — cross-renderer security/durability contract** | **3 — S5-A design admitted, S5-B2 admitted; two blocking child contracts (S5-B1, S5-B3) remain** | **S5_A_ADMITTED=YES / S5_B2_ADMITTED=YES / S5_IMPLEMENTATION_READY=NO / S5_TERMINAL=NO**; inventory, identity/AAD envelope, key epochs, fail-closed reads, durable replacement, crash-resumable migration, unified admission, and race-free `AuthoritySnapshot` acquisition/lifetime (`docs/native/r15/AUTHORITY-SNAPSHOT-LIFETIME.md`) are specified. Canonical Migration Source & Payload Evidence (S5-B1) and the chunked large-object envelope for records above 64 MiB (S5-B3) remain explicit fail-closed gates, not designed here. No production authority switch or plaintext migration is claimed. | S5-B1 and S5-B3 admitted and merged; then headless Core vectors, fault-injection tests, per-record migration tests, packaged durability evidence, and explicit #357/#359/#360/#361 reconciliation required before implementation gates can close | ## Decisions this table records diff --git a/docs/native/R15-SECURE-STORAGE-CONTRACT.md b/docs/native/R15-SECURE-STORAGE-CONTRACT.md index 6ca8f513f..b9e3d8039 100644 --- a/docs/native/R15-SECURE-STORAGE-CONTRACT.md +++ b/docs/native/R15-SECURE-STORAGE-CONTRACT.md @@ -4,7 +4,7 @@ **Status:** S5-A — admitted R-15 secure-storage architecture baseline; production implementation not started. `S5_A_ADMITTED = YES`, `S5_IMPLEMENTATION_READY = NO`, `S5_TERMINAL = NO`, -`PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO`. Three blocking S5 child contracts remain, each an explicit fail-closed gate rather than an implicit gap: **S5-B1** (Canonical Migration Source & Payload Evidence — plaintext packaged-IDB source evidence, per-class canonical destination-payload bytes, `source_value_digest` value-equivalence, surviving atomic-write-temporary reconciliation, and identity-upgrade/recovery for unbound AAD-less legacy sources and unidentified legacy quarantine data, §10.1.2, §10.1.3, §10.4.1); **S5-B2** (race-free `AuthoritySnapshot` acquisition/lifetime/reclamation, §5.3.3); **S5-B3** (Chunked Large-Object Envelope for records above the `64 MiB` whole-record limit, §13). S5 is terminal only once S5-A, S5-B1, S5-B2, and S5-B3 are all merged and post-merge green; none of the three child contracts' mechanisms are designed in this baseline. +`PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO`. `S5_B2_ADMITTED = YES` (`docs/native/r15/AUTHORITY-SNAPSHOT-LIFETIME.md` — race-free `AuthoritySnapshot` acquisition/lifetime/reclamation, §5.3.3). Two blocking S5 child contracts remain, each an explicit fail-closed gate rather than an implicit gap: **S5-B1** (Canonical Migration Source & Payload Evidence — plaintext packaged-IDB source evidence, per-class canonical destination-payload bytes, `source_value_digest` value-equivalence, surviving atomic-write-temporary reconciliation, and identity-upgrade/recovery for unbound AAD-less legacy sources and unidentified legacy quarantine data, §10.1.2, §10.1.3, §10.4.1); **S5-B3** (Chunked Large-Object Envelope for records above the `64 MiB` whole-record limit, §13). S5 is terminal only once S5-A, S5-B1, S5-B2, and S5-B3 are all merged and post-merge green; S5-B1's and S5-B3's mechanisms are not designed in this baseline. **Baseline:** `main` at `7ce506ee771f6273e22c08ded049b48955cb40a5` @@ -703,14 +703,13 @@ filesystem slot is never read authority by itself (per §5.3.1's "meaning of `CO ```text 1. acquire shared read admission -2. capture AuthoritySnapshot from the secure anchor and acquire its retention reference - (S5-B2, below — steps 2's capture and its retention reference are NOT yet specified as one - atomic acquisition in this S5-A baseline) -3. resolve ONLY snapshot.committed_root.root_key_ref -4. authenticate the exact root named by the snapshot +2. guard := acquire_authority_snapshot_guard() — atomic capture + retention-reference registration + (S5-B2, `docs/native/r15/AUTHORITY-SNAPSHOT-LIFETIME.md` §2) +3. resolve ONLY guard.committed_root.root_key_ref +4. authenticate the exact root named by the guard's snapshot 5. use that root's catalog/marker/data generations 6. decrypt + validate + hand off payload -7. release the retention reference +7. release(guard) 8. release shared read admission ``` @@ -737,7 +736,7 @@ lane reuse may occur only after no admitted reader can still reference the gener generation-addressed root representation when reusing a lane, and must never overwrite bytes a pinned reader snapshot still references. -**S5-B2 blocker — race-free acquisition not yet admitted.** `committed_root` remains the sole publication authority and the immutable read-snapshot semantics above remain admitted, but this S5-A baseline does **not** admit a race-free acquisition mechanism between snapshot capture and retention-reference registration (reader algorithm step 2, above): a reader descheduled in that gap can have its retention reference arrive after two further root commits already reclaimed the generation it names — a gap in *acquisition*, not *retention*, that no reference counting or generation-addressed layout closes merely by existing. No production implementation may claim `AuthoritySnapshot`/GC lifetime conformance until the dedicated **S5-B2** child contract admits atomic guarded acquisition (preferred direction: an `AuthoritySnapshotGuard` atomically capturing `committed_root` and registering its retention reference before the snapshot can become reclaimable); that mechanism is not designed in this S5-A baseline. +**S5-B2 admitted — race-free acquisition.** `docs/native/r15/AUTHORITY-SNAPSHOT-LIFETIME.md` (S5-B2) admits the atomic `AuthoritySnapshotGuard` acquisition this baseline originally left as an explicit blocker: reader algorithm step 2 (above) is `guard := acquire_authority_snapshot_guard()`, an operation indivisible with respect to a concurrent root commit's replacement of the current generation handle, closing the capture-to-registration race a reader could otherwise be descheduled inside. Reclamation eligibility (§3 of that document) extends this section's `ACTIVE_READER_PIN` reason precisely: a generation's reference count, maintained by the guard mechanism, must be zero in addition to satisfying this section's own retention conditions. ### 5.4 Canonical digest contract @@ -3157,4 +3156,4 @@ complete merely because a design document exists. ## 21. S5 admission decision -This S5-A baseline is admitted at the semantic level for everything it actually specifies (protected records/representations enumerated; logical identity, envelope, key/epoch, parse, failure, and downgrade semantics explicit; durable writes, generations/commit markers, admission, lock, recovery, and memory bounds defined; Core-vs-platform responsibilities and headless tests explicit; #357/#359/#360/#361 have implementation owners and closure evidence) but is **not** implementation-ready as a whole: `canonical_destination_payload_bytes`/plaintext-IDB source evidence (**S5-B1**), race-free `AuthoritySnapshot` acquisition (**S5-B2**), and the chunked large-object envelope for records above `64 MiB` (**S5-B3**) remain explicit fail-closed gates this baseline does not admit (above). This is **`S5_A_ADMITTED / CONTRACT_DEFINED / IMPLEMENTATION_NOT_STARTED`**, not `DESIGN_ADMITTED`/`IMPLEMENTATION_READY` for the whole S5 program; current desktop filesystem authority remains unchanged and current user data is not retroactively encrypted by S5-A. +This S5-A baseline is admitted at the semantic level for everything it actually specifies (protected records/representations enumerated; logical identity, envelope, key/epoch, parse, failure, and downgrade semantics explicit; durable writes, generations/commit markers, admission, lock, recovery, and memory bounds defined; Core-vs-platform responsibilities and headless tests explicit; #357/#359/#360/#361 have implementation owners and closure evidence) but is **not** implementation-ready as a whole: `canonical_destination_payload_bytes`/plaintext-IDB source evidence (**S5-B1**) and the chunked large-object envelope for records above `64 MiB` (**S5-B3**) remain explicit fail-closed gates this baseline does not admit; race-free `AuthoritySnapshot` acquisition (**S5-B2**) is now admitted (above). This is **`S5_A_ADMITTED / CONTRACT_DEFINED / IMPLEMENTATION_NOT_STARTED`**, not `DESIGN_ADMITTED`/`IMPLEMENTATION_READY` for the whole S5 program; current desktop filesystem authority remains unchanged and current user data is not retroactively encrypted by S5-A. diff --git a/docs/native/r15/AUTHORITY-SNAPSHOT-LIFETIME.md b/docs/native/r15/AUTHORITY-SNAPSHOT-LIFETIME.md new file mode 100644 index 000000000..e5865b2eb --- /dev/null +++ b/docs/native/r15/AUTHORITY-SNAPSHOT-LIFETIME.md @@ -0,0 +1,246 @@ +# S5-B2 — AuthoritySnapshot Lifetime & Reclamation + +**Issue:** [#578](https://github.com/qnbs/WorldScript-Studio/issues/578), child of [#445](https://github.com/qnbs/WorldScript-Studio/issues/445) + +**Status:** `S5_B2_ADMITTED = YES`. Admits the race-free `AuthoritySnapshotGuard` acquisition +mechanism that `docs/native/R15-SECURE-STORAGE-CONTRACT.md` §5.3.3 explicitly left unadmitted (its +"S5-B2 blocker" paragraph). Production implementation not started. + +**Baseline:** `docs/native/R15-SECURE-STORAGE-CONTRACT.md` at the S5-A baseline merged in PR #564 +(`main` commit `3e89e483`). This document extends that contract's §5.3.1–§5.3.3 without +renegotiating any of their admitted semantics; every term below (`committed_root`, +`AuthoritySnapshot`, `root_generation`, `ACTIVE_READER_PIN`, §5.5's retention rule) is the exact term +S5-A already defines. Where this document says "the parent contract," it means that file. + +**Scope:** Race-free `AuthoritySnapshot` acquisition, retention-reference lifetime, generation +reclamation eligibility, reader lifetime, GC interaction, and crash/process-lifetime implications. +Nothing else — this document does not reopen the two-phase secure-anchor commit protocol (§5.3.1), +the digest contract (§5.4), or any other S5-A section. + +## 1. The gap this document closes + +The parent contract's reader algorithm (§5.3.3) states: + +```text +1. acquire shared read admission +2. capture AuthoritySnapshot from the secure anchor and acquire its retention reference +3. resolve ONLY snapshot.committed_root.root_key_ref +... +``` + +S5-A explicitly left step 2's two actions — capturing the snapshot and registering the retention +reference that keeps its generation alive — unspecified as a single atomic operation. If a reader is +descheduled between them while two further root commits land, its eventual retention reference can +arrive after the generation it names was already reclaimed: a gap in *acquisition*, not *retention*. +No reference counting, epoch scheme, or generation-addressed file layout closes this gap merely by +existing, because §5.5's retention rule and this document's `ACTIVE_READER_PIN` reason both answer +"what stays alive," never "how do you register a pin without a window where it can be missed." + +This document defines that missing atomic operation and nothing more. It does not change what stays +alive (§5.5, extended by the parent contract's `ACTIVE_READER_PIN`) or how a root is published +(§5.3.1 step F) — only how a reader safely joins that already-admitted retention set. + +## 2. `AuthoritySnapshotGuard` + +**Model.** The secure anchor holds its current committed root behind a single atomically-swappable +cell — the exact mechanism is an implementation's own choice (a mutex-guarded reference-counted +handle, a lock-free atomic-swap of a reference-counted handle, or an equivalent construct), but it +MUST provide the following two operations with the atomicity guarantee stated below: + +```text +acquire_authority_snapshot_guard() -> AuthoritySnapshotGuard + = read the current committed-root generation handle + + increment that exact handle's own reference count + as one operation indivisible with respect to a concurrent + commit's replacement of the current handle + +release(guard: AuthoritySnapshotGuard) + = decrement the referenced handle's reference count +``` + +**Reference implementation sketch (illustrative, not normative wire format).** A conformant Rust +implementation is, for example: + +```text +struct RootGenerationHandle { root_generation, root_digest, root_slot, root_key_ref, ... } + +current: Mutex> // or an equivalent lock-free ArcSwap + +fn acquire_authority_snapshot_guard() -> Arc { + current.lock().clone() // Arc::clone increments the strong count atomically +} // while holding the same lock a commit's replace() uses + +fn commit_new_root(next: RootGenerationHandle) { + *current.lock() = Arc::new(next); // old Arc's count is now driven only by outstanding guards +} + +fn release(guard: Arc) { + drop(guard); // Arc's Drop decrements the count; reaching zero is the reclamation signal (§3) +} +``` + +`Arc::clone()` performed while holding the same synchronization primitive (`Mutex`, or an +equivalent single atomic swap point) a writer uses to replace the cell is what provides the +indivisibility in the atomicity requirement below — the clone cannot observe a handle after a +concurrent replace has already dropped the writer's own reference to it, because the replace cannot +proceed until the lock is released. This generalizes without modification to N concurrent readers +racing M concurrent commits: each `clone()` is independently atomic with respect to each `replace()`, +and `Arc`'s reference count is the single source of truth every acquisition and every release agree +on, so no coordination beyond the lock/atomic-swap itself is required for correctness at any +reader/writer concurrency level. + +**Publication ordering for `current`.** The parent contract's §5.3.1 step F is the sole durable +publication point; this document's `current` cell is exactly the in-memory reflection of +`secure_anchor.committed_root` for readers, and its replacement (`commit_new_root()` above) MUST +happen as part of the same critical section step F's own implementation uses to durably advance +`committed_root` — never before F durably completes (which would let a guard acquire a +`RootGenerationHandle` for a root that is not yet committed, violating §5.3.1's own ordering) and +never lagging after it completes (which would let a reader admitted after F, per the parent +contract's publication-point rule, still observe the old handle). Concretely: whatever serializes +step F's own durable write (the parent contract's `root_commit_mutex`, §11.1) also guards the +`current` replacement, so "F is durably complete" and "`current` names the new generation" become +one indivisible event from every other thread's perspective. On process restart, `current` holds no +state at all (§4) — Core startup populates it from the durably authenticated `committed_root` via +the parent contract's own cold-start algorithm (§5.3.1), before any caller may reach +`acquire_authority_snapshot_guard()`; there is no separate or divergent bootstrap path for `current`. + +**Atomicity requirement (the actual fix).** A concurrent root commit (§5.3.1 step F) replaces the +secure anchor's current-handle cell with a new handle for the new generation. `acquire_authority_ +snapshot_guard()` MUST observe exactly one of two outcomes with no third possibility: + +- it reads the **prior** handle and increments the prior handle's own reference count, in which case + step F's replacement — whenever it durably completes — does not, by itself, make the prior + generation unreferenced (this guard's increment already counts against it); or +- it reads the **new** handle (step F has already completed) and increments the new handle's own + reference count. + +It MUST NOT be possible for the operation to read a handle whose reference count has already reached +zero and been finalized (§3) — equivalently, "read the handle" and "increment its count" are never +separated by a window in which a concurrent commit-and-reclaim sequence can complete against that +same handle. A reference-counted handle behind a single lock or a single atomic swap satisfies this +by construction: cloning a reference-counted pointer while holding the same synchronization primitive +a writer uses to replace it can only observe the pointer that existed at that instant, and the clone's +own increment is visible to any reclamation check that inspects the count afterward — there is no +instant at which the pointer is readable but its target's count is not yet incremented. + +**No hazard pointers or RCU required.** This is intentional: reference counting behind one +synchronization point already provides the atomicity above without a separate memory-reclamation +scheme. Do not introduce hazard pointers, epoch-based reclamation, or RCU for `AuthoritySnapshotGuard` +unless a future revision demonstrates the reference-counted model is insufficient for a specific, +named implementation constraint — this is a hard constraint on scope, not a default recommendation +among equals. + +**Guard scope.** One guard corresponds to exactly one generation handle (the one captured at +acquisition). The parent contract's reader algorithm step 2 becomes, verbatim: + +```text +2. guard := acquire_authority_snapshot_guard() -- atomic capture + pin, this document +``` + +and step 7 (`release the retention reference`) becomes `release(guard)`. No other reader-algorithm +step changes. + +## 3. Reclamation eligibility + +A generation (authority-root, catalog-page, marker, key-epoch control, ordinary data, or any +migration/control generation reachable from a committed root, per the parent contract's "Retention +for pinned reads" paragraph) is reclaimable only when **all** of the following hold together — +satisfying any subset is never sufficient: + +1. **Root retention.** It is not reachable from the current `committed_root`, the previous + `committed_root` (before the most recent publication), a `prepared_root_commit` if present, **or + the specific root generation any live `AuthoritySnapshotGuard` pins** — this last clause extends + the parent contract's §5.5 retention set for exactly as long as a guard is live, regardless of how + many further commits have occurred since acquisition: a guard pinning root generation `N` keeps + `N` in this retained-root set even after commits advance the current generation to `N+1`, `N+2`, + and beyond, so it never silently falls out of retention merely because it is no longer "current" or + "previous." A catalog-page, marker, key-epoch, or data generation is retained by this same + condition transitively, exactly as §5.5 already defines "referenced by a retained root" for the + current/previous/prepared cases — extending which roots count as retained is sufficient; no + separate per-child-generation pin accounting is needed or introduced. +2. **Zero guard references** on the generation's own root handle — this document's reference count + for that handle is exactly zero (only meaningful for authority-root generations themselves; a + catalog/marker/data generation's eligibility is governed by condition 1's transitive test, not by + holding its own reference count). +3. No other admitted recovery-retention reason applies (the parent contract may extend this list in + the future; this document does not enumerate it exhaustively) — this condition is independent of + and never subsumed by conditions 1 or 2. + +Condition 2 (for a root generation) is `ACTIVE_READER_PIN` from the parent contract's §5.3.3, made +precise: "no admitted reader can still reference the generation" means "the generation handle's +reference count, maintained by this document's guard mechanism, is zero." A root generation failing +condition 1 or condition 2 remains retained exactly as if it were still the previous committed root — +GC treats a guard-extended root, an `ACTIVE_READER_PIN`, and an unresolved condition 3 all with equal +force; none is a softer or best-effort hint, and none may be checked in isolation from the other two. + +**Two-lane physical reuse.** The parent contract already requires that an implementation using two +physical root-slot lanes (`ROOT_SLOT_A`/`ROOT_SLOT_B`) create a new generation-addressed +representation rather than overwrite a lane a pinned snapshot still references. This document's +reference count is the exact signal that decision depends on: a lane whose most recent occupant still +has a nonzero reference count is not eligible for in-place reuse, regardless of how long the lane has +been physically idle. + +**Finalization is not deletion.** Satisfying all three conditions above makes a generation *eligible* +for reclamation; it does not require immediate physical deletion. An +implementation may batch, defer, or schedule the actual byte-level cleanup, exactly as the parent +contract already treats cleanup as separately retryable elsewhere (§9, §10.3's `RETIRE_OLD_ +AUTHORITY`/`FINALIZE` phases). This document constrains only when deletion becomes *permitted*, never +when it must occur. + +## 4. Crash and process-lifetime semantics + +`AuthoritySnapshotGuard` and its reference counts are pure in-memory constructs with **no durable +representation of any kind** — they are never written to the secure anchor, the authority root, a +journal, or any other persisted structure. This is a deliberate simplification, not an oversight: + +- A process crash or restart releases every outstanding guard implicitly, because the in-memory + reference counts holding them cease to exist along with the process. There is nothing to recover, + resume, or reconcile for guard state specifically. +- On restart, condition 2 trivially holds (zero guards, because no guards survive a restart) and the + guard-extended clause of condition 1 contributes no additional retained roots on a fresh process — + but condition 1's durable §5.5 retention rule and condition 3 (any other admitted recovery-retention + reason) are unaffected by the restart and still both apply exactly as before it; a restart never + reduces §3 to condition 1 alone. +- No implementation may persist a guard, a reference count, or any derived "reader still active" + marker across a restart, and no implementation may treat the *absence* of persisted guard state as + evidence that reclamation was safe at some point *before* the crash — the durable retention rule + (§5.5) is the only cross-restart authority, exactly as the parent contract already requires for + every other authority decision. +- A multi-process or multi-tab scenario (the packaged desktop app run twice, or a future + multi-window Core) is out of this document's scope: `AuthoritySnapshotGuard` as specified here is a + single-process, in-memory mechanism. Coordinating reader pins across process boundaries — if ever + required — needs its own explicit contract and is not implied or half-specified here. + +## 5. What this document does not change + +- The two-phase secure-anchor commit protocol (§5.3.1) — publication still happens at step F, exactly + as before. +- §5.5's durable retention conditions (previous/current/prepared root) — this document adds a + reference-counted precondition on top of them, never a replacement. +- The `AuthoritySnapshot` struct's own fields (§5.3.3) — unchanged. +- Any S5-B1 or S5-B3 domain — migration-source evidence and chunked large-object envelopes are + unrelated concerns. + +## 6. Required proof (headless, before production admission) + +- A golden-vector or property test demonstrating: N concurrent `acquire_authority_snapshot_guard()` + calls racing M concurrent root commits never observe a handle whose reference count was already + finalized to zero — i.e., every acquired guard's generation remains valid for the guard's full + lifetime, regardless of interleaving. +- A fault-injection test simulating a reader descheduled between "read current handle" and "increment + its count" (or the equivalent boundary for the chosen implementation) under a concurrent commit, + proving the atomicity requirement in §2 holds and not merely "usually holds." +- A reclamation test proving a generation with a nonzero reference count is never selected for + physical deletion, and a generation satisfying all of §3's conditions eventually becomes eligible + (no permanent leak from guards that are properly released). +- A process-restart test proving no in-memory guard state is assumed, expected, or required to + survive a restart, and that reclamation after restart proceeds from §5.5's durable rule alone. +- The parent contract's §5.3.3 "S5-B2 blocker" paragraph, reader-algorithm steps 2/7, header status + flags, and §21 were updated to reference this document as admitted as part of this document's own + integration (same PR) — this is a completed integration step, not an outstanding proof item. + +## 7. Non-goals + +No implementation. No production authority switch. No change to any S5-A digest, envelope, or +migration semantics. No multi-process/multi-window reader coordination (§4). No hazard-pointer or +RCU design (§2) unless a future revision demonstrates necessity.