feat: entity-tree family stamp — sourceGeneration + rollup coherence at open
The canonical entity tree now carries a FAMILY STAMP
(_system/family-stamps/entity-tree.json, JSON — forensics stay
terminal-readable): which committed generation the tree reflects
(sourceGeneration) plus the rollup invariants (entity/relationship
counts) that verify a millions-of-files projection whole where per-file
checks cannot scale. Written at flush/close boundaries; open-time
coherence is a COMPARISON, not a walk:
- coherent / absent (legacy store) → silent
- behind → benign for the tree (it is written BY the commit; only the
stamp is stale after a crash between commit and flush) — refreshes at
the next flush
- incoherent (counts diverged at equal generation, or a stamp AHEAD of
the log head) → loud, names the failing invariant; repairIndex()'s
unconditional recount heals and RE-STAMPS so repair leaves a coherent
stamp behind
- a fault reading the stamp is UNVERIFIABLE — never conflated with
absence
One verifier (verifyFamilyStamp, exported) reads both member modes:
enumerated (exact byte size per member, bounded families) and rollup
(invariants, unbounded families). New exports: readFamilyStamp,
verifyFamilyStamp, ENTITY_TREE_STAMP_PATH, FamilyStamp, StampMembers,
StampVerdict.
2026-07-15 10:54:20 -07:00
/ * *
* @module tests / integration / entity - tree - stamp
* @description The entity tree ' s FAMILY STAMP : written at flush / close with
* ` sourceGeneration ` ( the committed generation the canonical tree reflects )
* plus rollup invariants ( entity / relationship counts ) ; verified at open by
* comparison — coherent on a clean close , LOUD on genuine incoherence
* ( tampered counters ) , healed by repairIndex ( ) ' s recount + re - stamp . One
* verifier reads both member modes .
* /
import { describe , it , expect , beforeEach , afterEach , vi } from 'vitest'
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import {
Brainy ,
readFamilyStamp ,
verifyFamilyStamp ,
ENTITY_TREE_STAMP_PATH ,
type FamilyStamp
} from '../../src/index.js'
import { prodLog } from '../../src/utils/logger.js'
describe ( 'entity-tree family stamp' , ( ) = > {
let dir : string
let brain : any
const open = async ( ) = > {
const b : any = new Brainy ( {
requireSubtype : false ,
storage : { type : 'filesystem' , path : dir } ,
silent : true ,
dimensions : 384
} )
await b . init ( )
return b
}
beforeEach ( async ( ) = > {
process . env . BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
dir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'brainy-stamp-' ) )
brain = await open ( )
} )
afterEach ( async ( ) = > {
vi . restoreAllMocks ( )
await brain . close ? . ( ) . catch ( ( ) = > { } )
fs . rmSync ( dir , { recursive : true , force : true } )
} )
it ( 'flush() writes the stamp: rollup mode, counts + sourceGeneration match live state' , async ( ) = > {
for ( let i = 0 ; i < 3 ; i ++ ) await brain . add ( { data : ` s ${ i } ` , type : 'document' , metadata : { i } } )
await brain . flush ( )
const stamp = ( await readFamilyStamp ( brain . storage , ENTITY_TREE_STAMP_PATH ) ) as FamilyStamp
expect ( stamp ) . not . toBeNull ( )
expect ( stamp . family ) . toBe ( 'entity-tree' )
expect ( stamp . members . mode ) . toBe ( 'rollup' )
const invariants = ( stamp . members as any ) . invariants
expect ( invariants . nounCount ) . toBe ( await brain . storage . getNounCount ( ) )
expect ( invariants . verbCount ) . toBe ( await brain . storage . getVerbCount ( ) )
fix(recovery): a torn generation-log tail is a terminal verdict, never a wait
Two halves of one defect, found by a seeded-SIGKILL crash lane.
THE FALSE POSITIVE. stampEntityTree() recorded generationStore.generation()
— the ALLOCATED counter, a number a write in flight has claimed and may
never commit — while the JSDoc beside it already said the source is the
committed generation. Every crash inside a write window therefore produced
a spurious verdict at the next open: either 'sourceGeneration N is ahead of
the log head N-1' (the allocated generation died with the process) or
'rollup invariant nounCount: stamped X, observed Y' (the recovery fold
folded facts the stamp's counts predate). Both told the operator to run
repairIndex() — a whole-store recount — for a store that was coherent.
Measured before this commit: 4 of 11 SIGKILL cycles on a healthy store
raised one of the two. The stamp and the open now both read
committedGeneration(), which is what every other open-time watermark in the
class already reasons about.
THE TERMINAL VERDICT. A stamp still ahead of committed truth after the
recovery fold witnesses a generation that is not in the log — the stamp's
fsync outlived the tail's, and there is nothing to arrive. That is its own
verdict state now ('torn'), never folded in with 'incoherent': the two have
opposite cures. A writer open demotes it — the unusable stamped surface is
re-derived at the committed generation from the live counters, O(1),
straight-line, no loop and no await on external progress, narrated with
both count sets, the stamp's path and its committedAt. A read-only open
cannot re-stamp, so it says so and names the cure instead of guessing, and
still serves. Neither branch waits, and neither locks an owner out of a
canonical tree the stamp only describes.
Pins: the verifier returns the torn verdict with both generations; a
fabricated head-behind-source store narrates precisely, demotes inside a
bounded open, serves its rows, and is quiet at the next open (the demotion
converges); a read-only open narrates the same verdict and leaves the bytes
untouched.
(cherry picked from commit 298cb6dacaac9ef80db65a723d65cbd53c15d23e)
2026-08-31 09:07:18 -07:00
// THE SOURCE IS COMMITTED TRUTH, never the allocated counter. Stamping the
// counter labelled the stamp with a generation a write in flight had merely
// claimed, so every crash inside a write window produced a spurious verdict
// at the next open (see the torn-tail pins below).
expect ( stamp . sourceGeneration ) . toBe ( brain . generationStore . committedGeneration ( ) )
feat: entity-tree family stamp — sourceGeneration + rollup coherence at open
The canonical entity tree now carries a FAMILY STAMP
(_system/family-stamps/entity-tree.json, JSON — forensics stay
terminal-readable): which committed generation the tree reflects
(sourceGeneration) plus the rollup invariants (entity/relationship
counts) that verify a millions-of-files projection whole where per-file
checks cannot scale. Written at flush/close boundaries; open-time
coherence is a COMPARISON, not a walk:
- coherent / absent (legacy store) → silent
- behind → benign for the tree (it is written BY the commit; only the
stamp is stale after a crash between commit and flush) — refreshes at
the next flush
- incoherent (counts diverged at equal generation, or a stamp AHEAD of
the log head) → loud, names the failing invariant; repairIndex()'s
unconditional recount heals and RE-STAMPS so repair leaves a coherent
stamp behind
- a fault reading the stamp is UNVERIFIABLE — never conflated with
absence
One verifier (verifyFamilyStamp, exported) reads both member modes:
enumerated (exact byte size per member, bounded families) and rollup
(invariants, unbounded families). New exports: readFamilyStamp,
verifyFamilyStamp, ENTITY_TREE_STAMP_PATH, FamilyStamp, StampMembers,
StampVerdict.
2026-07-15 10:54:20 -07:00
expect ( stamp . generation ) . toBeGreaterThanOrEqual ( 1 )
} )
it ( 'a cleanly-closed store reopens COHERENT (no incoherence warning)' , async ( ) = > {
await brain . add ( { data : 'clean' , type : 'document' , metadata : { } } )
await brain . close ( )
const warn = vi . spyOn ( prodLog , 'warn' )
brain = await open ( )
const stampWarnings = warn . mock . calls . filter ( ( c ) = > String ( c [ 0 ] ) . includes ( 'entity-tree stamp' ) )
expect ( stampWarnings ) . toEqual ( [ ] )
} )
it ( 'tampered counters surface as INCOHERENT at open; repairIndex() heals + re-stamps' , async ( ) = > {
for ( let i = 0 ; i < 3 ; i ++ ) await brain . add ( { data : ` t ${ i } ` , type : 'document' , metadata : { i } } )
await brain . close ( )
// Simulate counter drift AFTER the stamp was written: inflate the
// persisted scalar the way the historical decrement-skip did.
brain = await open ( )
; ( brain . storage as any ) . totalNounCount += 52
await ( brain . storage as any ) . persistCounts ( )
await brain . close ( )
// The close boundary re-stamps with the inflated counter — so tamper the
// STAMP instead for a deterministic mismatch: stamped counts differ from
// the (inflated) live ones at the NEXT open only if the stamp is older.
// Rewrite the stamp with the honest counts + current sourceGeneration.
const raw = JSON . parse (
require ( 'node:zlib' )
. gunzipSync ( fs . readFileSync ( path . join ( dir , ` ${ ENTITY_TREE_STAMP_PATH } .gz ` ) ) )
. toString ( 'utf-8' )
) as FamilyStamp
const honest = { . . . raw }
; ( honest . members as any ) . invariants . nounCount -= 52
fs . writeFileSync (
path . join ( dir , ` ${ ENTITY_TREE_STAMP_PATH } .gz ` ) ,
require ( 'node:zlib' ) . gzipSync ( JSON . stringify ( honest ) )
)
const warn = vi . spyOn ( prodLog , 'warn' )
brain = await open ( )
const incoherent = warn . mock . calls . filter ( ( c ) = > String ( c [ 0 ] ) . includes ( 'INCOHERENT' ) )
expect ( incoherent . length ) . toBeGreaterThanOrEqual ( 1 )
expect ( String ( incoherent [ 0 ] [ 0 ] ) ) . toMatch ( /nounCount/ )
// The heal: recount from canonical + re-stamp → next open is quiet.
await brain . repairIndex ( )
await brain . close ( )
const warn2 = vi . spyOn ( prodLog , 'warn' )
brain = await open ( )
const stillIncoherent = warn2 . mock . calls . filter ( ( c ) = > String ( c [ 0 ] ) . includes ( 'INCOHERENT' ) )
expect ( stillIncoherent ) . toEqual ( [ ] )
} )
fix(recovery): a torn generation-log tail is a terminal verdict, never a wait
Two halves of one defect, found by a seeded-SIGKILL crash lane.
THE FALSE POSITIVE. stampEntityTree() recorded generationStore.generation()
— the ALLOCATED counter, a number a write in flight has claimed and may
never commit — while the JSDoc beside it already said the source is the
committed generation. Every crash inside a write window therefore produced
a spurious verdict at the next open: either 'sourceGeneration N is ahead of
the log head N-1' (the allocated generation died with the process) or
'rollup invariant nounCount: stamped X, observed Y' (the recovery fold
folded facts the stamp's counts predate). Both told the operator to run
repairIndex() — a whole-store recount — for a store that was coherent.
Measured before this commit: 4 of 11 SIGKILL cycles on a healthy store
raised one of the two. The stamp and the open now both read
committedGeneration(), which is what every other open-time watermark in the
class already reasons about.
THE TERMINAL VERDICT. A stamp still ahead of committed truth after the
recovery fold witnesses a generation that is not in the log — the stamp's
fsync outlived the tail's, and there is nothing to arrive. That is its own
verdict state now ('torn'), never folded in with 'incoherent': the two have
opposite cures. A writer open demotes it — the unusable stamped surface is
re-derived at the committed generation from the live counters, O(1),
straight-line, no loop and no await on external progress, narrated with
both count sets, the stamp's path and its committedAt. A read-only open
cannot re-stamp, so it says so and names the cure instead of guessing, and
still serves. Neither branch waits, and neither locks an owner out of a
canonical tree the stamp only describes.
Pins: the verifier returns the torn verdict with both generations; a
fabricated head-behind-source store narrates precisely, demotes inside a
bounded open, serves its rows, and is quiet at the next open (the demotion
converges); a read-only open narrates the same verdict and leaves the bytes
untouched.
(cherry picked from commit 298cb6dacaac9ef80db65a723d65cbd53c15d23e)
2026-08-31 09:07:18 -07:00
/ * *
* Rewrite the on - disk stamp so its ` sourceGeneration ` sits ABOVE the store ' s
* committed watermark — the durable shape a torn generation - log tail leaves
* behind ( the stamp 's fsync outlived the tail' s ) . Fabricated rather than
* crash - produced so the pin is deterministic ; the seeded - SIGKILL lane
* ( ` scripts/crash-consistency.mjs ` in the engine repo ) produces the same
* shape from a real abrupt termination .
* /
const fabricateTear = ( ahead : number ) : FamilyStamp = > {
const file = path . join ( dir , ` ${ ENTITY_TREE_STAMP_PATH } .gz ` )
const zlib = require ( 'node:zlib' )
const raw = JSON . parse ( zlib . gunzipSync ( fs . readFileSync ( file ) ) . toString ( 'utf-8' ) ) as FamilyStamp
const torn : FamilyStamp = { . . . raw , sourceGeneration : raw.sourceGeneration + ahead }
fs . writeFileSync ( file , zlib . gzipSync ( JSON . stringify ( torn ) ) )
return torn
}
it ( 'a torn generation-log tail is a TERMINAL VERDICT at open: narrated, demoted, never a wait' , async ( ) = > {
for ( let i = 0 ; i < 3 ; i ++ )
await brain . add ( { data : ` torn ${ i } ` , type : 'document' , metadata : { i } } )
await brain . close ( )
const torn = fabricateTear ( 5 )
const warn = vi . spyOn ( prodLog , 'warn' )
const startedAt = Date . now ( )
brain = await open ( )
const openMs = Date . now ( ) - startedAt
const tearLines = warn . mock . calls . filter ( ( c ) = > String ( c [ 0 ] ) . includes ( 'TORN GENERATION-LOG TAIL' ) )
expect ( tearLines . length ) . toBe ( 1 )
const said = String ( tearLines [ 0 ] [ 0 ] )
// Narrated PRECISELY: both generations, the file, and the named cure.
expect ( said ) . toContain ( ` source generation ${ torn . sourceGeneration } ` )
expect ( said ) . toContain ( ` committed generation ${ brain . generationStore . committedGeneration ( ) } ` )
expect ( said ) . toContain ( ENTITY_TREE_STAMP_PATH )
expect ( said ) . toContain ( 'DEMOTED' )
expect ( said ) . toMatch ( /repairIndex\(\)/ )
// Terminal, not a wait: the demotion is O(1) straight-line work, so a tear
// cannot turn an open into the 8-minute spin this class was reported as.
expect ( openMs ) . toBeLessThan ( 30 _000 )
// The store SERVES — a tear in a stamp never locks an owner out of the
// canonical tree the stamp merely describes.
expect ( ( await brain . find ( { type : 'document' , limit : 100 } ) ) . length ) . toBe ( 3 )
// The demotion CONVERGED: the stamp now names committed truth, and the
// next open is quiet. A verdict that re-narrates every open is a wait
// wearing a different hat.
const restamped = ( await readFamilyStamp ( brain . storage , ENTITY_TREE_STAMP_PATH ) ) as FamilyStamp
expect ( restamped . sourceGeneration ) . toBe ( brain . generationStore . committedGeneration ( ) )
await brain . close ( )
const warn2 = vi . spyOn ( prodLog , 'warn' )
brain = await open ( )
expect ( warn2 . mock . calls . filter ( ( c ) = > String ( c [ 0 ] ) . includes ( 'TORN' ) ) ) . toEqual ( [ ] )
} )
it ( 'a READ-ONLY open on a torn tail refuses to guess: terminal verdict + named cure, no re-stamp' , async ( ) = > {
await brain . add ( { data : 'ro' , type : 'document' , metadata : { } } )
await brain . close ( )
const torn = fabricateTear ( 3 )
const warn = vi . spyOn ( prodLog , 'warn' )
const reader : any = await Brainy . openReadOnly ( {
requireSubtype : false ,
storage : { type : 'filesystem' , path : dir } ,
silent : true ,
dimensions : 384
} )
const tearLines = warn . mock . calls . filter ( ( c ) = > String ( c [ 0 ] ) . includes ( 'TORN GENERATION-LOG TAIL' ) )
expect ( tearLines . length ) . toBe ( 1 )
const said = String ( tearLines [ 0 ] [ 0 ] )
expect ( said ) . toContain ( 'READ-ONLY' )
expect ( said ) . toContain ( 'UNVERIFIED' )
expect ( said ) . toMatch ( /repairIndex\(\)/ )
await reader . close ( )
// A reader never rewrites the store: read the bytes back off disk (not
// through a writer open, which would demote them) — the torn stamp is
// exactly as it was found.
const onDisk = JSON . parse (
require ( 'node:zlib' )
. gunzipSync ( fs . readFileSync ( path . join ( dir , ` ${ ENTITY_TREE_STAMP_PATH } .gz ` ) ) )
. toString ( 'utf-8' )
) as FamilyStamp
expect ( onDisk . sourceGeneration ) . toBe ( torn . sourceGeneration )
expect ( onDisk . generation ) . toBe ( torn . generation )
brain = await open ( )
} )
feat: entity-tree family stamp — sourceGeneration + rollup coherence at open
The canonical entity tree now carries a FAMILY STAMP
(_system/family-stamps/entity-tree.json, JSON — forensics stay
terminal-readable): which committed generation the tree reflects
(sourceGeneration) plus the rollup invariants (entity/relationship
counts) that verify a millions-of-files projection whole where per-file
checks cannot scale. Written at flush/close boundaries; open-time
coherence is a COMPARISON, not a walk:
- coherent / absent (legacy store) → silent
- behind → benign for the tree (it is written BY the commit; only the
stamp is stale after a crash between commit and flush) — refreshes at
the next flush
- incoherent (counts diverged at equal generation, or a stamp AHEAD of
the log head) → loud, names the failing invariant; repairIndex()'s
unconditional recount heals and RE-STAMPS so repair leaves a coherent
stamp behind
- a fault reading the stamp is UNVERIFIABLE — never conflated with
absence
One verifier (verifyFamilyStamp, exported) reads both member modes:
enumerated (exact byte size per member, bounded families) and rollup
(invariants, unbounded families). New exports: readFamilyStamp,
verifyFamilyStamp, ENTITY_TREE_STAMP_PATH, FamilyStamp, StampMembers,
StampVerdict.
2026-07-15 10:54:20 -07:00
it ( 'the one verifier handles both member modes' , ( ) = > {
const rollup : FamilyStamp = {
family : 'x' ,
generation : 1 ,
committedAt : new Date ( ) . toISOString ( ) ,
sourceGeneration : 5 ,
members : { mode : 'rollup' , invariants : { nounCount : 10 } }
}
expect ( verifyFamilyStamp ( rollup , 5 , { nounCount : 10 } ) ) . toEqual ( { state : 'coherent' } )
expect ( verifyFamilyStamp ( rollup , 5 , { nounCount : 11 } ) . state ) . toBe ( 'incoherent' )
expect ( verifyFamilyStamp ( rollup , 9 , { nounCount : 10 } ) ) . toEqual ( {
state : 'behind' ,
stampSource : 5 ,
head : 9
} )
fix(recovery): a torn generation-log tail is a terminal verdict, never a wait
Two halves of one defect, found by a seeded-SIGKILL crash lane.
THE FALSE POSITIVE. stampEntityTree() recorded generationStore.generation()
— the ALLOCATED counter, a number a write in flight has claimed and may
never commit — while the JSDoc beside it already said the source is the
committed generation. Every crash inside a write window therefore produced
a spurious verdict at the next open: either 'sourceGeneration N is ahead of
the log head N-1' (the allocated generation died with the process) or
'rollup invariant nounCount: stamped X, observed Y' (the recovery fold
folded facts the stamp's counts predate). Both told the operator to run
repairIndex() — a whole-store recount — for a store that was coherent.
Measured before this commit: 4 of 11 SIGKILL cycles on a healthy store
raised one of the two. The stamp and the open now both read
committedGeneration(), which is what every other open-time watermark in the
class already reasons about.
THE TERMINAL VERDICT. A stamp still ahead of committed truth after the
recovery fold witnesses a generation that is not in the log — the stamp's
fsync outlived the tail's, and there is nothing to arrive. That is its own
verdict state now ('torn'), never folded in with 'incoherent': the two have
opposite cures. A writer open demotes it — the unusable stamped surface is
re-derived at the committed generation from the live counters, O(1),
straight-line, no loop and no await on external progress, narrated with
both count sets, the stamp's path and its committedAt. A read-only open
cannot re-stamp, so it says so and names the cure instead of guessing, and
still serves. Neither branch waits, and neither locks an owner out of a
canonical tree the stamp only describes.
Pins: the verifier returns the torn verdict with both generations; a
fabricated head-behind-source store narrates precisely, demotes inside a
bounded open, serves its rows, and is quiet at the next open (the demotion
converges); a read-only open narrates the same verdict and leaves the bytes
untouched.
(cherry picked from commit 298cb6dacaac9ef80db65a723d65cbd53c15d23e)
2026-08-31 09:07:18 -07:00
// AHEAD is its own class — a torn generation-log tail, never folded in
// with `incoherent`: the two have opposite cures (recount vs demote).
expect ( verifyFamilyStamp ( rollup , 3 , { nounCount : 10 } ) ) . toEqual ( {
state : 'torn' ,
stampSource : 5 ,
head : 3
} )
feat: entity-tree family stamp — sourceGeneration + rollup coherence at open
The canonical entity tree now carries a FAMILY STAMP
(_system/family-stamps/entity-tree.json, JSON — forensics stay
terminal-readable): which committed generation the tree reflects
(sourceGeneration) plus the rollup invariants (entity/relationship
counts) that verify a millions-of-files projection whole where per-file
checks cannot scale. Written at flush/close boundaries; open-time
coherence is a COMPARISON, not a walk:
- coherent / absent (legacy store) → silent
- behind → benign for the tree (it is written BY the commit; only the
stamp is stale after a crash between commit and flush) — refreshes at
the next flush
- incoherent (counts diverged at equal generation, or a stamp AHEAD of
the log head) → loud, names the failing invariant; repairIndex()'s
unconditional recount heals and RE-STAMPS so repair leaves a coherent
stamp behind
- a fault reading the stamp is UNVERIFIABLE — never conflated with
absence
One verifier (verifyFamilyStamp, exported) reads both member modes:
enumerated (exact byte size per member, bounded families) and rollup
(invariants, unbounded families). New exports: readFamilyStamp,
verifyFamilyStamp, ENTITY_TREE_STAMP_PATH, FamilyStamp, StampMembers,
StampVerdict.
2026-07-15 10:54:20 -07:00
expect ( verifyFamilyStamp ( null , 5 , { } ) ) . toEqual ( { state : 'absent' } )
const enumerated : FamilyStamp = {
family : 'y' ,
generation : 1 ,
committedAt : new Date ( ) . toISOString ( ) ,
sourceGeneration : 2 ,
members : { mode : 'enumerated' , files : [ { path : 'a.bin' , bytes : 128 } ] }
}
expect ( verifyFamilyStamp ( enumerated , 2 , { 'a.bin' : 128 } ) ) . toEqual ( { state : 'coherent' } )
expect ( verifyFamilyStamp ( enumerated , 2 , { 'a.bin' : 64 } ) . state ) . toBe ( 'incoherent' )
expect ( verifyFamilyStamp ( enumerated , 2 , { } ) . state ) . toBe ( 'incoherent' ) // missing member
2026-07-15 12:36:27 -07:00
// Rollup invariants may be STRING fingerprints (e.g. a per-tree SHA-256):
// strict equality either way; a type mismatch reads as incoherence.
const fingerprinted : FamilyStamp = {
family : 'z' ,
generation : 1 ,
committedAt : new Date ( ) . toISOString ( ) ,
sourceGeneration : 3 ,
members : { mode : 'rollup' , invariants : { treeDigest : 'abc123' , rows : 42 } }
}
expect ( verifyFamilyStamp ( fingerprinted , 3 , { treeDigest : 'abc123' , rows : 42 } ) ) . toEqual ( {
state : 'coherent'
} )
expect ( verifyFamilyStamp ( fingerprinted , 3 , { treeDigest : 'deadbeef' , rows : 42 } ) . state ) . toBe (
'incoherent'
)
expect ( verifyFamilyStamp ( fingerprinted , 3 , { treeDigest : 'abc123' , rows : '42' } ) . state ) . toBe (
'incoherent' // type mismatch never passes
)
feat: entity-tree family stamp — sourceGeneration + rollup coherence at open
The canonical entity tree now carries a FAMILY STAMP
(_system/family-stamps/entity-tree.json, JSON — forensics stay
terminal-readable): which committed generation the tree reflects
(sourceGeneration) plus the rollup invariants (entity/relationship
counts) that verify a millions-of-files projection whole where per-file
checks cannot scale. Written at flush/close boundaries; open-time
coherence is a COMPARISON, not a walk:
- coherent / absent (legacy store) → silent
- behind → benign for the tree (it is written BY the commit; only the
stamp is stale after a crash between commit and flush) — refreshes at
the next flush
- incoherent (counts diverged at equal generation, or a stamp AHEAD of
the log head) → loud, names the failing invariant; repairIndex()'s
unconditional recount heals and RE-STAMPS so repair leaves a coherent
stamp behind
- a fault reading the stamp is UNVERIFIABLE — never conflated with
absence
One verifier (verifyFamilyStamp, exported) reads both member modes:
enumerated (exact byte size per member, bounded families) and rollup
(invariants, unbounded families). New exports: readFamilyStamp,
verifyFamilyStamp, ENTITY_TREE_STAMP_PATH, FamilyStamp, StampMembers,
StampVerdict.
2026-07-15 10:54:20 -07:00
} )
} )