feat: registered-blob family contract — declared index blobs are undeletable (ADR-004 Pass 2)

The class-killer for the lost-main.dkann incident (ADR-004 §7). A provider can
declare a derived-index blob FAMILY (a set of members that are load-bearing
together, e.g. vector-base = main.dkann + main.slotmap + main.slotrev). Once
declared:

- deleteBinaryBlob / removeRawPrefix REFUSE to remove a declared member, throwing
  the new ProtectedArtifactError — an in-process GC / sweeper is now INCAPABLE of
  deleting a load-bearing index file (COLD != DEAD). Intentional retirement is an
  explicit unregisterDerivedFamily(name) first.
- The declaration persists to _system/derived-artifacts.json, so protection
  survives a reopen; clear() resets it with the rest of the derived footprint.
- checkDerivedFamiliesPresent() names any member missing on open (the catch for an
  EXTERNAL deleter that bypasses the in-process refusal) → rebuild from canonical.
- Transients (*.tmp.*, *.rebuild-tmp, *.rotate-tmp) are never protected; a
  namespace family protects a growing prefix (seg-*).

New StorageAdapter surface (optional): registerDerivedFamily / unregisterDerivedFamily
/ listDerivedFamilies + DerivedFamilyDeclaration; new exported errors
ProtectedArtifactError / DerivedArtifactMissingError. Enforcement is inert until a
provider declares a family (no regression). Cor declares its 6 families + does the
atomic set-swap in 3.0.15 (M2); brainy builds the contract now. 8 tests.
This commit is contained in:
David Snelling 2026-07-13 14:52:06 -07:00
parent 6bcb54f0d9
commit bfa1762107
7 changed files with 429 additions and 5 deletions

View file

@ -0,0 +1,130 @@
/**
* @module tests/unit/storage/registered-blob-contract
* @description Pass 2 (ADR-004 §7): declared derived-index blob FAMILIES are
* undeletable through the storage layer an in-process GC/sweeper cannot remove
* a load-bearing index file (the lost-main.dkann class). Covers declare
* protected-delete-throws; unregister delete-ok; transient passthrough;
* namespace prefix protects children; removeRawPrefix refuses a protected
* intersection; persistence across reopen; missing-on-open detection.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { MemoryStorage } from '../../../src/storage/adapters/memoryStorage.js'
import { FileSystemStorage } from '../../../src/storage/adapters/fileSystemStorage.js'
import { ProtectedArtifactError } from '../../../src/index.js'
describe('registered-blob family contract (Pass 2, ADR-004 §7)', () => {
let storage: any
beforeEach(async () => {
storage = new MemoryStorage()
await storage.init()
})
const seedVectorFamily = async () => {
await storage.saveBinaryBlob('_system/vector-index/main.dkann', Buffer.from([1]))
await storage.saveBinaryBlob('_system/vector-index/main.slotmap', Buffer.from([2]))
await storage.saveBinaryBlob('_system/vector-index/main.slotrev', Buffer.from([3]))
await storage.registerDerivedFamily({
name: 'vector-base',
members: [
'_system/vector-index/main.dkann',
'_system/vector-index/main.slotmap',
'_system/vector-index/main.slotrev'
]
})
}
it('a declared member is undeletable (throws ProtectedArtifactError)', async () => {
await seedVectorFamily()
await expect(storage.deleteBinaryBlob('_system/vector-index/main.dkann')).rejects.toBeInstanceOf(
ProtectedArtifactError
)
// The blob is still there.
expect(await storage.loadBinaryBlob('_system/vector-index/main.dkann')).not.toBeNull()
})
it('unregistering the family makes its members deletable again', async () => {
await seedVectorFamily()
await storage.unregisterDerivedFamily('vector-base')
await expect(storage.deleteBinaryBlob('_system/vector-index/main.dkann')).resolves.toBeUndefined()
expect(await storage.loadBinaryBlob('_system/vector-index/main.dkann')).toBeNull()
})
it('an undeclared blob is still deletable (contract does not lock everything)', async () => {
await seedVectorFamily()
await storage.saveBinaryBlob('graph-lsm/source/sstable-1', Buffer.from([9]))
await expect(storage.deleteBinaryBlob('graph-lsm/source/sstable-1')).resolves.toBeUndefined()
})
it('a transient (*.tmp.*) is deletable even if it sits under a protected namespace', async () => {
await storage.registerDerivedFamily({
name: 'vector-ns',
members: ['_system/vector-index/'],
namespace: true
})
await storage.saveBinaryBlob('_system/vector-index/main.dkann.tmp.123', Buffer.from([1]))
await expect(
storage.deleteBinaryBlob('_system/vector-index/main.dkann.tmp.123')
).resolves.toBeUndefined()
})
it('a namespace family protects a growing child key', async () => {
await storage.registerDerivedFamily({
name: 'vector-segments',
members: ['_system/vector-index/'],
namespace: true
})
await storage.saveBinaryBlob('_system/vector-index/seg-42', Buffer.from([7]))
await expect(storage.deleteBinaryBlob('_system/vector-index/seg-42')).rejects.toBeInstanceOf(
ProtectedArtifactError
)
})
it('checkDerivedFamiliesPresent() names a member deleted outside the write path', async () => {
await seedVectorFamily()
// Simulate an EXTERNAL deleter (bypasses the in-process refusal): drop a
// member straight from the underlying store.
;(storage as any).blobStore.delete('_system/vector-index/main.slotmap')
const incomplete = await storage.checkDerivedFamiliesPresent()
expect(incomplete).toHaveLength(1)
expect(incomplete[0].name).toBe('vector-base')
expect(incomplete[0].missing).toContain('_system/vector-index/main.slotmap')
expect(incomplete[0].rebuildable).toBe(true)
})
it('no families registered → deleteBinaryBlob behaves exactly as before (no enforcement)', async () => {
await storage.saveBinaryBlob('some/blob', Buffer.from([1]))
await expect(storage.deleteBinaryBlob('some/blob')).resolves.toBeUndefined()
})
})
describe('registered-blob family protection survives a reopen (FileSystemStorage)', () => {
let dir: string
beforeEach(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-regblob-'))
})
afterEach(() => fs.rmSync(dir, { recursive: true, force: true }))
it('a family declared in one session protects members after reopen', async () => {
const s1: any = new FileSystemStorage(dir)
await s1.init()
await s1.saveBinaryBlob('_system/vector-index/main.dkann', Buffer.from([1]))
await s1.registerDerivedFamily({ name: 'vector-base', members: ['_system/vector-index/main.dkann'] })
// Reopen — the registry is loaded from _system/derived-artifacts.json.
const s2: any = new FileSystemStorage(dir)
await s2.init()
expect(await s2.listDerivedFamilies()).toHaveLength(1)
await expect(s2.deleteBinaryBlob('_system/vector-index/main.dkann')).rejects.toBeInstanceOf(
ProtectedArtifactError
)
// removeRawPrefix nuking the vector dir is also refused.
await expect(s2.removeRawPrefix('_blobs/_system/vector-index')).rejects.toBeInstanceOf(
ProtectedArtifactError
)
})
})