Filter Operator Conformance
In this section
cor is a drop-in accelerator, so the law for find() is same answers, only faster. This page is the per-operator accounting of that law: what cor does with every operator in Brainy's filter vocabulary, and why.
There are exactly three verdicts, and no fourth. An operator is served (cor returns what pure-JS Brainy returns), served beyond the baseline (cor returns the correct rows where pure-JS Brainy returns an empty result), or refused by name (cor throws, naming the operator and the field). What never happens is a quiet wrong answer.
The table
Operator | Verdict |
|---|---|
| served |
| served |
| served |
| served |
| served |
| served |
| served |
| served |
| served |
| served |
| served |
| served beyond the baseline |
| served beyond the baseline |
| served beyond the baseline |
| refused by name |
| refused by name |
| refused by name |
| refused by name |
Aliases are not a footnote here. An alias that silently went unhandled would answer empty on its own and drop its clause beside another operator — the same failure as an unhandled operator — so every alias spelling is pinned to return exactly its canonical twin's rows.
Why four operators are refused
startsWith, endsWith, length and matches are in Brainy's vocabulary, and cor refuses all four. This is a proof, not a shortfall.
The metadata index does not store your values; it stores normalized posting keys. Strings are lowercased and trimmed, arrays are posted one element at a time, numbers are canonicalized, and anything longer than 100 UTF-16 units is hashed. That normalization is what makes the index small and fast, and it erases exactly the distinctions these operators read:
These two rows… | …collapse to one posting key because of | …but need opposite answers from |
|---|---|---|
| lowercasing |
|
| trimming |
|
| one posting per array element |
|
| canonical numbers |
|
| one posting per array element |
|
Brainy's own matcher requires typeof value === 'string' for startsWith and endsWith, and Array.isArray(value) for length — so each pair above genuinely needs two different verdicts. No function of a single posting key can return two different answers, which makes serving these from the index impossible rather than merely unimplemented.
Two escape routes were measured and closed:
Answer a superset and let the host narrow.
find()does not re-apply the value matcher to what the metadata index returns — the index's answer for a clause is the final answer — so a superset ships as an under-constrained result set. Measured on the baseline engine, pinned in the suite.Read the raw values instead. The column store keeps raw strings, but it flattens arrays exactly as the index does and infers one type per field, so it cannot separate
['ax']from'ax'or recover an array's length either.
matches carries a second, independent reason: JavaScript RegExp and Rust's regex are different dialects (backreferences, lookaround, Unicode class spellings), so even with the raw value in hand a native evaluation would diverge on exactly the patterns that distinguish them.
What to write instead
The durable fix is to make the predicate's answer an indexed value at write time, then filter on it with a served operator:
Instead of | Write | Then filter |
|---|---|---|
| a |
|
| an |
|
| a |
|
| a |
|
Otherwise, narrow with a served operator and apply the predicate in your own code over the returned rows — the honest version of what a superset would have done, with the row budget visible to you.
Served beyond the baseline
hasAll, noneOf and excludes are posting-list expressions — an intersection, a universe-minus-union, and a negation of contains respectively — so cor serves them exactly. Pure-JS Brainy's index path currently answers all three with an empty result, which means cor and pure-JS Brainy differ on these three clauses today, and cor is the correct side. This is filed upstream; when Brainy serves them, the pins on the baseline's behavior red and this section comes off the page.
The set law — how the three answer at their edges
The index posts a field's value as a set of elements: a scalar is a singleton, an array is its elements, a missing field is the empty set. The set operators are served with exactly those semantics, and the edges are pinned in stringOperatorConformance.test.ts:
clause | set law | Brainy's value-matcher (never runs on |
|---|---|---|
| matches — the empty set lacks everything | no match (it presupposes an array) |
| no match — a scalar is a singleton set | no match, but for a different reason (non-array never matches) |
| no match — the sets intersect | matches (a literal |
| matches — | no match (requires |
| matches every row, missing fields included | matches array-valued fields only |
| refused by name — never match-everything | type mismatch, no match |
The matcher column is Brainy's value-level matcher frozen at 10.3.1 — it never runs on the find() path (pure-JS find() answers all of these []), so no shipped behavior disagrees with the set law today.
The two laws are a jointly frozen contract (agreed with Brainy 2026-08-19, VENUE-ENGINE-CONFORMANCE-GAPS), and they split by call site, never by pipeline stage: find() follows the set law end-to-end — index evaluation and find()'s own egress re-check — on both engines, so a row the index correctly serves under the set law is never silently stripped downstream. The value-level matcher keeps the matcher law for its direct callers only. Brainy's find() gains the same set-law serving (its index evaluator cure) plus operand-shape validation at ingress; until then its silent-empty baseline is pinned here.
One asymmetry is chosen, not drifted, and this row keeps it that way: Brainy's cure will serve raw startsWith/endsWith from its own registry, while cor keeps refusing them by name — a lowercased, trimmed index cannot answer raw prefix semantics, and a loud refusal beats a quietly wrong answer. Code that needs prefix queries under cor writes a slug field (see "What to write instead").
Known divergence: negation on a string field with case or padding
cor's metadata index normalizes a value before posting it — lowercased and trimmed — and Brainy's does not. For positive operators that difference is invisible: cor answers a superset and Brainy narrows it with an exact post-filter, so { title: 'Alice' } matches two rows inside cor's index and returns exactly the one row whose title really is 'Alice'.
A negation gets no such repair. cor computes "everything except this value's posting bucket", and that bucket holds every case and whitespace variant of the operand, so rows that merely look like the operand are dropped — and no post-filter can add a row back.
Measured, on a corpus holding both 'Alice' and 'alice':
await brain.find({ where: { title: { ne: 'alice' } } })
// pure-JS Brainy: includes the row titled 'Alice'
// cor: does notJSnoneOf and excludes inherit the same shape, and are affected more, because Brainy does not post-filter those at all.
Scope. Only string fields whose values differ from the operand solely by letter case or surrounding whitespace. Numeric fields, boolean fields, and string fields written in a consistent case (slugs, enums, ids — the usual shape for a field you filter on) are unaffected, and their parity is pinned. If your negation filters on free-text values, normalize case at write time until this is closed.
This is filed, and stringOperatorConformance pins the divergence's exact extent so that closing it reds the gate deliberately rather than silently.
The not combinator
not is not evaluated inside cor's posting-list dispatch. Brainy applies the negation itself, downstream of whatever the metadata-index provider returns, so cor deliberately returns the un-negated set and lets Brainy subtract — which is what its own JS index evaluator does. The suite pins parity in every position (not beside a field, not of an oneOf, not inside allOf, and standalone) rather than asserting a hand-written set, so cor tracks Brainy here by construction.
How this page stays true
src/native/stringOperatorConformance.test.ts runs both engines — a cor-backed brain and a pure-JS brain over byte-identical data — and compares their answers operator by operator. Nothing on this page is a hand-written expectation that could drift from what Brainy really does.
It also carries a vocabulary-completeness pin: it reads Brainy's operator tokens out of its shipped source at runtime and asserts every one is either served by cor or refused by cor by name. An operator Brainy adds in a future release lands in neither bucket and reds the gate — so the question "are we missing any operators?" is answered by the test suite forever, instead of by someone re-deriving it.