Filed by the domain:ui execution seat (session session_01NRRumy89BYdW9ogbcdHTho) from the measurement PR #7348 (#7221) landed as test-only pins. Dedupe: semantic search returns only #4744 (closed — ListView.convertFilterGroupToAST dropping a fresh null-ness row, the producer side, a different defect); control query hit. No open card names the in-memory matcher.
Serial constraint: dispatch after PR #7348 merges — its two it.fails pins are the red-first evidence this card flips.
The defect (measured on origin/main2956d7af8)
packages/core/src/adapters/ValueDataSource.ts:49matchesASTFilter(record, filterNode) recognises exactly two shapes: a node whose head is and / or, and a 3-element comparison node. Everything else reaches the closing return true. Two consequences, both silent:
- A flat implicit-AND array selects every row.
[['role','=','admin'],['age','>',24]] over rows a(admin,30), b(user,25), c(admin,20) returns a, b, c; the same rules as ['and',['role','=','admin'],['age','>',24]] return a only. A single-rule flat array [['role','=','admin']] is inert too — it is the shape, not the count. The gate's real two-source output ['and', [flat authored rules], [composed tuple]] loses its nested authored child the same way. - The null-ness operators are unimplemented. The operator
switch has no is_null / is_not_null / isnull / isnotnull arm; default returns true. The card's own filter [['visible_from','is_not_null'],['due_date','is_not_null']] therefore selects every row in BOTH dialects — the two "agreed" only at "no filter applied".
Why it is reachable. The flat array is what mergeFilterNodes emits for a lone surviving source and what ListView.tsx sends as $filter (finalFilter); resolveDataSource.ts:70 builds a ValueDataSource for provider: 'value'. So an inline-data list with an authored multi-rule filter shows every row, with no error and no console line — the silent wider answer an AI-authored metadata app hides best.
The wire contract already exists and is settled: the server's isFilterAST / parseFilterAST accept the flat legacy array, the 2-tuple comparison node and the and-wrapped form, and lower all of them to the identical {"$and":[…]} (pinned in packages/data-objectstack/src/filter-dialect-wire-7221.test.ts). The in-memory matcher is simply behind that vocabulary.
The fix (one executable criterion)
Teach the consumer the vocabulary the wire already has — no producer changes, no request byte moves:
matchesASTFilter reads a flat array of rules (top level, and as a child of and / or) as an implicit AND, the way the server does;is_null / is_not_null / isnull / isnotnull take their direction from the operator NAME and never read the value slot (2-tuple and 3-tuple-with-null both);- an operator or shape the matcher does not know must not fall through to
true — measure what the sibling adapters do for an unknown operator and match it (a thrown error or a logged false); do not widen further.
Criterion: the two it.fails cases named "diverges — objectui#7221" in packages/core/src/utils/__tests__/filter-dialect-equivalence-7221.test.ts go red on the fix and are converted to plain it in the same PR; new red-first cases cover the null-ness operators in both spellings; the ValueDataSource suite and packages/core are green. @object-ui/core: patch.
Deliberately NOT proposed
Producer unification — making mergeFilterNodes always emit ['and', …] so one authored filter has one lowered form. That would change the wire string of every lone-source filter shipped today and needs a ruling nobody has asked for; it is recorded on #7221 as the tidiness question that remains after this fix, not as work.
Refs: #7221 (the measurement; blocked on this card) · PR #7348 (the pins) · #7210 (the unlowered ViewFilterRule[] producer that reaches the adapter bypassing toFilterNode — named there, not here).
Filed by the
domain:uiexecution seat (sessionsession_01NRRumy89BYdW9ogbcdHTho) from the measurement PR #7348 (#7221) landed as test-only pins. Dedupe: semantic search returns only #4744 (closed —ListView.convertFilterGroupToASTdropping a fresh null-ness row, the producer side, a different defect); control query hit. No open card names the in-memory matcher.Serial constraint: dispatch after PR #7348 merges — its two
it.failspins are the red-first evidence this card flips.The defect (measured on
origin/main2956d7af8)packages/core/src/adapters/ValueDataSource.ts:49matchesASTFilter(record, filterNode)recognises exactly two shapes: a node whose head isand/or, and a 3-element comparison node. Everything else reaches the closingreturn true. Two consequences, both silent:[['role','=','admin'],['age','>',24]]over rowsa(admin,30),b(user,25),c(admin,20)returns a, b, c; the same rules as['and',['role','=','admin'],['age','>',24]]return a only. A single-rule flat array[['role','=','admin']]is inert too — it is the shape, not the count. The gate's real two-source output['and', [flat authored rules], [composed tuple]]loses its nested authored child the same way.switchhas nois_null/is_not_null/isnull/isnotnullarm;defaultreturnstrue. The card's own filter[['visible_from','is_not_null'],['due_date','is_not_null']]therefore selects every row in BOTH dialects — the two "agreed" only at "no filter applied".Why it is reachable. The flat array is what
mergeFilterNodesemits for a lone surviving source and whatListView.tsxsends as$filter(finalFilter);resolveDataSource.ts:70builds aValueDataSourceforprovider: 'value'. So an inline-data list with an authored multi-rule filter shows every row, with no error and no console line — the silent wider answer an AI-authored metadata app hides best.The wire contract already exists and is settled: the server's
isFilterAST/parseFilterASTaccept the flat legacy array, the 2-tuple comparison node and theand-wrapped form, and lower all of them to the identical{"$and":[…]}(pinned inpackages/data-objectstack/src/filter-dialect-wire-7221.test.ts). The in-memory matcher is simply behind that vocabulary.The fix (one executable criterion)
Teach the consumer the vocabulary the wire already has — no producer changes, no request byte moves:
matchesASTFilterreads a flat array of rules (top level, and as a child ofand/or) as an implicit AND, the way the server does;is_null/is_not_null/isnull/isnotnulltake their direction from the operator NAME and never read the value slot (2-tuple and 3-tuple-with-nullboth);true— measure what the sibling adapters do for an unknown operator and match it (a thrown error or a loggedfalse); do not widen further.Criterion: the two
it.failscases named "diverges — objectui#7221" inpackages/core/src/utils/__tests__/filter-dialect-equivalence-7221.test.tsgo red on the fix and are converted to plainitin the same PR; new red-first cases cover the null-ness operators in both spellings; theValueDataSourcesuite andpackages/coreare green.@object-ui/core: patch.Deliberately NOT proposed
Producer unification — making
mergeFilterNodesalways emit['and', …]so one authored filter has one lowered form. That would change the wire string of every lone-source filter shipped today and needs a ruling nobody has asked for; it is recorded on #7221 as the tidiness question that remains after this fix, not as work.Refs: #7221 (the measurement; blocked on this card) · PR #7348 (the pins) · #7210 (the unlowered
ViewFilterRule[]producer that reaches the adapter bypassingtoFilterNode— named there, not here).