fix: never serve a silent [] from find({connected}) on a cold-loaded graph
On a cold process start of a large brain (above the eager index-rebuild
threshold), a native graph adjacency can report size()>0 — its membership set
reloaded — while the source->target edges did NOT load, so find({connected}),
neighbors() and related() returned [] for edges that are persisted on disk. A
database returning empty for data that exists, based purely on warm/cold state,
is a correctness bug; it hit a production deployment after every deploy.
Refactor the cold-load guard into verifyGraphAdjacencyLive(), which gates its
heal/throw decision on a GLOBAL known-edge sample (a persisted verb's source,
which by definition has an outgoing edge) rather than the queried anchor: a
stale adjacency is rebuilt from storage, an unrecoverable one throws
GraphIndexNotReadyError instead of serving [], and a genuinely edgeless anchor
still returns [] with no spurious rebuild. executeGraphSearch re-verifies before
trusting an empty connected result and re-collects after a heal. Adds a 5-case
integration test (self-heal / loud-throw / edgeless-no-false-positive / healthy
/ re-collect) plus updated unit coverage.
This commit is contained in:
parent
d1665bb1a9
commit
fd699d0e07
3 changed files with 251 additions and 33 deletions
|
|
@ -52,7 +52,7 @@ describe('7.33.2 graph-adjacency cold-load consistency guard', () => {
|
|||
const calls = stubGraphIndex(brain, { size: 12, neighborsBeforeRebuild: ['n1'] })
|
||||
brain.storage.getVerbs = async () => ({ items: [{ id: 'v1', sourceId: 's1', targetId: 't1' }], hasMore: false })
|
||||
|
||||
await brain.ensureGraphAdjacencyConsistent()
|
||||
await brain.verifyGraphAdjacencyLive()
|
||||
expect(calls.rebuild).toBe(0)
|
||||
await brain.close()
|
||||
})
|
||||
|
|
@ -62,11 +62,11 @@ describe('7.33.2 graph-adjacency cold-load consistency guard', () => {
|
|||
const calls = stubGraphIndex(brain, { size: 12, neighborsBeforeRebuild: [], neighborsAfterRebuild: ['n1'] })
|
||||
brain.storage.getVerbs = async () => ({ items: [{ id: 'v1', sourceId: 's1', targetId: 't1' }], hasMore: false })
|
||||
|
||||
await brain.ensureGraphAdjacencyConsistent()
|
||||
await brain.verifyGraphAdjacencyLive()
|
||||
expect(calls.rebuild).toBe(1) // detected the empty adjacency + rebuilt it
|
||||
|
||||
// Cached: a second call does not re-probe or re-rebuild.
|
||||
await brain.ensureGraphAdjacencyConsistent()
|
||||
await brain.verifyGraphAdjacencyLive()
|
||||
expect(calls.rebuild).toBe(1)
|
||||
await brain.close()
|
||||
})
|
||||
|
|
@ -76,7 +76,7 @@ describe('7.33.2 graph-adjacency cold-load consistency guard', () => {
|
|||
stubGraphIndex(brain, { size: 12, neighborsBeforeRebuild: [], neighborsAfterRebuild: [] }) // never loads
|
||||
brain.storage.getVerbs = async () => ({ items: [{ id: 'v1', sourceId: 's1', targetId: 't1' }], hasMore: false })
|
||||
|
||||
await expect(brain.ensureGraphAdjacencyConsistent()).rejects.toBeInstanceOf(GraphIndexNotReadyError)
|
||||
await expect(brain.verifyGraphAdjacencyLive()).rejects.toBeInstanceOf(GraphIndexNotReadyError)
|
||||
await brain.close()
|
||||
})
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ describe('7.33.2 graph-adjacency cold-load consistency guard', () => {
|
|||
return { items: [], hasMore: false }
|
||||
}
|
||||
|
||||
await brain.ensureGraphAdjacencyConsistent()
|
||||
await brain.verifyGraphAdjacencyLive()
|
||||
expect(calls.rebuild).toBe(0)
|
||||
expect(probedStorage).toBe(false)
|
||||
await brain.close()
|
||||
|
|
@ -100,7 +100,7 @@ describe('7.33.2 graph-adjacency cold-load consistency guard', () => {
|
|||
const calls = stubGraphIndex(brain, { size: 12, neighborsBeforeRebuild: [] })
|
||||
brain.storage.getVerbs = async () => ({ items: [], hasMore: false })
|
||||
|
||||
await brain.ensureGraphAdjacencyConsistent()
|
||||
await brain.verifyGraphAdjacencyLive()
|
||||
expect(calls.rebuild).toBe(0)
|
||||
await brain.close()
|
||||
})
|
||||
|
|
@ -118,12 +118,12 @@ describe('7.33.2 graph-adjacency cold-load consistency guard', () => {
|
|||
}
|
||||
|
||||
// First call: probe throws transiently → swallowed (no GraphIndexNotReadyError), flag reset.
|
||||
await brain.ensureGraphAdjacencyConsistent()
|
||||
await brain.verifyGraphAdjacencyLive()
|
||||
expect(calls.rebuild).toBe(0)
|
||||
expect(brain._graphAdjacencyVerified).toBe(false) // re-check allowed
|
||||
|
||||
// Second call: storage works → detects the empty adjacency + rebuilds.
|
||||
await brain.ensureGraphAdjacencyConsistent()
|
||||
await brain.verifyGraphAdjacencyLive()
|
||||
expect(calls.rebuild).toBe(1)
|
||||
await brain.close()
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue