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

@ -630,6 +630,9 @@ export class FileSystemStorage extends BaseStorage {
*/
public override async removeRawPrefix(prefix: string): Promise<void> {
await this.ensureInitialized()
// Registered-blob contract: a prefix-nuke must not take out a protected
// family member (ADR-004 §7). Throws if the prefix intersects one.
await this.assertPrefixNotProtected(prefix)
await fs.promises.rm(path.join(this.rootDir, prefix), { recursive: true, force: true })
}
@ -1250,6 +1253,11 @@ export class FileSystemStorage extends BaseStorage {
*/
public async deleteBinaryBlob(key: string): Promise<void> {
await this.ensureInitialized()
// Registered-blob contract (ADR-004 §7): refuse to delete a declared
// derived-index family member — an in-process GC/sweeper cannot remove a
// load-bearing index file. Throws ProtectedArtifactError; no-op when no
// families are registered.
await this.assertBlobKeyDeletable(key)
try {
await fs.promises.unlink(this.blobPath(key))
} catch {