fix: re-validate find() results against the predicate (index-integrity guard)
find() trusted the metadata index's returned ids: it loaded each id's entity
and returned it, checking only that the entity existed — never that it actually
matched the query. The indexes are acceleration structures; the loaded entity
is ground truth. A stale or cross-bucket index posting (e.g. an id left in a
field-value bucket by a delete or an `update({ field: undefined })`) therefore
surfaced an entity matching NEITHER the requested type NOR the where filter — a
production report saw a timeslot (NounType.Event) returned for
`find({ type: Person, where: { entityType: 'staff' } })`.
Add a single egress chokepoint after the result IIFE that re-validates every
result with a new `entityMatchesFindParams` predicate (type/subtype/where/
service/excludeVFS, reusing matchesMetadataFilter for the where leg). It covers
every find() branch (metadata, vector, text, proximity, graph) and similar()
(which delegates to find) in one place. A no-op on a healthy index; on a
corrupted one it drops the bad row instead of returning a phantom. Full unit
gate green (1535) confirms the where re-validation is consistent with the index
(no valid rows dropped).
This is the safety net. The durable corruption itself lives in the metadata
index that owns the query (the native provider when present) and is addressed
separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9593a27338
commit
7b5db0ddf9
2 changed files with 154 additions and 1 deletions
|
|
@ -1407,6 +1407,43 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Index-integrity predicate: does this loaded entity actually satisfy the
|
||||
* find() metadata predicate (type/subtype/service/excludeVFS/where)?
|
||||
*
|
||||
* find() trusts the ids the metadata index returns; the loaded entity is
|
||||
* ground truth. A stale or cross-bucket index posting (e.g. an id left in a
|
||||
* field-value bucket by a delete or an `update({ field: undefined })`) can
|
||||
* otherwise surface an entity matching NEITHER the requested type NOR the
|
||||
* where filter. Re-validating each result against this predicate before
|
||||
* returning restores the invariant "find never returns an entity that does
|
||||
* not match its query." The structural legs are exact; the `where` leg reuses
|
||||
* `matchesMetadataFilter` — the same matcher the filter path itself uses — so
|
||||
* a healthy index is a no-op.
|
||||
*/
|
||||
private entityMatchesFindParams(entity: Entity<T>, params: FindParams<T>): boolean {
|
||||
if (params.type !== undefined) {
|
||||
const types = Array.isArray(params.type) ? params.type : [params.type]
|
||||
if (!types.includes(entity.type)) return false
|
||||
}
|
||||
if (params.subtype !== undefined) {
|
||||
const subtypes = Array.isArray(params.subtype) ? params.subtype : [params.subtype]
|
||||
if (entity.subtype === undefined || !subtypes.includes(entity.subtype)) return false
|
||||
}
|
||||
if (params.service !== undefined && entity.service !== params.service) {
|
||||
return false
|
||||
}
|
||||
if (params.excludeVFS === true) {
|
||||
const md = (entity.metadata ?? {}) as Record<string, unknown>
|
||||
if (md.vfsType !== undefined) return false
|
||||
if (md.isVFSEntity === true || md.isVFS === true) return false
|
||||
}
|
||||
if (params.where !== undefined) {
|
||||
if (!matchesMetadataFilter((entity.metadata ?? {}) as any, params.where as any)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a noun from storage to an entity (SIMPLIFIED!)
|
||||
*
|
||||
|
|
@ -3177,7 +3214,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
const isHidden = (id: string): boolean => hiddenIds.size > 0 && hiddenIds.has(id)
|
||||
|
||||
const startTime = Date.now()
|
||||
const result = await (async () => {
|
||||
let result = await (async () => {
|
||||
let results: Result<T>[] = []
|
||||
|
||||
// Distinguish between search criteria (need vector search) and filter criteria (metadata only)
|
||||
|
|
@ -3618,6 +3655,21 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
return results.slice(finalOffset, finalOffset + limit)
|
||||
})()
|
||||
|
||||
// Index-integrity guard — applied ONCE here so every find() path (metadata,
|
||||
// vector, text, proximity, graph) is covered uniformly. The indexes are
|
||||
// acceleration structures; the loaded entity is ground truth. Re-validate
|
||||
// each result against the query predicate so a stale or cross-bucket index
|
||||
// entry — e.g. an id left in a field-value posting by a delete or an
|
||||
// `update({ field: undefined })` — can never surface an entity that does not
|
||||
// actually match `type`/`subtype`/`where`/`service`/`excludeVFS`. On a
|
||||
// healthy index this is a no-op; on a corrupted one it drops the bad row
|
||||
// instead of returning a phantom.
|
||||
if (result.length > 0) {
|
||||
result = result.filter(
|
||||
(r) => r.entity != null && this.entityMatchesFindParams(r.entity, params)
|
||||
)
|
||||
}
|
||||
|
||||
// Record performance for auto-tuning
|
||||
const duration = Date.now() - startTime
|
||||
recordQueryPerformance(duration, result.length)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue