Surfaced by the os-dev seat delivering objectui#6939 (PR #9558) as its fence-line-2 observation,
and re-measured in this seat before filing — ⛔ not adopted from the report.
objectui origin/main + installed @objectstack/spec@17.4.0, 2026-09-15T03:50Z.
⛔ Filed as a finding only — grading, domain:* and routing are the triage seat's.
The defect
A user picks an operator from the filter-builder dropdown; the resulting filter is refused by the
mirror that validates it. objectui#6939 fixed the three catalog fixtures that had the same
problem; this is the same defect on the path a real user takes, and no card owns it.
Measured
defaultOperators in packages/components/src/custom/filter-builder.tsx offers 22 ids.
FilterOperatorSchema in packages/types/src/zod/complex.zod.ts declares 14 members.
|
count |
ids |
| offered by the dropdown |
22 |
equals notEquals contains containsCaseInsensitive notContains isEmpty isNotEmpty greaterThan lessThan greaterOrEqual lessOrEqual before after between in notIn startsWith endsWith isNull isNotNull exists notExists |
| accepted as authored by the mirror |
3 |
equals contains in |
| refused as authored |
19 |
the rest |
Folding every offered id through the spec's own normalizeFilterOperator and testing the result
against the declared set:
|
count |
detail |
| fold onto a declared member |
14 |
e.g. lessThan → less_than — a pure spelling difference |
| do not fold |
8 |
splits again, and the split is the whole decision |
The 8 that do not fold are two different problems:
- 2 fold to a name the enum does not declare:
isEmpty → is_empty, isNotEmpty → is_not_empty.
The spec's alias table knows these names; this mirror's enum does not list them.
- 6 fold to themselves:
containsCaseInsensitive, before, after, between, exists,
notExists. ⭐ normalizeFilterOperator returns its input unchanged for an id it does not know,
so "folds to itself" is exactly "the protocol has no member for this".
Controls in the same run: normalizeFilterOperator('lessThan') → less_than (fires);
normalizeFilterOperator('qqzzAbsent') → qqzzAbsent (the identity behaviour that makes the
6-row reading meaningful); declared set size 14.
⚠️ My first instrument returned 0 dropdown ids — a regex that assumed single quotes where the
file uses double. ⛔ A zero from a broken parser is not a reading; it was thrown away and the block
was re-read by offset. The numbers above come from the second instrument, with both controls firing.
Why a spelling fix is not the whole answer
The tempting repair — fold at the storage boundary — closes 14 of the 19. It leaves 8 ids a
user can pick from a dropdown and cannot store, and those 8 are not one group:
isEmpty / isNotEmpty need the mirror to declare two names the protocol already publishes.
- The other six need the protocol to gain a member, or the dropdown to stop offering them.
⇒ under the maintainer's standing principle 「以 objectstack 协议为准…协议不正确的应该先修改协议」,
group 2 is a spec card first, ⛔ not an objectui change.
⚠️ filter-builder.tsx already records part of this: OPT_IN_OPERATORS holds
containsCaseInsensitive, exists and notExists, with an upstream blocker named for the last two
— so three of the six are known-unsupported by design, and the card that rules this should start
from that comment rather than re-deriving it.
Already pinned, ⛔ so this is a recorded decision rather than a silence
PR #9558 pins the refusal in both
examples/schema-catalog/test/filter-builder-operator-vocabulary-6939.test.tsx and
packages/types/src/__tests__/filter-builder-mirror-6939.test.ts. ⇒ whichever way this is ruled,
the pin names the key that moves and a repair cannot land silently.
Siblings — the same shape, one key over
⭐ Three cards now say the same thing about three different keys: objectui's UI vocabularies and
the protocol's declared sets are drifting apart in more than one place, and each was found
separately. Whoever grades these may want to ask whether the right artefact is a per-key ruling or
one gate that compares a component's offered vocabulary against the protocol's declared set.
Dedup: searched before filing; #8961 / #9554 / #6939 are the nearest and none is about the
filter-builder's operator dropdown.
Generated by Claude Code
Surfaced by the
os-devseat delivering objectui#6939 (PR #9558) as its fence-line-2 observation,and re-measured in this seat before filing — ⛔ not adopted from the report.
objectui
origin/main+ installed@objectstack/spec@17.4.0,2026-09-15T03:50Z.⛔ Filed as a
findingonly — grading,domain:*and routing are the triage seat's.The defect
A user picks an operator from the filter-builder dropdown; the resulting filter is refused by the
mirror that validates it. objectui#6939 fixed the three catalog fixtures that had the same
problem; this is the same defect on the path a real user takes, and no card owns it.
Measured
defaultOperatorsinpackages/components/src/custom/filter-builder.tsxoffers 22 ids.FilterOperatorSchemainpackages/types/src/zod/complex.zod.tsdeclares 14 members.equalsnotEqualscontainscontainsCaseInsensitivenotContainsisEmptyisNotEmptygreaterThanlessThangreaterOrEquallessOrEqualbeforeafterbetweeninnotInstartsWithendsWithisNullisNotNullexistsnotExistsequalscontainsinFolding every offered id through the spec's own
normalizeFilterOperatorand testing the resultagainst the declared set:
lessThan→less_than— a pure spelling differenceThe 8 that do not fold are two different problems:
isEmpty→is_empty,isNotEmpty→is_not_empty.The spec's alias table knows these names; this mirror's enum does not list them.
containsCaseInsensitive,before,after,between,exists,notExists. ⭐normalizeFilterOperatorreturns its input unchanged for an id it does not know,so "folds to itself" is exactly "the protocol has no member for this".
Controls in the same run:
normalizeFilterOperator('lessThan')→less_than(fires);normalizeFilterOperator('qqzzAbsent')→qqzzAbsent(the identity behaviour that makes the6-row reading meaningful); declared set size 14.
file uses double. ⛔ A zero from a broken parser is not a reading; it was thrown away and the block
was re-read by offset. The numbers above come from the second instrument, with both controls firing.
Why a spelling fix is not the whole answer
The tempting repair — fold at the storage boundary — closes 14 of the 19. It leaves 8 ids a
user can pick from a dropdown and cannot store, and those 8 are not one group:
isEmpty/isNotEmptyneed the mirror to declare two names the protocol already publishes.⇒ under the maintainer's standing principle 「以 objectstack 协议为准…协议不正确的应该先修改协议」,
group 2 is a spec card first, ⛔ not an objectui change.
filter-builder.tsxalready records part of this:OPT_IN_OPERATORSholdscontainsCaseInsensitive,existsandnotExists, with an upstream blocker named for the last two— so three of the six are known-unsupported by design, and the card that rules this should start
from that comment rather than re-deriving it.
Already pinned, ⛔ so this is a recorded decision rather than a silence
PR #9558 pins the refusal in both
examples/schema-catalog/test/filter-builder-operator-vocabulary-6939.test.tsxandpackages/types/src/__tests__/filter-builder-mirror-6939.test.ts. ⇒ whichever way this is ruled,the pin names the key that moves and a repair cannot land silently.
Siblings — the same shape, one key over
needs-user-decision) — the sort vocabulary: three$orderbyshapes, ofwhich the protocol declares one. Same family, same question, already in the decision inbox.
object-gridsends$orderbyas an object array the protocol does not declare.⭐ Three cards now say the same thing about three different keys: objectui's UI vocabularies and
the protocol's declared sets are drifting apart in more than one place, and each was found
separately. Whoever grades these may want to ask whether the right artefact is a per-key ruling or
one gate that compares a component's offered vocabulary against the protocol's declared set.
Dedup: searched before filing; #8961 / #9554 / #6939 are the nearest and none is about the
filter-builder's operator dropdown.
Generated by Claude Code