fix(adoption): the reserved-root mint exemption — int 0 is legitimate for exactly one id
The release-holding finding from the joint gate's six real depot brains: the adoption path's positive-int mint check false-flagged the reserved VFS-root sentinel (the all-zeros UUID, minted int 0 BY CONSTRUCTION at genesis on existing brains) as a corrupt mint — so every existing brain refused log-authority adoption and stayed on the old lossy-under-power-cut durability, defeating the release's headline crash-safety exactly where it matters most. The exemption, at both mint seams (the host's minter thunk and the fact log's encoder guard): int 0 is legal iff the id is the reserved root; zero for ANY other id remains a corrupt-mint refusal naming the reserved exception. The codec's u64 layer already tolerated 0 — only the guards over-refused. Pins: adoption goes green on a brain whose VFS root carries int 0 (the depot-brain shape, previously refused) · a non-root zero still refuses typed at the mint seam — held at the seam itself because a full write SELF-HEALS a poisoned zero (the index cycle re-mints before the fact is written, which is the correct outcome and was verified in the pinning). Gates: unit 2065/2065 · integration 830 · conformance 31/31.
This commit is contained in:
parent
0e3facf4a8
commit
2abe8b3806
3 changed files with 118 additions and 4 deletions
|
|
@ -1306,10 +1306,18 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
}
|
||||
const minted = mapper.getOrAssign(id, undefined)
|
||||
const asBigint = typeof minted === 'bigint' ? minted : BigInt(minted)
|
||||
if (asBigint <= 0n) {
|
||||
// THE RESERVED-ROOT EXEMPTION: the VFS root (the all-zeros UUID) is
|
||||
// minted int 0 BY CONSTRUCTION at genesis on existing brains — the
|
||||
// one legitimate zero in the id space. Zero for ANY other id is a
|
||||
// corrupt mint and refuses. (Without this, every existing brain's
|
||||
// adoption oracle false-flagged its own root and refused the flip.)
|
||||
const isReservedRoot =
|
||||
asBigint === 0n && id === '00000000-0000-0000-0000-000000000000'
|
||||
if (asBigint < 0n || (asBigint === 0n && !isReservedRoot)) {
|
||||
throw new Error(
|
||||
`fact log v2: the id mapper minted ${asBigint} for ${kind} ${id} — ` +
|
||||
`minted ints are positive; refusing to write`
|
||||
`minted ints are positive (int 0 is reserved for the VFS root alone); ` +
|
||||
`refusing to write`
|
||||
)
|
||||
}
|
||||
return asBigint
|
||||
|
|
|
|||
|
|
@ -1325,10 +1325,16 @@ export class FactLog {
|
|||
)
|
||||
}
|
||||
const minted = this.intMinter(kind, id)
|
||||
if (typeof minted !== 'bigint' || minted <= 0n) {
|
||||
// Reserved-root exemption: int 0 is legitimate for exactly one id —
|
||||
// the all-zeros VFS root, minted 0 by construction at genesis on
|
||||
// existing brains. Zero anywhere else is a corrupt mint.
|
||||
const isReservedRoot =
|
||||
minted === 0n && id === '00000000-0000-0000-0000-000000000000'
|
||||
if (typeof minted !== 'bigint' || minted < 0n || (minted === 0n && !isReservedRoot)) {
|
||||
throw new Error(
|
||||
`fact log v2: the int minter returned ${String(minted)} for ${kind} ${id} — ` +
|
||||
`minted ints are positive bigints; refusing to write`
|
||||
`minted ints are positive bigints (int 0 reserved for the VFS root alone); ` +
|
||||
`refusing to write`
|
||||
)
|
||||
}
|
||||
return minted
|
||||
|
|
|
|||
100
tests/integration/reserved-root-mint.test.ts
Normal file
100
tests/integration/reserved-root-mint.test.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
/**
|
||||
* @module tests/integration/reserved-root-mint
|
||||
* @description THE RESERVED-ROOT MINT EXEMPTION (the release's final fix):
|
||||
* existing brains mint the VFS root (the all-zeros UUID) as int 0 by
|
||||
* construction at genesis — the one legitimate zero in the id space. The
|
||||
* adoption path must accept it (every real depot brain refused adoption
|
||||
* over this); a zero mint for ANY OTHER id remains a corrupt-mint refusal.
|
||||
*/
|
||||
import { describe, it, expect, afterEach } from 'vitest'
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { Brainy } from '../../src/index.js'
|
||||
import { NounType } from '../../src/types/graphTypes.js'
|
||||
|
||||
const ROOT = '00000000-0000-0000-0000-000000000000'
|
||||
const dirs: string[] = []
|
||||
const brains: Brainy[] = []
|
||||
afterEach(async () => {
|
||||
for (const b of brains.splice(0)) await b.close().catch(() => {})
|
||||
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
type MapperBox = {
|
||||
metadataIndex: {
|
||||
getIdMapper(): {
|
||||
uuidToInt: Map<string, number>
|
||||
intToUuid: Map<number, string>
|
||||
dirty?: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('reserved-root mint exemption', () => {
|
||||
it('adoption succeeds on a brain whose VFS root carries int 0 (the depot-brain shape)', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'brainy-root0-'))
|
||||
dirs.push(dir)
|
||||
// Build the brain in 'defer' so we control the adoption moment.
|
||||
const brain = new Brainy({
|
||||
storage: { type: 'filesystem', path: dir },
|
||||
requireSubtype: false,
|
||||
logAuthority: 'defer'
|
||||
})
|
||||
await brain.init()
|
||||
brains.push(brain)
|
||||
await brain.add({ data: 'depot row', type: NounType.Document, metadata: { k: 1 } })
|
||||
|
||||
// The genesis-era shape: the root's mint is 0 (white-box — real depot
|
||||
// brains carry this in their persisted mapper).
|
||||
const mapper = (brain as unknown as MapperBox).metadataIndex.getIdMapper()
|
||||
const currentInt = mapper.uuidToInt.get(ROOT)
|
||||
if (currentInt !== undefined) mapper.intToUuid.delete(currentInt)
|
||||
mapper.uuidToInt.set(ROOT, 0)
|
||||
mapper.intToUuid.set(0, ROOT)
|
||||
|
||||
// THE PIN: adoption goes green — the backfill re-commits the root with
|
||||
// its legitimate int 0 instead of refusing the whole brain.
|
||||
const report = await brain.adoptLogAuthority()
|
||||
expect(report.verdict).toBe('green')
|
||||
expect(brain.logAuthority().authority).toBe('log')
|
||||
// And the brain keeps serving + writing after the flip.
|
||||
const fresh = await brain.add({ data: 'post-adopt', type: NounType.Document, metadata: { k: 2 } })
|
||||
expect((await brain.get(fresh))!.data).toContain('post-adopt')
|
||||
}, 120000)
|
||||
|
||||
it('a zero mint for a NON-root id still refuses at the mint seam, loudly and typed', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'brainy-nonroot0-'))
|
||||
dirs.push(dir)
|
||||
const brain = new Brainy({
|
||||
storage: { type: 'filesystem', path: dir },
|
||||
requireSubtype: false,
|
||||
logAuthority: 'defer'
|
||||
})
|
||||
await brain.init()
|
||||
brains.push(brain)
|
||||
const victim = await brain.add({ data: 'poisoned mint target', type: NounType.Document, metadata: {} })
|
||||
|
||||
// Corrupt shape: some OTHER id maps to 0. (A full update() SELF-HEALS
|
||||
// this — the index cycle re-mints before the fact is written, which is
|
||||
// the correct outcome — so the pin holds the guard at its real seam:
|
||||
// the fact log's minter, which is what stands between a surviving zero
|
||||
// and the wire.)
|
||||
const mapper = (brain as unknown as MapperBox).metadataIndex.getIdMapper()
|
||||
const currentInt = mapper.uuidToInt.get(victim)
|
||||
if (currentInt !== undefined) mapper.intToUuid.delete(currentInt)
|
||||
mapper.uuidToInt.set(victim, 0)
|
||||
mapper.intToUuid.set(0, victim)
|
||||
|
||||
const factLog = (brain as unknown as {
|
||||
generationStore: { getFactLog(): { intMinter(kind: string, id: string): bigint } }
|
||||
}).generationStore.getFactLog()
|
||||
expect(() => factLog.intMinter('noun', victim)).toThrow(
|
||||
/reserved for the VFS root|minted ints are positive/
|
||||
)
|
||||
// And the reserved root itself passes the same seam with 0.
|
||||
mapper.uuidToInt.set(ROOT, 0)
|
||||
mapper.intToUuid.set(0, ROOT)
|
||||
expect(factLog.intMinter('noun', ROOT)).toBe(0n)
|
||||
}, 120000)
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue