fix: honest counters — removal never re-reads the removed record + repairIndex recounts and persists all rollups
Persisted entity totals inflated permanently: a delete's count decrement was sourced from re-reading the record being removed, so a null read (replace race, or a ghost left by a pre-8.3.1 partial delete) silently skipped the decrement while the paired add had counted — and Math.max(persistedTotal, scanned) meant no disk cleanup could ever lower the reported total. Reported by a production deployment with a write→delete→re-create repro minting drift within hours. - The caller's pre-delete read now rides through the entire delete path (remove()/removeMany()/plan → DeleteNoun/DeleteVerb operations → deleteNoun/deleteVerb → deleteNounMetadata/deleteVerbMetadata): a null internal read falls back to the known prior record instead of skipping the decrement. StorageAdapter.deleteNoun/deleteVerb gain an optional priorMetadata parameter (additive). - rebuildTypeCounts() rebuilt only the type-statistics arrays and computed the total just to LOG it — the persisted scalar (counts.json) survived every rebuild. One canonical walk now rebuilds every counter rollup (scalar totals + per-type maps + type statistics) and persists them; repairIndex() runs the recount unconditionally, since counters can be inflated over clean shelves.
This commit is contained in:
parent
d9fa3be648
commit
2e2ba9c9aa
5 changed files with 237 additions and 28 deletions
|
|
@ -3093,9 +3093,11 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
)
|
||||
}
|
||||
|
||||
// Operation 3: Delete noun metadata
|
||||
// Operation 3: Delete noun (full removal). The pre-read metadata rides
|
||||
// along so the count decrement never depends on re-reading the record
|
||||
// being removed (a null re-read must not silently skip it).
|
||||
tx.addOperation(
|
||||
new DeleteNounMetadataOperation(this.storage, id)
|
||||
new DeleteNounMetadataOperation(this.storage, id, metadata)
|
||||
)
|
||||
|
||||
// Operations 4+: Delete all related verbs atomically
|
||||
|
|
@ -6871,8 +6873,10 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
)
|
||||
}
|
||||
|
||||
// Pre-read metadata rides along: the count decrement must not
|
||||
// depend on re-reading the record being removed (see remove()).
|
||||
tx.addOperation(
|
||||
new DeleteNounMetadataOperation(this.storage, id)
|
||||
new DeleteNounMetadataOperation(this.storage, id, metadata)
|
||||
)
|
||||
|
||||
for (const verb of allVerbs) {
|
||||
|
|
@ -9265,7 +9269,9 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
if (metadata) {
|
||||
plan.operations.push(new RemoveFromMetadataIndexOperation(this.metadataIndex, id, metadata))
|
||||
}
|
||||
plan.operations.push(new DeleteNounMetadataOperation(this.storage, id))
|
||||
// Pre-read metadata rides along: the count decrement must not depend on
|
||||
// re-reading the record being removed (see remove()).
|
||||
plan.operations.push(new DeleteNounMetadataOperation(this.storage, id, metadata))
|
||||
for (const verb of cascade.values()) {
|
||||
plan.operations.push(
|
||||
// Endpoint ints resolve at EXECUTE time — a cascade verb (or its
|
||||
|
|
@ -15278,15 +15284,22 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
if (typeof pruner.pruneOrphanedEntities === 'function') {
|
||||
const orphans = await pruner.pruneOrphanedEntities()
|
||||
if (orphans.nouns.length + orphans.verbs.length > 0) {
|
||||
await pruner.rebuildTypeCounts?.()
|
||||
await pruner.rebuildSubtypeCounts?.()
|
||||
prodLog.warn(
|
||||
`[Brainy] repairIndex() pruned ${orphans.nouns.length} orphaned noun + ` +
|
||||
`${orphans.verbs.length} orphaned verb container(s) left by a pre-8.3.1 ` +
|
||||
`partial delete, and recomputed counts.`
|
||||
`partial delete.`
|
||||
)
|
||||
}
|
||||
}
|
||||
// SANCTIONED RECOUNT — unconditional, not gated on orphans found: the
|
||||
// persisted counters can be inflated over perfectly clean shelves (deletes
|
||||
// whose decrement was skipped by the removed-record re-read), and
|
||||
// Math.max(totalNounCount, scanned) means an inflated scalar can never
|
||||
// correct itself. rebuildTypeCounts() recomputes EVERY counter rollup
|
||||
// (scalar totals + per-type maps + type-statistics arrays) from one
|
||||
// canonical walk and persists them.
|
||||
await pruner.rebuildTypeCounts?.()
|
||||
await pruner.rebuildSubtypeCounts?.()
|
||||
|
||||
await this.metadataIndex.detectAndRepairCorruption()
|
||||
// Lift a failed-rollback write-quarantine: force a full rebuild so the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue