fix(durability): three block-layer power-loss findings from the first fault-injection box run — all cured, matrix 15/15
All checks were successful
CI / Node 22 (push) Successful in 12m20s
CI / Node 24 (push) Successful in 12m7s
CI / Bun (latest) (push) Successful in 12m21s

An internal cross-engine fault-injection run (frozen-platter power-loss
capture) surfaced three release-gating findings; each cured in its owning
layer, each pinned:

1. WHOLE-LOG REPLAY ON UNCLEAN OPEN (the big one): log-authority replay
   only covered facts ABOVE the manifest — but live canonical entity
   writes are tmp+rename without per-file fsync, and the group-commit
   flush syncs staging + manifest, never the live tree. Power loss could
   therefore vaporize acked canonical bytes BELOW the manifest while the
   log held every fact scan-clean (measured: 299 of 301 acks lost).
   Now: a clean close stamps a clean-shutdown marker (fsynced, written
   last); every open consumes it; an UNCLEAN open under log authority
   folds the ENTIRE log into canonical — whole-entity after-images make
   the re-apply idempotent and byte-safe. Zero cost on the happy path;
   crash recovery pays one narrated fold. Recovery is replay: a crash is
   just bigger lag.

2. TORN WRITER LOCK: power loss legally leaves the lock file present but
   empty; the parse failure read as 'no holder' while the O_EXCL claim
   EEXISTed forever — a PERMANENT lockout no staleness check could clear.
   An unparseable lock is stale by definition (no live holder has one):
   unlink loudly and re-loop; a racer rewriting a valid lock first wins.

3. PAIR GUARD: flush() called metadataIndex.stampWatermark unguarded;
   a replacement metadata provider without the method killed the pair at
   first flush. All three stamp calls are optional-chained — a missing
   stamp is a verdict-side rescan, never a flush crash.

Pins: whole-log fold restores rows vanished below the manifest ·
clean-shutdown marker lifecycle (stamp/consume/re-stamp) · torn-lock
recovery with a fresh write after · stampless-provider flush.
Gates: unit 2055/2055 · integration 824 · kill-matrix 15/15.
This commit is contained in:
David Snelling 2026-08-10 14:48:32 -07:00
parent d1698fa5be
commit 67c606be69
4 changed files with 180 additions and 14 deletions

View file

@ -11247,7 +11247,10 @@ export class Brainy<T = any> implements BrainyInterface<T> {
{
const wmGen = this.storage?.committedGeneration?.() ?? null
if (wmGen !== null) {
this.metadataIndex.stampWatermark(wmGen)
// ALL THREE optional-chained: a replacement provider (the native
// pair swaps these managers) may not carry the stamp method — a
// missing stamp is a verdict-side rescan, never a flush crash.
;(this.metadataIndex as { stampWatermark?: (g: number) => void }).stampWatermark?.(wmGen)
;(this.index as { stampWatermark?: (g: number) => void }).stampWatermark?.(wmGen)
;(this.graphIndex as { stampWatermark?: (g: number) => void }).stampWatermark?.(wmGen)
}