fix: full-removal canonical deletes + family-scoped migration gate
Two production-reported spine fixes: - Canonical delete is a FULL removal: remove()/removeMany() deleted the metadata leg but never the canonical vectors.json leg or the <id>/ directory, leaving ghost rows that inflated enumerated counts forever, read as damage scars, and caused duplicate readdir entries for re-created VFS paths (the stale-unpost path). The delete operation now routes through storage.deleteNoun — both legs + the entity container — with a full two-leg before-image rollback; deleteVerb symmetric; the blind 'no metadata file' catch that masked real faults is gone. The generation log still holds the delete's before-image, so asOf() history is unchanged. repairIndex() gains a conservative, loud orphan-prune sweep (containers with no metadata content leg) + count recompute for stores damaged by earlier versions. - Family-scoped migration gate: every read blocked on the whole-brain migration lock even when the migrating index was irrelevant. The gate now scopes to the index families a read actually consults — canonical reads never wait; find() waits only on its query shape's families; graph traversals wait only on the graph family. Writes keep the conservative whole-brain wait. A read that needs the migrating family still blocks (bounded) with the retryable MigrationInProgressError.
This commit is contained in:
parent
1d26988963
commit
366f9a91f5
8 changed files with 657 additions and 66 deletions
|
|
@ -104,13 +104,26 @@ export class SaveNounOperation implements Operation {
|
|||
}
|
||||
|
||||
/**
|
||||
* Delete noun metadata with rollback support
|
||||
* Delete a noun — FULL canonical removal, with rollback support.
|
||||
*
|
||||
* Despite the historical name, this removes the WHOLE entity: both canonical
|
||||
* legs (metadata + vector) AND the entity's container. Previously it deleted
|
||||
* only the metadata leg via `deleteNounMetadata`, leaving the canonical
|
||||
* `vectors.json` leg and the `<id>/` directory orphaned on disk — a "ghost"
|
||||
* that reads as absent (getNoun needs both legs) yet inflates the enumerated
|
||||
* count and can never be told apart from a damage scar. Routing through
|
||||
* `storage.deleteNoun` removes both legs + the container in one place.
|
||||
*
|
||||
* Immutability is preserved: this cleans only the live-HEAD projection; the
|
||||
* generation store retains the before-image so `asOf()` still reconstructs the
|
||||
* deleted entity until retention expires.
|
||||
*
|
||||
* Rollback strategy:
|
||||
* - Restore deleted metadata
|
||||
* - Restore BOTH legs from the before-image (vector leg raw, metadata leg via
|
||||
* the count-aware save so deleteNoun()'s decrement is reversed).
|
||||
*/
|
||||
export class DeleteNounMetadataOperation implements Operation {
|
||||
readonly name = 'DeleteNounMetadata'
|
||||
readonly name = 'DeleteNoun'
|
||||
|
||||
constructor(
|
||||
private readonly storage: StorageAdapter,
|
||||
|
|
@ -118,21 +131,34 @@ export class DeleteNounMetadataOperation implements Operation {
|
|||
) {}
|
||||
|
||||
async execute(): Promise<RollbackAction> {
|
||||
// Get metadata before deletion (for rollback)
|
||||
// Capture the FULL before-image (both legs) so the undo restores the whole
|
||||
// entity — a metadata-only rollback would leave the vector leg unrestored.
|
||||
const previousNoun = await this.storage.getNoun(this.id)
|
||||
const previousMetadata = await this.storage.getNounMetadata(this.id)
|
||||
|
||||
if (!previousMetadata) {
|
||||
if (!previousNoun && !previousMetadata) {
|
||||
// Nothing to delete - no rollback needed
|
||||
return async () => {}
|
||||
}
|
||||
|
||||
// Delete metadata
|
||||
await this.storage.deleteNounMetadata(this.id)
|
||||
// Full removal: both canonical legs + the entity container + count decrement.
|
||||
await this.storage.deleteNoun(this.id)
|
||||
|
||||
// Return rollback action
|
||||
return async () => {
|
||||
// Restore deleted metadata
|
||||
await this.storage.saveNounMetadata(this.id, previousMetadata)
|
||||
// Restore the vector leg, then the metadata leg through the count-aware
|
||||
// save so deleteNoun()'s decrement is reversed.
|
||||
if (previousNoun) {
|
||||
await this.storage.saveNoun({
|
||||
id: previousNoun.id,
|
||||
vector: previousNoun.vector,
|
||||
connections: previousNoun.connections || new Map(),
|
||||
level: previousNoun.level || 0
|
||||
})
|
||||
}
|
||||
if (previousMetadata) {
|
||||
await this.storage.saveNounMetadata(this.id, previousMetadata)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue