Found by the os-dev seat while implementing the filter-builder group of #6939 (PR #7560). Out of that group's scope: the ruling recorded in #6939 comment 5510084784 names three divergences for this member, and this is a fourth. Grading and domain:* are the triage seat's.
The class
filter-builder has TWO operator vocabularies and they are very nearly disjoint. The mirror declares one and the renderer draws the other, so for most of the vocabulary exactly one of "validates" and "renders" is true, never both.
FilterOperatorSchema (packages/types/src/zod/complex.zod.ts) is the spec's canonical snake_case set: equals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, greater_than_or_equal, less_than, less_than_or_equal, in, not_in, is_null, is_not_null.
- The operator dropdown (
packages/components/src/custom/filter-builder.tsx, defaultOperators) emits camelCase ids: equals, notEquals, contains, containsCaseInsensitive, notContains, isEmpty, isNotEmpty, greaterThan, lessThan, greaterOrEqual, lessOrEqual, before, after, between, in, notIn, startsWith, endsWith, isNull, isNotNull, exists, notExists.
The row's operator is rendered as Select value={condition.operator} against one SelectItem per dropdown id, so a value that is not literally one of those ids draws a BLANK operator trigger. The overlap between the two sets is three members: equals, contains, in.
⇒ Every spelling the dropdown emits except those three is REFUSED by the mirror. Every spelling the mirror accepts except those three renders BLANK.
Measured
Rendered through the real SchemaRenderer on origin/main 3e01cb55f, examples/schema-catalog/.../product-search.json (which authors eq / lt / gt, a third spelling — the spec's alias table):
as authored 'Product Search Filters Where AND Clear all Category Remove condition Price Remove condition Stock Quantity Remove condition Add filter'
operators corrected 'Product Search Filters Where AND Clear all Category Equals Remove condition Price Less than Remove condition Stock Quantity Greater than Remove condition Add filter'
The three operator cells are EMPTY as authored and carry their label once the ids are the dropdown's. Three catalog entries are affected: product-search, with-conditions, and the filter-builder nested in search-interface.
Consequence for #6939's own pin: after PR #7560 lands all three ruled divergences, 2 of the 4 census entries validate and 2 do not, and the residual issue is value.conditions.N.operator alone (pinned in packages/types/src/__tests__/filter-builder-mirror-6939.test.ts — swapping only those three spellings makes both entries parse).
Why this is a ruling and not a repair
Three candidate resolutions, none of them local:
- The mirror follows the dropdown (camelCase ids). Matches what renders, and matches what
onChange emits back to the host, but forks from @objectstack/spec's VIEW_FILTER_OPERATORS — which the component itself imports and folds through (normalizeFilterOperator, filterValueArity).
- The dropdown follows the mirror (canonical snake_case ids). Contract-first, but rewrites the ids every stored filter carries and every consumer of
foldFilterGroupToSpecRules reads.
- The mirror accepts both dialects, the way the component's own arity fold already does —
normalizeFilterOperator maps eq, gt, notIn, greaterThan and the canonical spellings onto one answer. Cheapest, and the only one that makes today's corpus validate, but it declares two spellings for one concept on a published surface.
Note that the component ALREADY resolves both dialects everywhere the operator's MEANING matters (filterValueArity, reconcileOperatorForField); the one place it does not is the Select's identity comparison, which is what produces the blank trigger.
Reproduce
pnpm exec vitest run packages/types/src/__tests__/filter-builder-mirror-6939.test.ts
The two the residual refusal is the operator alias and NOTHING else assertions are the measurement.
Related
Generated by Claude Code
Found by the
os-devseat while implementing thefilter-buildergroup of #6939 (PR #7560). Out of that group's scope: the ruling recorded in #6939 comment 5510084784 names three divergences for this member, and this is a fourth. Grading anddomain:*are the triage seat's.The class
filter-builderhas TWO operator vocabularies and they are very nearly disjoint. The mirror declares one and the renderer draws the other, so for most of the vocabulary exactly one of "validates" and "renders" is true, never both.FilterOperatorSchema(packages/types/src/zod/complex.zod.ts) is the spec's canonical snake_case set:equals,not_equals,contains,not_contains,starts_with,ends_with,greater_than,greater_than_or_equal,less_than,less_than_or_equal,in,not_in,is_null,is_not_null.packages/components/src/custom/filter-builder.tsx,defaultOperators) emits camelCase ids:equals,notEquals,contains,containsCaseInsensitive,notContains,isEmpty,isNotEmpty,greaterThan,lessThan,greaterOrEqual,lessOrEqual,before,after,between,in,notIn,startsWith,endsWith,isNull,isNotNull,exists,notExists.The row's operator is rendered as
Select value={condition.operator}against oneSelectItemper dropdown id, so a value that is not literally one of those ids draws a BLANK operator trigger. The overlap between the two sets is three members:equals,contains,in.⇒ Every spelling the dropdown emits except those three is REFUSED by the mirror. Every spelling the mirror accepts except those three renders BLANK.
Measured
Rendered through the real
SchemaRendereronorigin/main3e01cb55f,examples/schema-catalog/.../product-search.json(which authorseq/lt/gt, a third spelling — the spec's alias table):The three operator cells are EMPTY as authored and carry their label once the ids are the dropdown's. Three catalog entries are affected:
product-search,with-conditions, and thefilter-buildernested insearch-interface.Consequence for #6939's own pin: after PR #7560 lands all three ruled divergences, 2 of the 4 census entries validate and 2 do not, and the residual issue is
value.conditions.N.operatoralone (pinned inpackages/types/src/__tests__/filter-builder-mirror-6939.test.ts— swapping only those three spellings makes both entries parse).Why this is a ruling and not a repair
Three candidate resolutions, none of them local:
onChangeemits back to the host, but forks from@objectstack/spec'sVIEW_FILTER_OPERATORS— which the component itself imports and folds through (normalizeFilterOperator,filterValueArity).foldFilterGroupToSpecRulesreads.normalizeFilterOperatormapseq,gt,notIn,greaterThanand the canonical spellings onto one answer. Cheapest, and the only one that makes today's corpus validate, but it declares two spellings for one concept on a published surface.Note that the component ALREADY resolves both dialects everywhere the operator's MEANING matters (
filterValueArity,reconcileOperatorForField); the one place it does not is the Select's identity comparison, which is what produces the blank trigger.Reproduce
The two
the residual refusal is the operator alias and NOTHING elseassertions are the measurement.Related
safeValidateSchemabecause the Zod mirror names a key its renderer never reads (8 components) #6939 — the parent card; itsfilter-builderrow covers the field key, the type vocabulary and the group shape, and does not reach thisGenerated by Claude Code