…ry its renderer reads
`FilterFieldSchema` required `name`, an entry key with zero read sites, and its
`type` enum was `string | number | date | boolean | select` — `string` a
phantom, `text` / `datetime` / `time` all refused. `FilterGroupSchema` was
`{ operator, conditions }` where the component's `isValidGroup` gate reads
`conditions` and `logic`. All five `components-complex-filter-builder/*`
catalog entries author the spellings the renderer reads, so the mirror refused
documents it draws correctly (objectui#6939, maintainer ruling 2026-09-02).
Both faces move together: `FilterField.value`, the six `FilterValueFamily`
members plus `select`, and `FilterGroup { id?, logic, conditions }`.
Two declared departures from a literal reading of the ruling, both measured:
`select` is retained (it has its own operator bucket and draws the
option-driven Select, so dropping it would refuse a live spelling), and the
group's `id` is optional (zero read sites; requiring it would invent a
refusal). A fourth divergence the ruling does not cover — the
`conditions[].operator` alias fork — is reported rather than repaired, and the
residual refusal it leaves on two catalog entries is pinned rather than
described.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Part of #6939 — the
filter-buildergroup (group 4 of eight; the card's own instruction is that the groups land as separate PRs).Clause-②: yes
This changes what a published mirror ACCEPTS, in both directions. Draft,
needs:contract-review, no auto-merge, no self-review.Authority
Maintainer ruling recorded 2026-09-02, director seat summon #8, verbatim 「同意」 — #6939 comment 5510084784. Its
filter-builderrow:What was wrong, re-measured on
origin/main3e01cb55fvalueFilterFieldSchemarequiresnamefields.find((f) => f.value === …)ingetOperatorsForField/changeField/getInputType/renderValueInput;fields[0]?.valueinaddCondition;SelectItem value={field.value}in the field dropdown.name: zero read sitestext/number/boolean/date/datetime/timez.enum(['string','number','date','boolean','select'])FilterValueFamily—custom/filter-builder.tsx:406— folded from a column'stypebyvalueFamilyForFieldType, drawn byFILTER_INPUT_TYPE_BY_FAMILY{ id, logic, conditions }FilterGroupSchemais{ operator, conditions }isValidGroup,custom/filter-builder.tsx:1060—Array.isArray(v.conditions) && (v.logic === "and" || v.logic === "or")All five
components-complex-filter-builder/*entries author the spellings the renderer reads. Four refuse at the root (objectui checkrunssafeValidateSchemaon the ROOT only —packages/cli/src/commands/check.ts:137— sosearch-interface, which roots atstack, is not one of the four, while thefilter-builderit wraps refuses on its own).The presupposition check — what did NOT hold
The card asked for this explicitly, and two of the row's presuppositions failed.
1. The six-member "type switch" is the value-FAMILY switch, not the field-
typevocabulary. The card read the cases atcustom/filter-builder.tsx:532-573; that switch isconvertScalarToFamily(value, family), overFilterValueFamily. The live field-typevocabulary is wider —numberLikeTypes = ["number","currency","percent","rating"],dateLikeTypes,selectLikeTypes = ["select","status"],lookupLikeTypes = ["lookup","master_detail","user"],boolean, and anything unrecognised falling to the text bucket.Measured, one condition row per spelling, through the real
SchemaRenderer:typetext/string/ any unrecognised wordinput type="text"number/currency/percent/ratinginput type="number"date/datetime/timeinput type="date"/"datetime-local"/"time"boolean/select/status/lookup/master_detail/userinputat all — an option-driven Select⇒
stringis a phantom (indistinguishable from a nonsense spelling) and is dropped. ⇒selectis RETAINED against a literal reading of the ruling. The ruling inherits the finding card's description ofselectas "extra"; it is not — it has its own operator bucket and its own control, and this mirror accepts it today. Dropping it would refuse a live spelling, which is a fresh instance of the class #6939 exists to close. Flagged here rather than decided.2. The group's
idhas ZERO read sites.isValidGroupgates onconditionsandlogiconly; nothing readsfilterGroup.id. Measured: deletingidfrom an authored group renders byte-identically. Soidis declared but optional — requiring it would invent a refusal the renderer does not make. It stays declared (rather than dropped) because a plainz.objectstrips unknown keys in silence, so an undeclaredidwould be admitted unvalidated; declared,id: 42now refuses.A third declaration agrees with the renderer and against the mirror.
content/docs/components/complex/filter-builder.mdxhas all along publishedvalue: string,logic: 'and' | 'or', and a fourteen-member optionaltype?.@object-ui/fields'deriveFilterFields— the real producer that buildsfieldsfrom an object schema — emits{ value, label, type?, options?, referenceTo? }and passes any object field type through except thirteen non-filterable ones. Pinned, so the claim is a reading.What this does NOT reach — declared, not left as an absence
product-search,with-conditionsandsearch-interface's nested node authorconditions[].operatoraseq/gt/lt, whileFilterOperatorSchemais the spec's canonicalequals/greater_than/less_than. 2 of the 4 census entries validate after this change; the other 2 do not. That is pinned mechanically rather than described — swapping only those three spellings makes both parse, so "the three ruled divergences are gone from all four" is measured. It is a genuine fork needing its own ruling: the builder's dropdown ids arenotEquals/greaterThan, which this mirror also refuses, while the canonical spellings it accepts render a blank operator trigger (visible inPRE_REPAIR—…CategoryRemove condition…with no operator text).status,currency,percent,rating,lookup,master_detail,user. Each draws a real control; each was refused before this change as well, so this is a pre-existing gap, not a regression introduced here. Widening to them is an accept-set change the ruling does not cover.typestays required, though the renderer and the published doc both treat it as optional. Same reason.Each of the three is pinned with an assertion, so the gap is a recorded decision rather than an absence, and each is filed rather than left in prose:
typethe renderer and the docs both make optional, and its condition shape omits theidfour read sites match on #7562 — the seven live field types, thetypeoptionality, and the condition-levelidthe mirror omits.The pin — both halves
Validator —
packages/types/src/__tests__/filter-builder-mirror-6939.test.ts(39 assertions): the ruled spellings accept, the old ones refuse, the residual operator refusal is isolated, the unruled gaps are recorded, and the published doc is read off disk.Render —
examples/schema-catalog/test/filter-builder-mirror-6939.test.tsx(43 assertions): all five tiles measured throughSchemaRendererdirectly, never through a parse-then-render path — at base four of the five fail validation, so a validating path has no "before" to measure.PRE_REPAIRwas taken onorigin/main3e01cb55fwith both faces untouched: element count, tag census,textContent, and its SHA-256.empty-filter-builder4f7116e4…product-search708dab4e…search-interface31b9cb48…user-filtersbc346672…with-conditions23cdc026…All five identical after. Plus the #6318 discriminators, re-measured rather than quoted:
value→name: the field is lost —…Clear allCategoryRemove condition…becomes…Clear allRemove condition…, every field trigger goes blank, and a non-text column additionally degrades to a text box (search-interface: 65 → 62).{id,logic,…}→{operator,…}: the board empties — 76 elements and three rows become 11 and none.text→string: identical — the phantom.iddeleted: identical — the zero-read-site measurement.Verification
pnpm exec vitest run packages/types/ examples/schema-catalog/test/filter-builder-mirror-6939.test.tsx3c54edb28pnpm --filter @object-ui/types --filter @object-ui/components --filter @object-ui/react --filter @object-ui/example-schema-catalog run type-checkDone, at3c54edb28tsc -p packages/types/tsconfig.test.json --listFileszod-mirror-parity.test.tsand the new pin are both IN the program — the parity ledger's compile-timeassertionDriftMatchesLedgeris a reading, not a not-runpnpm --filter @object-ui/types --filter @object-ui/example-schema-catalog run lintDone, 0 errors (268 pre-existing warnings in types)check:control-bytes,check:changeset-*,check:doc-types,check:doc-snippets,check:doc-fences,check:designer-field-key-parity,check:spec-symbols,check:vi-mock-inherit,check:vi-mock-specifiers,check:published-tsconfig-exclude,check:readme-exports,check:self-import,check:phantom-deps,check:dist-completenesscheck:doc-snippetsandcheck:readme-exportsonly after building what they need — both werePRECONDITION NOT METon an unbuilt closure first)The parity ledger did not move.
FilterFieldSchema'sKnownDriftentry stays'operators'andFilterBuilderSchema's stays'fields' \| 'onChange', because both faces moved in lockstep;FilterGroupSchema/FilterBuilderConditionSchemaareEXCLUSIONS(recursivez.ZodType<any>, no.shape). No count in that file is touched, so #7433's header defect is not compounded here.Reverse verification — prediction written before the run.
5c5c9aa3…≠ HEADb0503d08…): validator pin 22 failed / 17 passed. Predicted 23; the one that stayed green is the assertion that the formernamespelling is REFUSED, and the reason is worth recording — under the old enum{ name, label, type: 'text' }refuses for the wrong reason (textis not in['string','number','date','boolean','select']), which is why that assertion cannot carry the claim on its own.41f6a22d…≠ HEAD68d79a4a…):tsc -p packages/types/tsconfig.test.json→ 6 errors, 4 on the type pins and 2 onzod-mirror-parity.test.ts— the ledger reddens the moment the faces stop moving together.git diff HEADempty AND the worktree blob equal to the HEAD blob, for both files.nameandvalue,operatorandlogic, andtype: 'select'(a member of both enums) — so a red run cannot be misread as unrelated breakage.Declared narrowings.
check:sdui-registration-pinswas NOT run:PRECONDITION NOT MET— it needs anapps/consoleproduction build. What it could have caught that this diff introduces: a change in registration weight or eager closure. This diff registers nothing and edits no registration; the onlyfilter-builderregistration is untouched.pnpm lintwas replaced by the two affected packages' ownlint(which is exactly whatturbo run lintinvokes for them). ESLint here is not type-aware, so this diff cannot move a verdict on a file it does not touch.@object-ui/types,@object-ui/components,@object-ui/reactand@object-ui/example-schema-catalogwere type-checked. The renamed keys have no other in-repo consumer: everyFilterGroupin this repo imports@object-ui/components' own interface, and the sole@object-ui/typesimport ofFilterBuilderSchemaisrenderers/complex/filter-builder.tsx.CI runs the full farm.
Changeset
@object-ui/types:minor, not thepatchthe ruling grades this class. Measured, the accept set does not only widen — three refusal classes are created, each a document that validated before:fields[].nameas the field key;type: 'string'; and{ operator, conditions }as the group shape.majoris forbidden here (check-changeset-no-major), so a breaking change isminor. Every refusal class is named in the changeset, with its migration (a document carrying both key spellings validates in either state).🤖 Generated with Claude Code
https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Generated by Claude Code