fix(log): acked writes survive power loss; rejected writes never silently commit — the kill-matrix goes 11/11 with zero .fails debt
Two release-blocking findings from the durability kill-matrix, both fixed
in the owning layer:
1. LOG-AUTHORITY REPLAY AT OPEN: durable-at-ack fsynced the fact before
the ack, but open() truncated every fact above the manifest — after a
power loss that takes the un-fsynced tmp+rename canonical bytes, the
acked write's ONLY durable copy was discarded. Now: under 'log'
authority, open() REPLAYS intact facts above the manifest into
canonical (FactLog.peekFactsAbove — CRC-gated, order-sorted) and
advances the manifest to cover them; tree-authority brains keep the
truncate contract they were promised. Pinned end to end: the power-loss
row constructs the exact disk state (fsynced log, vanished canonical
rename) and the acked write lives.
2. NO SILENT COMMIT: commitSingleOp buffered the generation BEFORE the
fact append; an append failure (ENOSPC) rejected the caller but the
next flush durably committed the generation with NO fact — a permanent
silent log gap. Now the failure path un-buffers and returns the counter
reservation: nothing commits, the log stays gap-free, and the canonical
execute-residue orphan is the documented crash-equivalent.
Plus: the kill-matrix itself (11 rows — every commit-path fault point ×
reopen-as-crash recovery contract, at-ack variants, disk-full row; five
new zero-cost faultPoint sites), the log-authority pin suite (oracle
green/red/state-differs, flip refusal, switch survives reopen, 9/9), and
the group-commit covering pins (5/5).
Gates: unit 2002/2002 (152 files) · integration 785 · conformance 27/27.
2026-08-10 09:29:21 -07:00
/ * *
* @module tests / helpers / durabilityKillMatrix
* @description Shared machinery for the durability kill - matrix suite
* ( tests / integration / durability - kill - matrix . test . ts ) : open filesystem brains
* with fully explicit durability ( no background cadence , no embedder ) , arm
* the generation store ' s test - only commit fault injector at one exact phase ,
* abandon a "crashed" brain the way a dead process would ( its RAM is gone ,
* nothing flushes , nothing closes ) , and read the fact log / on - disk state the
* recovery assertions pin .
*
* The crash model is PROCESS DEATH : in - memory state is lost , file bytes the
* process already handed to the OS survive . One helper additionally models
* POWER LOSS for a chosen entity by removing its canonical files — legal ,
* because single - op canonical writes are tmp + rename WITHOUT fsync , and a
* rename that was never fsynced may surface as "no directory entry" after
* power loss .
* /
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { Brainy } from '../../src/brainy.js'
import type { CommitFaultPhase , GenerationStore } from '../../src/db/generationStore.js'
/** The error a throwing fault injector uses to simulate a process crash. */
export class SimulatedCrash extends Error {
constructor ( phase : CommitFaultPhase ) {
super ( ` simulated process crash at ${ phase } ` )
this . name = 'SimulatedCrash'
}
}
/** Deterministic 384-dim vector so no test ever invokes the embedder. */
export function vec ( seed : number ) : number [ ] {
return Array . from ( { length : 384 } , ( _ , i ) = > ( ( seed * 31 + i * 7 ) % 100 ) / 100 )
}
/ * *
* Map a readable label to a deterministic UUID - shaped id ( entity ids must be
* UUIDs — the sharded storage layout derives the shard from the UUID hex ) .
* /
export function uid ( label : string ) : string {
let h1 = 0x811c9dc5
for ( let i = 0 ; i < label . length ; i ++ ) {
h1 = Math . imul ( h1 ^ label . charCodeAt ( i ) , 0x01000193 ) >>> 0
}
let h2 = 0xdeadbeef
for ( let i = label . length - 1 ; i >= 0 ; i -- ) {
h2 = Math . imul ( h2 ^ label . charCodeAt ( i ) , 0x85ebca6b ) >>> 0
}
const hex = h1 . toString ( 16 ) . padStart ( 8 , '0' ) + h2 . toString ( 16 ) . padStart ( 8 , '0' )
return ` 00000000-0000-4000-8000- ${ hex . slice ( 0 , 12 ) } `
}
/** Create a fresh temp directory for one brain's storage root. */
export function makeTempDir ( ) : string {
return fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'brainy-kill-matrix-' ) )
}
/ * *
* Open a writer brain over ` dir ` with every implicit durability knob off :
* persistence policy 'manual' ( the engine never flushes on its own , so every
* durable transition in a test is an explicit ` flush() ` / commit ) , deterministic
feat(log): log authority is the fleet default — adopt-at-open, oracle-gated; plus the power-cut throw-site cures and the loud torn-record contract
THE DEFAULT FLIP (ruled on proven evidence — at-ack survived 301/301
acked-writes-through-power-cut in block-layer fault injection; deferred
tree authority demonstrably loses flush-covered acks): a brain with NO
stored authority artifact now ADOPTS LOG AUTHORITY AT OPEN. The oracle
gates the flip exactly as the guarded adoption path always did — curable
divergences baseline-backfilled, the flip lands ONLY on a green verdict —
and a brain that cannot verify STAYS tree-authoritative loudly, with the
refusal recorded on the switch artifact so subsequent opens are cheap.
config logAuthority: 'defer' is the explicit documented opt-out (no
automatic adoption; declared flush-window loss; adoptLogAuthority() flips
later). A stored artifact always wins. RELEASES.md carries the posture.
Two standing .fails debt pins FLIP TO HOLDING under the default: the
at-ack crash-survival gap and the ack-at-log durability target — both now
permanent asserted truths, not aspirations.
POWER-CUT THROW SITES (fault-injection findings, brainy-alone config):
- A manifest-listed-but-unloadable column segment QUARANTINES at
discovery (loud once, counted always, quarantinedSegments() exposed for
the heal) and the field serves its remaining segments DEGRADED — never
a raw throw killing every query on the field. Real storage faults still
propagate untouched.
- Torn generation artifacts (NaN/garbage in manifest or counter) DISCARD
with narration at the store's open and recovery re-derives — plus a
defensive finite-integer guard at the init consumer. Never a RangeError
killing an open.
THE LOUD TORN-RECORD CONTRACT: an existing-but-unparseable stored record
now surfaces as a typed, counted TornRecordError on every entity-read
surface (including fifteen previously-blind per-item batch catches);
ENOENT stays clean-absent; artifact readers with designed absent-recovery
keep null-tolerance behind the loud floor. Disk corruption can no longer
read as silent data invisibility.
Suite migration: the default's pins inverted deliberately, generation
baselines made relative, quarantine-contract pins rewritten to the ruled
behavior.
Gates: tsc 0 · unit 2065/2065 (159 files) · integration 826 (93 files) ·
conformance 31/31 · kill-matrix 15/15 · torn-open guards 2/2.
2026-08-11 08:37:38 -07:00
* embeddings ( tests always pass explicit vectors anyway ) , silent logs — and
* ` logAuthority: 'defer' ` ( the explicit opt - out of the 10.0 . 0 adopt - at - open
* fleet default ) , so the durability POSTURE is explicit per row too : rows
* pinning deferred / tree recovery semantics get exactly that , and at - ack rows
* engage log authority via ` flipToAtAck ` . The fleet default ' s open - time
* adoption would inject a baseline - backfill generation into every floor
* computation and pre - flip every row .
fix(log): acked writes survive power loss; rejected writes never silently commit — the kill-matrix goes 11/11 with zero .fails debt
Two release-blocking findings from the durability kill-matrix, both fixed
in the owning layer:
1. LOG-AUTHORITY REPLAY AT OPEN: durable-at-ack fsynced the fact before
the ack, but open() truncated every fact above the manifest — after a
power loss that takes the un-fsynced tmp+rename canonical bytes, the
acked write's ONLY durable copy was discarded. Now: under 'log'
authority, open() REPLAYS intact facts above the manifest into
canonical (FactLog.peekFactsAbove — CRC-gated, order-sorted) and
advances the manifest to cover them; tree-authority brains keep the
truncate contract they were promised. Pinned end to end: the power-loss
row constructs the exact disk state (fsynced log, vanished canonical
rename) and the acked write lives.
2. NO SILENT COMMIT: commitSingleOp buffered the generation BEFORE the
fact append; an append failure (ENOSPC) rejected the caller but the
next flush durably committed the generation with NO fact — a permanent
silent log gap. Now the failure path un-buffers and returns the counter
reservation: nothing commits, the log stays gap-free, and the canonical
execute-residue orphan is the documented crash-equivalent.
Plus: the kill-matrix itself (11 rows — every commit-path fault point ×
reopen-as-crash recovery contract, at-ack variants, disk-full row; five
new zero-cost faultPoint sites), the log-authority pin suite (oracle
green/red/state-differs, flip refusal, switch survives reopen, 9/9), and
the group-commit covering pins (5/5).
Gates: unit 2002/2002 (152 files) · integration 785 · conformance 27/27.
2026-08-10 09:29:21 -07:00
* /
feat(log): log authority is the fleet default — adopt-at-open, oracle-gated; plus the power-cut throw-site cures and the loud torn-record contract
THE DEFAULT FLIP (ruled on proven evidence — at-ack survived 301/301
acked-writes-through-power-cut in block-layer fault injection; deferred
tree authority demonstrably loses flush-covered acks): a brain with NO
stored authority artifact now ADOPTS LOG AUTHORITY AT OPEN. The oracle
gates the flip exactly as the guarded adoption path always did — curable
divergences baseline-backfilled, the flip lands ONLY on a green verdict —
and a brain that cannot verify STAYS tree-authoritative loudly, with the
refusal recorded on the switch artifact so subsequent opens are cheap.
config logAuthority: 'defer' is the explicit documented opt-out (no
automatic adoption; declared flush-window loss; adoptLogAuthority() flips
later). A stored artifact always wins. RELEASES.md carries the posture.
Two standing .fails debt pins FLIP TO HOLDING under the default: the
at-ack crash-survival gap and the ack-at-log durability target — both now
permanent asserted truths, not aspirations.
POWER-CUT THROW SITES (fault-injection findings, brainy-alone config):
- A manifest-listed-but-unloadable column segment QUARANTINES at
discovery (loud once, counted always, quarantinedSegments() exposed for
the heal) and the field serves its remaining segments DEGRADED — never
a raw throw killing every query on the field. Real storage faults still
propagate untouched.
- Torn generation artifacts (NaN/garbage in manifest or counter) DISCARD
with narration at the store's open and recovery re-derives — plus a
defensive finite-integer guard at the init consumer. Never a RangeError
killing an open.
THE LOUD TORN-RECORD CONTRACT: an existing-but-unparseable stored record
now surfaces as a typed, counted TornRecordError on every entity-read
surface (including fifteen previously-blind per-item batch catches);
ENOENT stays clean-absent; artifact readers with designed absent-recovery
keep null-tolerance behind the loud floor. Disk corruption can no longer
read as silent data invisibility.
Suite migration: the default's pins inverted deliberately, generation
baselines made relative, quarantine-contract pins rewritten to the ruled
behavior.
Gates: tsc 0 · unit 2065/2065 (159 files) · integration 826 (93 files) ·
conformance 31/31 · kill-matrix 15/15 · torn-open guards 2/2.
2026-08-11 08:37:38 -07:00
export async function openBrain (
dir : string ,
opts ? : { logAuthority ? : 'adopt' | 'defer' }
) : Promise < Brainy > {
fix(log): acked writes survive power loss; rejected writes never silently commit — the kill-matrix goes 11/11 with zero .fails debt
Two release-blocking findings from the durability kill-matrix, both fixed
in the owning layer:
1. LOG-AUTHORITY REPLAY AT OPEN: durable-at-ack fsynced the fact before
the ack, but open() truncated every fact above the manifest — after a
power loss that takes the un-fsynced tmp+rename canonical bytes, the
acked write's ONLY durable copy was discarded. Now: under 'log'
authority, open() REPLAYS intact facts above the manifest into
canonical (FactLog.peekFactsAbove — CRC-gated, order-sorted) and
advances the manifest to cover them; tree-authority brains keep the
truncate contract they were promised. Pinned end to end: the power-loss
row constructs the exact disk state (fsynced log, vanished canonical
rename) and the acked write lives.
2. NO SILENT COMMIT: commitSingleOp buffered the generation BEFORE the
fact append; an append failure (ENOSPC) rejected the caller but the
next flush durably committed the generation with NO fact — a permanent
silent log gap. Now the failure path un-buffers and returns the counter
reservation: nothing commits, the log stays gap-free, and the canonical
execute-residue orphan is the documented crash-equivalent.
Plus: the kill-matrix itself (11 rows — every commit-path fault point ×
reopen-as-crash recovery contract, at-ack variants, disk-full row; five
new zero-cost faultPoint sites), the log-authority pin suite (oracle
green/red/state-differs, flip refusal, switch survives reopen, 9/9), and
the group-commit covering pins (5/5).
Gates: unit 2002/2002 (152 files) · integration 785 · conformance 27/27.
2026-08-10 09:29:21 -07:00
process . env . BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
const brain = new Brainy ( {
requireSubtype : false ,
storage : { type : 'filesystem' , path : dir } ,
silent : true ,
feat(log): log authority is the fleet default — adopt-at-open, oracle-gated; plus the power-cut throw-site cures and the loud torn-record contract
THE DEFAULT FLIP (ruled on proven evidence — at-ack survived 301/301
acked-writes-through-power-cut in block-layer fault injection; deferred
tree authority demonstrably loses flush-covered acks): a brain with NO
stored authority artifact now ADOPTS LOG AUTHORITY AT OPEN. The oracle
gates the flip exactly as the guarded adoption path always did — curable
divergences baseline-backfilled, the flip lands ONLY on a green verdict —
and a brain that cannot verify STAYS tree-authoritative loudly, with the
refusal recorded on the switch artifact so subsequent opens are cheap.
config logAuthority: 'defer' is the explicit documented opt-out (no
automatic adoption; declared flush-window loss; adoptLogAuthority() flips
later). A stored artifact always wins. RELEASES.md carries the posture.
Two standing .fails debt pins FLIP TO HOLDING under the default: the
at-ack crash-survival gap and the ack-at-log durability target — both now
permanent asserted truths, not aspirations.
POWER-CUT THROW SITES (fault-injection findings, brainy-alone config):
- A manifest-listed-but-unloadable column segment QUARANTINES at
discovery (loud once, counted always, quarantinedSegments() exposed for
the heal) and the field serves its remaining segments DEGRADED — never
a raw throw killing every query on the field. Real storage faults still
propagate untouched.
- Torn generation artifacts (NaN/garbage in manifest or counter) DISCARD
with narration at the store's open and recovery re-derives — plus a
defensive finite-integer guard at the init consumer. Never a RangeError
killing an open.
THE LOUD TORN-RECORD CONTRACT: an existing-but-unparseable stored record
now surfaces as a typed, counted TornRecordError on every entity-read
surface (including fifteen previously-blind per-item batch catches);
ENOENT stays clean-absent; artifact readers with designed absent-recovery
keep null-tolerance behind the loud floor. Disk corruption can no longer
read as silent data invisibility.
Suite migration: the default's pins inverted deliberately, generation
baselines made relative, quarantine-contract pins rewritten to the ruled
behavior.
Gates: tsc 0 · unit 2065/2065 (159 files) · integration 826 (93 files) ·
conformance 31/31 · kill-matrix 15/15 · torn-open guards 2/2.
2026-08-11 08:37:38 -07:00
persistence : { policy : 'manual' } ,
logAuthority : opts?.logAuthority ? ? 'defer'
fix(log): acked writes survive power loss; rejected writes never silently commit — the kill-matrix goes 11/11 with zero .fails debt
Two release-blocking findings from the durability kill-matrix, both fixed
in the owning layer:
1. LOG-AUTHORITY REPLAY AT OPEN: durable-at-ack fsynced the fact before
the ack, but open() truncated every fact above the manifest — after a
power loss that takes the un-fsynced tmp+rename canonical bytes, the
acked write's ONLY durable copy was discarded. Now: under 'log'
authority, open() REPLAYS intact facts above the manifest into
canonical (FactLog.peekFactsAbove — CRC-gated, order-sorted) and
advances the manifest to cover them; tree-authority brains keep the
truncate contract they were promised. Pinned end to end: the power-loss
row constructs the exact disk state (fsynced log, vanished canonical
rename) and the acked write lives.
2. NO SILENT COMMIT: commitSingleOp buffered the generation BEFORE the
fact append; an append failure (ENOSPC) rejected the caller but the
next flush durably committed the generation with NO fact — a permanent
silent log gap. Now the failure path un-buffers and returns the counter
reservation: nothing commits, the log stays gap-free, and the canonical
execute-residue orphan is the documented crash-equivalent.
Plus: the kill-matrix itself (11 rows — every commit-path fault point ×
reopen-as-crash recovery contract, at-ack variants, disk-full row; five
new zero-cost faultPoint sites), the log-authority pin suite (oracle
green/red/state-differs, flip refusal, switch survives reopen, 9/9), and
the group-commit covering pins (5/5).
Gates: unit 2002/2002 (152 files) · integration 785 · conformance 27/27.
2026-08-10 09:29:21 -07:00
} )
await brain . init ( )
return brain
}
/** Typed access to the brain's private generation store (test injection point). */
export function storeOf ( brain : Brainy ) : GenerationStore {
return ( brain as unknown as { generationStore : GenerationStore } ) . generationStore
}
/ * *
* Arm the commit fault injector to simulate a process crash at EXACTLY one
* phase ( all other phases pass through untouched ) . Returns the list of phases
* observed before ( and including ) the trip , so a test can assert the fault
* actually fired where intended .
* /
export function armCrash ( brain : Brainy , phase : CommitFaultPhase ) : { fired : CommitFaultPhase [ ] } {
const fired : CommitFaultPhase [ ] = [ ]
storeOf ( brain ) . setCommitFaultInjector ( ( p ) = > {
fired . push ( p )
if ( p === phase ) {
throw new SimulatedCrash ( p )
}
} )
return { fired }
}
/ * *
* Abandon a crashed brain the way process death would : its buffered RAM state
* is discarded and no background machinery may ever touch the storage
* directory again ( a dead process cannot flush ) . The fault injector stays
* installed so any in - flight commit path still "crashes" . Serialized behind
* the store ' s commit mutex so an interleaved background flush cannot be
* severed mid - section .
*
* NEVER calls close ( ) — graceful close is exactly what a crash denies .
* /
export async function abandonAsCrashed ( brain : Brainy ) : Promise < void > {
const store = storeOf ( brain ) as unknown as {
withMutex < R > ( fn : ( ) = > Promise < R > ) : Promise < R >
clearPendingFlushTimer ( ) : void
pendingGens : number [ ]
pendingBuffer : Map < number , unknown >
}
await store . withMutex ( async ( ) = > {
store . clearPendingFlushTimer ( )
store . pendingGens = [ ]
store . pendingBuffer . clear ( )
} )
}
/ * *
* Every generation present in the brain 's fact log, ascending — the suite' s
* "what does the log claim is committed" probe . Empty when no fact log exists .
* A scan abort ( gap detection ) propagates — callers that PIN gap behavior
* catch it themselves .
* /
export async function factGenerations ( brain : Brainy ) : Promise < number [ ] > {
const scan = brain . scanFacts ( { fromGeneration : 1 } )
if ( ! scan ) return [ ]
const gens : number [ ] = [ ]
for await ( const batch of scan . batches ( ) ) {
for ( const fact of batch . facts ) gens . push ( fact . generation )
}
return gens . sort ( ( a , b ) = > a - b )
}
/** An ENOSPC-shaped error, matching what a full disk surfaces from node:fs. */
export function enospcError ( ) : NodeJS . ErrnoException {
const err = new Error ( "ENOSPC: no space left on device, write" ) as NodeJS . ErrnoException
err . code = 'ENOSPC'
err . errno = - 28
err . syscall = 'write'
return err
}
/ * *
* Make the storage adapter ' s next raw - byte append ( the fact - log append path )
* fail once with ENOSPC , then restore the original — " the disk filled for one
* append , then space was freed " . Returns a probe telling how many appends
* were failed .
* /
export function failNextAppendWithEnospc ( brain : Brainy ) : { failed : ( ) = > number } {
const storage = ( brain as unknown as {
storage : { appendRawBytes ( p : string , b : Uint8Array ) : Promise < void > }
} ) . storage
const original = storage . appendRawBytes . bind ( storage )
let failures = 0
storage . appendRawBytes = async ( p : string , b : Uint8Array ) : Promise < void > = > {
storage . appendRawBytes = original
failures ++
throw enospcError ( )
}
return { failed : ( ) = > failures }
}
/ * *
* POWER - LOSS MODEL for one entity : remove its canonical noun files from the
* storage root . Legal disk state — a single - op write ' s canonical bytes are
* tmp + rename WITHOUT fsync ( only ` transact() ` runs the write barrier ) , and an
* un - fsynced rename may resolve to "no directory entry" after power loss .
* Throws when nothing was removed ( the caller ' s premise would be wrong ) .
* /
export function dropCanonicalNoun ( dir : string , id : string ) : void {
const removed : string [ ] = [ ]
const walk = ( p : string ) : void = > {
for ( const entry of fs . readdirSync ( p , { withFileTypes : true } ) ) {
const full = path . join ( p , entry . name )
if ( entry . isDirectory ( ) ) {
if ( entry . name === id ) {
fs . rmSync ( full , { recursive : true , force : true } )
removed . push ( full )
} else {
walk ( full )
}
}
}
}
const nounsRoot = path . join ( dir , 'entities' , 'nouns' )
if ( fs . existsSync ( nounsRoot ) ) walk ( nounsRoot )
if ( removed . length === 0 ) {
throw new Error ( ` power-loss model: no canonical files found for noun ${ id } under ${ nounsRoot } ` )
}
}
/** True when the staged record-set directory for `gen` exists on disk. */
export function generationDirExists ( dir : string , gen : number ) : boolean {
return fs . existsSync ( path . join ( dir , '_generations' , String ( gen ) ) )
}