fix(delete): the null-metadata skip closes — index legs run id-keyed or narrate, never silently strand postings
Some checks failed
CI / Node 24 (push) Successful in 12m19s
CI / Node 22 (push) Successful in 12m25s
CI / Integration + conformance (Node 22) (push) Failing after 13m19s
CI / Bun (latest) (push) Successful in 12m20s

remove() guarded its index legs with 'if (metadata)': a row whose canonical
metadata was unreadable at delete time kept its postings forever, silently
(the leak class a partner audit confirmed at this exact site). Closed at
all three provider shapes: a provider exposing the id-keyed removal
contract (removeEntityById, arriving with the accelerator's next minor)
gets exact per-entity retraction; the JS index gets id-keyed cleanup
(deleted bitmap + id mapper — field stats reconcile at rebuild), narrated;
a native provider without the contract is NEVER called metadata-omitted
(that path walks the store's value space) — its skip is narrated and
tracked in the degraded set for repairIndex, never silent. The pre-reads
are torn-tolerant: a torn record is deletable (the delete is the cure).

Pinned: a metadata-less row with live postings deletes cleanly and leaves
the query universe; the delete-family suites stay green alongside.
This commit is contained in:
David Snelling 2026-08-20 11:53:58 -07:00
parent 8d45f964e9
commit 607e9f5492
2 changed files with 129 additions and 4 deletions

View file

@ -3692,9 +3692,22 @@ export class Brainy<T = any> implements BrainyInterface<T> {
// stored, so remove() deletes the same entity. A real UUID passes through.
id = resolveEntityId(id)
// Get entity metadata and related verbs before deletion
const metadata = await this.storage.getNounMetadata(id)
const noun = await this.storage.getNoun(id)
// Get entity metadata and related verbs before deletion. TORN-TOLERANT:
// a torn record must still be deletable (the delete IS the cure) — a
// torn pre-read reads as null and the null-path below handles it loudly.
let metadata: any = null
let noun: any = null
try {
metadata = await this.storage.getNounMetadata(id)
} catch (err) {
if ((err as { code?: string }).code !== 'TORN_RECORD') throw err
prodLog.warn(`[Brainy] remove(${id}): metadata pre-read is TORN — deleting anyway; index legs run id-keyed`)
}
try {
noun = await this.storage.getNoun(id)
} catch (err) {
if ((err as { code?: string }).code !== 'TORN_RECORD') throw err
}
const verbs = await this.storage.getVerbsBySource(id)
const targetVerbs = await this.storage.getVerbsByTarget(id)
const allVerbs = [...verbs, ...targetVerbs]
@ -3712,11 +3725,61 @@ export class Brainy<T = any> implements BrainyInterface<T> {
)
}
// Operation 2: Remove from metadata index
// Operation 2: Remove from metadata index. THE NULL-METADATA SKIP IS
// CLOSED (a posting-leak class, confirmed at this site): when the
// pre-read missed, the leg no longer silently skips —
// - a provider exposing removeEntityById (the id-keyed contract)
// gets it: exact per-entity retraction via its reverse record;
// - the JS index gets removeFromIndex(id) — safe id-keyed cleanup
// (deleted bitmap + id mapper; field stats reconcile at rebuild);
// - a NATIVE provider WITHOUT the contract is never called
// metadata-omitted (that path walks its value space) — the skip
// happens, but NARRATED and tracked in the degraded set so
// repairIndex reconciles it. Silence is the only thing outlawed.
if (metadata) {
tx.addOperation(
new RemoveFromMetadataIndexOperation(this.metadataIndex, id, metadata, this.indexWriteGeneration)
)
} else {
const prov = this.metadataIndex as unknown as {
removeEntityById?: (id: string) => Promise<void>
removeFromIndex?: (id: string, metadata?: unknown, generation?: bigint) => Promise<void>
}
if (typeof prov.removeEntityById === 'function') {
const g = this.indexWriteGeneration
tx.addOperation({
name: 'RemoveEntityByIdTombstone',
execute: async () => {
await prov.removeEntityById!(id)
return async () => {
// Undo of an id-keyed tombstone on an absent row: nothing
// to restore (the row had no readable metadata to re-post).
void g
}
}
})
} else if (this.metadataIndex instanceof MetadataIndexManager) {
const gv = this.indexWriteGeneration
tx.addOperation({
name: 'IdKeyedIndexCleanup',
execute: async () => {
await prov.removeFromIndex!(id, undefined, typeof gv === 'function' ? gv() : gv)
return async () => {}
}
})
prodLog.warn(
`[Brainy] remove(${id}): no metadata at delete — id-keyed index cleanup ran ` +
`(deleted bitmap + id mapper); field statistics reconcile at the next rebuild/repairIndex.`
)
} else {
this._indexDegradedIds.add(id)
prodLog.warn(
`[Brainy] remove(${id}): no metadata at delete and this provider has no id-keyed ` +
`removal — its postings for this id are NOT tombstoned yet (tracked as degraded; ` +
`repairIndex() reconciles). Never calling a metadata-omitted native removal: that ` +
`path walks the store's value space.`
)
}
}
// Operation 3: Delete noun (full removal). The pre-read metadata rides