Found by the domain:ui os-dev seat while running objectui#8568's prerequisite census (PR for that card is separate; this is out of its scope, which is measurement only and zero behaviour change). Grading and domain:* are the triage seat's. ⛔ Not claimed.
The class
objectui#8568 records that convertFiltersToAST and ValueDataSource answer one $-dialect with two acceptance sets, and names the direction where the converter is MORE tolerant (four lowercase aliases the matcher refuses).
There is a third leg, in the opposite direction, and it is not on that card: $icontains is a canonical member of the spec's FILTER_OPERATORS, ValueDataSource executes it, and convertFiltersToAST refuses it with a 400 envelope.
Measured
origin/main 3ff3fc63f, spec 17.4.0, both legs in one process, with a control operator both sides agree on so the difference is about $icontains rather than about the harness:
spec FILTER_OPERATORS includes $icontains: true
LOWERING $icontains => THREW FilterOperatorError / INVALID_FILTER / 400
LOWERING CONTROL $contains => ["name","contains","AC"]
MATCHER $icontains => rows ["ACME"]
MATCHER CONTROL $contains => rows ["ACME"]
The refusal message enumerates its own supported set, and $icontains is absent from it:
[ObjectUI] Unknown filter operator '$icontains' for field 'name'. Supported operators:
$eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $between, $contains, $notContains,
$startsWith, $endsWith, $null, $exists.
Why this is a contract violation and not a preference
@objectstack/spec data/filter.zod.ts declares:
export const FILTER_OPERATORS = [
'$eq', '$ne',
'$gt', '$gte', '$lt', '$lte',
'$in', '$nin', '$between',
'$contains', '$notContains', '$startsWith', '$endsWith', '$icontains',
'$null', '$exists',
] as const;
$icontains is in it. packages/core/src/adapters/README.md goes further and names $icontains as the prescription printed when $like / $ilike / $regex are refused — so the repo tells an author to write the one spelling one of its two lowering paths rejects.
This repo is itself a producer of it
packages/fields/src/widgets/FilterConditionField.tsx emits it for the containsCaseInsensitive builder operator:
case 'containsCaseInsensitive': return { [field]: { $icontains: value } };
That arm exists because objectui#4023 / objectui#3567 (both CLOSED) fixed the case-insensitive contains being unreachable from the filter UI. kvToCondition reads it back. So an admin who picks "contains (ignore case)" writes criteria that the in-memory matcher honours and the ObjectStack lowering path refuses.
⚠️ The end-to-end reach from criteria_json into convertFiltersToAST is not provable from this repo — the criteria store is not in this tree, and this is exactly the consumer-local caveat objectui#6839 established. What IS proven here is the acceptance-set disagreement and the producer arm.
Why no existing pin catches it
PR #8565's reconciliation pin (packages/data-objectstack/src/readme-filter-operator-table.test.ts) holds the README's operator tables complete against two populations: the keys of operatorMap, and the operators the unknown-operator error calls supported. $icontains is in neither, so the pin is structurally blind to it — and packages/data-objectstack/README.md carries no row for $icontains in its supported table or its refused table. Measured: git grep -n icontains -- packages/data-objectstack/ returns nothing.
Directions (not ruled here)
- Add
$icontains to operatorMap so the lowering matches the spec and the matcher. icontains is a member of the spec's VALID_AST_OPERATORS (it is already an identity row in FILTER_OPERATOR_ALIASES for the object-form dialect, with a comment recording that check), so nothing downstream needs inventing.
- Refuse it deliberately and say so — a refused-table row plus a prescription, the way
$like / $regex are handled. That contradicts the adapters README, which currently prescribes $icontains.
- Whatever is chosen, the pin's blind spot is worth closing: it should hold the tables against the spec's
FILTER_OPERATORS too, not only against the code's own two lists — the omission is invisible to it by construction.
Dedup
Two channels, each with controls.
- REST issue listing, complete and reconciled: all 459 open items enumerated across 5 pages,
open_issues_count 459 = 459 listed. Local grep of title+body: icontains hits objectui#7750 and objectui#7383, containsCaseInsensitive hits objectui#7561. Positive control ValueDataSource fires with 12 hits; negative control ZZQQNOTAWORDZZ reads 0.
- One targeted semantic search including closed: returns objectui#4023 and objectui#3567 (both the PRODUCER side, both closed and fixed), plus objectui#8568 itself as a lit control.
None of them names the lowering path refusing $icontains: objectui#7383 is like / ilike in the matcher's AST dialect; objectui#7561 is filter-builder's dropdown-vs-mirror vocabularies; objectui#7750 is DataScopeManager. No card names this.
Found by the
domain:uios-devseat while running objectui#8568's prerequisite census (PR for that card is separate; this is out of its scope, which is measurement only and zero behaviour change). Grading anddomain:*are the triage seat's. ⛔ Not claimed.The class
objectui#8568 records that
convertFiltersToASTandValueDataSourceanswer one$-dialect with two acceptance sets, and names the direction where the converter is MORE tolerant (four lowercase aliases the matcher refuses).There is a third leg, in the opposite direction, and it is not on that card:
$icontainsis a canonical member of the spec'sFILTER_OPERATORS,ValueDataSourceexecutes it, andconvertFiltersToASTrefuses it with a 400 envelope.Measured
origin/main3ff3fc63f, spec17.4.0, both legs in one process, with a control operator both sides agree on so the difference is about$icontainsrather than about the harness:The refusal message enumerates its own supported set, and
$icontainsis absent from it:Why this is a contract violation and not a preference
@objectstack/specdata/filter.zod.tsdeclares:$icontainsis in it.packages/core/src/adapters/README.mdgoes further and names$icontainsas the prescription printed when$like/$ilike/$regexare refused — so the repo tells an author to write the one spelling one of its two lowering paths rejects.This repo is itself a producer of it
packages/fields/src/widgets/FilterConditionField.tsxemits it for thecontainsCaseInsensitivebuilder operator:That arm exists because objectui#4023 / objectui#3567 (both CLOSED) fixed the case-insensitive contains being unreachable from the filter UI.
kvToConditionreads it back. So an admin who picks "contains (ignore case)" writes criteria that the in-memory matcher honours and the ObjectStack lowering path refuses.criteria_jsonintoconvertFiltersToASTis not provable from this repo — the criteria store is not in this tree, and this is exactly the consumer-local caveat objectui#6839 established. What IS proven here is the acceptance-set disagreement and the producer arm.Why no existing pin catches it
PR #8565's reconciliation pin (
packages/data-objectstack/src/readme-filter-operator-table.test.ts) holds the README's operator tables complete against two populations: the keys ofoperatorMap, and the operators the unknown-operator error calls supported.$icontainsis in neither, so the pin is structurally blind to it — andpackages/data-objectstack/README.mdcarries no row for$icontainsin its supported table or its refused table. Measured:git grep -n icontains -- packages/data-objectstack/returns nothing.Directions (not ruled here)
$icontainstooperatorMapso the lowering matches the spec and the matcher.icontainsis a member of the spec'sVALID_AST_OPERATORS(it is already an identity row inFILTER_OPERATOR_ALIASESfor the object-form dialect, with a comment recording that check), so nothing downstream needs inventing.$like/$regexare handled. That contradicts the adapters README, which currently prescribes$icontains.FILTER_OPERATORStoo, not only against the code's own two lists — the omission is invisible to it by construction.Dedup
Two channels, each with controls.
open_issues_count459 = 459 listed. Local grep of title+body:icontainshits objectui#7750 and objectui#7383,containsCaseInsensitivehits objectui#7561. Positive controlValueDataSourcefires with 12 hits; negative controlZZQQNOTAWORDZZreads 0.None of them names the lowering path refusing
$icontains: objectui#7383 islike/ilikein the matcher's AST dialect; objectui#7561 isfilter-builder's dropdown-vs-mirror vocabularies; objectui#7750 isDataScopeManager. No card names this.