fix(8.0): real bugs surfaced by integration hardening — where-intersect, related() offset, relate() updatedAt

Three of the five bugs the rot pass surfaced (correct inputs → wrong output), fixed:

1. where-filter dropped all but the last operator on a field. getIdsForFilter()'s
   per-operator loop overwrote fieldResults each iteration, so
   { year: { greaterThan: 2009, lessThan: 2020 } } kept only lessThan. Now each
   operator's match set is AND-intersected (multi-operator-per-field works).
   (metadataIndex.ts)
2. related({ offset }) ignored the offset — getVerbs() zeroed it and smuggled it via
   an unimplemented cursor, so every page returned items [0, limit). Now the real
   offset is passed through to getVerbsWithPagination (which slices [offset,+limit)).
   (baseStorage.ts) — verified: get-relations-fix pagination test passes.
3. relate() never persisted updatedAt, so reads fabricated a fresh Date.now() each
   call (non-idempotent). Now createdAt and updatedAt share one timestamp at create.
   (brainy.ts) — verified: get-relations-fix equivalence test passes.

Follow-ups (precisely diagnosed, not papered over): exclusive range bounds —
ColumnStore.rangeQuery(field,min,max) is inclusive-only, so getIdsForRange() drops
includeMin/includeMax (metadataIndex.ts:996) and lessThan/greaterThan behave as
lte/gte when the column store is active (the dual-bound test's popularity:95 boundary
stays red); counts not rehydrating after restart; unscoped VFS path-cache.
This commit is contained in:
David Snelling 2026-06-17 13:19:54 -07:00
parent e5997a1516
commit 5eaf579937
3 changed files with 26 additions and 13 deletions

View file

@ -1820,7 +1820,14 @@ export class MetadataIndexManager implements MetadataIndexProvider {
if (condition && typeof condition === 'object' && !Array.isArray(condition)) {
// Handle Brainy Field Operators (canonical operators defined)
// See docs/api/README.md for complete operator reference
//
// Multiple operators on ONE field are AND-combined (intersected): e.g.
// { greaterThan: 2009, lessThan: 2020 } requires BOTH bounds to hold. Each
// operator computes its own match set, then intersects with the running set.
let opIndex = 0
for (const [op, operand] of Object.entries(condition)) {
const prevOpResults = fieldResults
fieldResults = []
switch (op) {
// ===== EQUALITY OPERATORS =====
// Canonical: 'eq' | Alias: 'equals' | Deprecated: 'is'
@ -1950,6 +1957,13 @@ export class MetadataIndexManager implements MetadataIndexProvider {
break
}
}
// Intersect this operator's matches with the running set (AND semantics
// for multiple operators on the same field).
if (opIndex > 0) {
const prevSet = new Set(prevOpResults)
fieldResults = fieldResults.filter((id) => prevSet.has(id))
}
opIndex++
}
} else {
// Direct value match (shorthand for 'eq' operator)