Skip to content

finding(components/types): the filter-builder dropdown offers 22 operator ids and the mirror declares 14 — 19 are refused as authored, and 8 of those cannot be reached by normalisation at all #9559

Description

@claude

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. lessThanless_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: isEmptyis_empty, isNotEmptyis_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:

  1. isEmpty / isNotEmpty need the mirror to declare two names the protocol already publishes.
  2. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions