fix(storage): enumeration re-keys on the identity record, not the vector leg
The noun/verb pagination walks (getNounsWithPagination, getNounIdsWithPagination, getVerbsWithPagination) listed shard contents by filtering for vectors.json, while the canonical count ledger has always counted a row by its metadata.json presence alone. A row with metadata and no vector file was therefore counted by the ledger but never yielded by the walk — a permanent "counted but invisible" phantom for any downstream consumer that iterates the walk to account for the ledger's total. Nouns now enumerate by metadata.json and hydrate the vector leg optionally, yielding the sanctioned unvectored shape (vector: []) when it's absent. Verbs enumerate the same way, but a metadata-only verb row can only be fully reconstructed when sourceId/targetId happen to be recoverable from metadata (never true for a current production write — those fields live only in the vector leg); otherwise the row is counted but loudly skipped rather than fabricated, since a phantom edge with fake endpoints would be worse than the original defect. Separately, GenerationStore's recovery-fold replay (replayFact) now applies preserve-if-absent: a metadata-only after-image replayed over an already- vectored row carries the existing vector forward instead of deleting it via writeNounRaw/writeVerbRaw's exact-restore null-means-delete contract (which must stay exact for transaction-abort rollback). A genuine tombstone still removes both legs.
This commit is contained in:
parent
4c7b0fab7a
commit
f8d8ce16b9
3 changed files with 519 additions and 25 deletions
|
|
@ -654,10 +654,43 @@ export class GenerationStore {
|
|||
let replayed = 0
|
||||
const replayFact = async (fact: CommitFact): Promise<void> => {
|
||||
for (const op of fact.ops) {
|
||||
const image =
|
||||
op.record === null
|
||||
? { metadata: null, vector: null }
|
||||
: { metadata: op.record.metadata, vector: op.record.vector }
|
||||
let image: { metadata: unknown | null; vector: unknown | null }
|
||||
if (op.record === null) {
|
||||
// A genuine tombstone (both legs absent) — the fold removes
|
||||
// both legs, exactly like `writeNounRaw`/`writeVerbRaw`'s raw
|
||||
// exact-restore contract.
|
||||
image = { metadata: null, vector: null }
|
||||
} else if (
|
||||
op.record.metadata !== null &&
|
||||
(op.record.vector === null || op.record.vector === undefined)
|
||||
) {
|
||||
// PRESERVE-IF-ABSENT (population law, ADR-008 G1 — the fold's
|
||||
// half): a metadata-only after-image must never DELETE an
|
||||
// existing vector leg through the fold. `writeNounRaw`/
|
||||
// `writeVerbRaw` are exact-restore primitives — a `vector:
|
||||
// null` there means "delete", which is exactly right for
|
||||
// `rollBackUncommittedGeneration`'s before-image restore (a
|
||||
// transaction abort legitimately un-writes a vector the failed
|
||||
// transaction added). It is NOT right here: this fold replays
|
||||
// AFTER-IMAGES, and re-applying an already-intact record must
|
||||
// be byte-safe (this module's own invariant, see the log-authority
|
||||
// comment above) — silently erasing a landed vector because one
|
||||
// replayed fact's vector leg came back null is the exact defect
|
||||
// that left metadata-counted, never-enumerated rows in a
|
||||
// production store (confirmed root cause: the enumeration walk
|
||||
// used to key on the vector leg, so a preserved-but-then-deleted
|
||||
// vector made the row invisible while the ledger still counted
|
||||
// it by metadata). A genuine "unvector" has its own sanctioned,
|
||||
// ledger-correct path (`Brainy.unvectorNounForRootMigration`) —
|
||||
// never this raw primitive, and never the fold.
|
||||
const current =
|
||||
op.kind === 'verb'
|
||||
? await this.storage.readVerbRaw(op.id)
|
||||
: await this.storage.readNounRaw(op.id)
|
||||
image = { metadata: op.record.metadata, vector: current.vector ?? null }
|
||||
} else {
|
||||
image = { metadata: op.record.metadata, vector: op.record.vector }
|
||||
}
|
||||
if (op.kind === 'verb') await this.storage.writeVerbRaw(op.id, image)
|
||||
else await this.storage.writeNounRaw(op.id, image)
|
||||
this.noteCheckpointDirty(op.kind, op.id)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue