Skip to content

feat(types): FilterFieldSchema.type — the published doc's fourteen members, optional - #8766

Merged
os-bill merged 2 commits into
mainfrom
claude/issue-7562-filter-builder-fourteen-type-members
Sep 9, 2026
Merged

feat(types): FilterFieldSchema.type — the published doc's fourteen members, optional#8766
os-bill merged 2 commits into
mainfrom
claude/issue-7562-filter-builder-fourteen-type-members

Conversation

@os-bill

@os-bill os-bill commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Fixes #7562

DRAFT, deliberately — do not flip ready, do not enqueue, do not enable auto-merge.
Clause-②: yes (this widens the accept set of a published authoring surface), so under
the rule that landed 2026-09-08T02:17:47Z it is BUILT at this tier and REVIEWED at
CONTRACT_REVIEW_TIER. That ceiling review has not happened yet. needs:contract-review
is on the PR.

The ruling this executes

objectui#7562, director seat, decision batch #88, 2026-09-08 (comment 5582379508):
of the three declarations of the filter-builder authoring surface — the published doc,
the component, and the zod mirror — the published doc is the authority. Items 1 and 2
land together: FilterFieldSchema.type widens to the doc's fourteen members and becomes
optional (text when absent, as the renderer already reads it).

The ruling carried a precondition measured before the mirror moved: every one of the
fourteen has a renderer branch. A member with none would have come OUT of the doc in this
same PR. All fourteen have one, so the doc is untouched by this change.

The measurement leg (the precondition)

Measured on e3fb3b6 with a throwaway harness that drives one condition row per member
through the real FilterBuilder (not a parse-then-render path) and reads three
things: the value control, the number of comboboxes on the row, and the operator bucket
operatorsForFieldType answers with. Three controls were probed alongside — a nonsense
spelling, string, and an absent type — because "it draws something" is only a finding
against something that draws the fallthrough. Read input=[...] as the type attribute
of each input element on the row; combos counts role="combobox".

# member bucket, custom/filter-builder.tsx control drawn operator bucket vs. the nonsense control
1 text named at :408 (fieldType || "text"), :405, :431 input=["text"], combos=2, 35 els text ⚠️ identical — see below
2 number numberLikeTypes:931 input=["number"], combos=2 numeric distinct
3 currency numberLikeTypes:931 input=["number"], combos=2 numeric distinct
4 percent numberLikeTypes:931 input=["number"], combos=2 numeric distinct
5 rating numberLikeTypes:931 input=["number"], combos=2 numeric distinct
6 date :411, dateLikeTypes:933 input=["date"], combos=2 date distinct
7 datetime :412, dateLikeTypes:933 input=["datetime-local"], combos=2 date distinct
8 time :413, dateLikeTypes:933 input=["time"], combos=2 date distinct
9 boolean :410 no input, combos=3, 38 els equals/notEquals only distinct
10 select selectLikeTypes:935 no input, combos=3, 38 els select (in/notIn) distinct
11 status selectLikeTypes:935 no input, combos=3, 38 els select (in/notIn) distinct
12 lookup lookupLikeTypes:947 no input, combos=3; with referenceTo and no options, the remote picker (:1277) lookup (in/notIn) distinct
13 master_detail lookupLikeTypes:947 same as lookup lookup (in/notIn) distinct
14 user lookupLikeTypes:947 same, and it reaches the remote picker with no referenceTo (:1277 names it) lookup (in/notIn) distinct
CONTROL nonsense none input=["text"], combos=2, 35 els text
CONTROL string none input=["text"], combos=2, 35 els text identical
CONTROL absent :408 supplies text input=["text"], combos=2, 35 els text identical

Verdict: fourteen of fourteen have a renderer branch. Nothing removed from the doc.

⚠️ text is the one member the ruling's DOM criterion cannot decide, and why it is not a fork

text is the unrecognised-word fallthrough target, so it measures byte-identical to a
nonsense spelling and to an absent type — same 35 elements, same control, same operator
bucket, all three. That is exactly the reading that made string a phantom in
objectui#6939. I checked whether this is the Zone-1 fork condition and concluded it is
not, on two independent grounds that point the same way:

  1. The ruling decides it internally. Item 2's content is «text when absent, as the
    renderer already reads it» — the ruling asserts the renderer reads text. Item 2
    cannot land while text is deleted, and items 1 and 2 "land together".
  2. Removing it is option B on one member. text is accepted by the mirror today,
    is what the registration's defaultProps and all five catalog entries author, and
    renders correctly. Withdrawing it is the published-promise retraction the ruling
    refused.

The criterion that survives both is the one the mirror's own docblock already used to
separate string from text: is the spelling NAMED in the renderer? text is, at
three sites (:408 where an absent type acquires it, the FilterValueFamily union at
:405, the FILTER_INPUT_TYPE_BY_FAMILY key at :431). string is named at none. Both
facts are now pinned in a test rather than left as prose.

Reverse verification — both legs, from the committed state

Each leg: mutate → prove the bytes moved on disk (blob hash before/after, anchor
occurrence count) → run → restore via git checkout HEAD -- path → prove git diff HEAD
is empty and the blob is back. trap ... EXIT INT TERM on absolute paths throughout.

  • Leg A — the type pin is live. Dropped 'currency' from the expectType union in
    the mirror test. Blob 0be5ad6743628b, anchor 1 → 0.
    pnpm --filter @object-ui/types type-checkexit 2,
    filter-builder-mirror-6939.test.ts(149,12): error TS2344: Type 'false' does not satisfy the constraint 'true'.tsconfig.test.json really compiles this file, so the
    compile-time half of the accept set is measured and not merely written down.
  • Leg B — the parity ledger measures type. Dropped 'user' from the zod enum
    only, leaving the TS union at fourteen. Blob 489fe69443504e, anchor 1 → 0.
    Same command → exit 2,
    zod-mirror-parity.test.ts(2746,14): error TS2322: Type '"complex.zod.ts#FilterFieldSchema"' is not assignable to type 'never'.
  • Restore leg: unmutated tree, same command → exit 0, git status clean.

Leg B is also the answer to a premise this card was dispatched with. The dispatch expected
the widening to move a FilterFieldSchema row in the parity ledger. It did not, and the
ledger file is not in this diff.
Leg B shows why: the ledger reddens the moment the two
faces disagree, and this change moved both faces symmetrically. The ledger's three existing
FilterFieldSchema rows are all about operators, a pre-existing drift this card does not
touch.

What is still refused, on purpose

  • string — named nowhere in the renderer, reaches the text control only by the
    fallthrough. The phantom objectui#6939 removed; the published doc does not offer it
    either, so the two faces agree.
  • Any unrecognised spelling. The vocabulary stays closed. { value, label, type: 'zzz' }
    is still refused, pinned in the same test as the optional-type accept, so this cannot
    be read as "type stopped being checked".

Out of scope, and left alone

  • FilterBuilderConditionSchema.id — objectui#8415, split out at comment 5575381230.
  • The filter operator vocabulary — objectui#7561.
  • The group's id — stays optional. objectui#7560 measured zero read sites for it and
    the condition-id answer does not transfer.
  • packages/components/src/custom/filter-builder.tsx — the measurement subject,
    read-only for this card and not in the diff.

Verification

Commands and their own printed verdicts, all at final HEAD 0b92457 (after the
origin/main merge, which pulled in a sibling's packages/types changes and so invalidated
the pre-merge readings):

command exit
pnpm --filter @object-ui/types type-check (tsc --noEmit + examples + test projects) 0
pnpm exec vitest run packages/types/Test Files 154 passed (154), Tests 3065 passed (3065) 0
pnpm exec eslint . in packages/types224 files, 0 errors, 266 warnings 0
pnpm check:control-bytes 0
pnpm check:doc-types 0
pnpm check:spec-symbols 0
pnpm check:handler-key-reads 0
node scripts/check-changeset-presence.mjs 0

Run pre-merge and unaffected by the merge (none of them reads a file the merge moved):
pnpm exec turbo run build $(node scripts/check-doc-snippet-types.mjs --build-filter) --concurrency=2
(35/35), then check:doc-examples · check:doc-snippets · check:readme-exports ·
check:dist-completeness · changeset:check — all 0. check:doc-fences ·
check:designer-field-key-parity · check:shell-escape-residue — all 0. The render
half plus the retired-field-type pin — pnpm exec vitest run examples/schema-catalog/ packages/components/src/__tests__/filter-builder-retired-field-type.test.tsx
32 files / 2160 tests passed, exit 0.

Lint narrowing, declared. turbo run lint covers 40 packages; this ran the one package
the diff touches. The population is eslint's own — 224 files, counted from the length of
its --format json output, not estimated. The narrowing is sound because type-aware
linting is off in this repo: eslint.config.js extends tseslint.configs.recommended
(not recommendedTypeChecked) and sets no parserOptions.project / projectService, so
every file is judged from its own AST alone and this diff cannot move the verdict on a file
it does not touch. CI runs the full farm.

Acceptance notes

  • The doc's line "Lookup-like fields without options render a remote-search picker" is
    true but under-specified: measured, lookup and master_detail also need referenceTo
    (only user defaults its own). The doc lists referenceTo in the very next lines, so
    this is a reading nit, not a false promise — noted, not filed.
  • scripts/measure-strict-authoring-face.mjs quotes FilterFieldSchema's enum
    illustratively with an elision; still accurate after this change and the script is not
    wired into any workflow — noted, not filed.
  • Toolchain note for the next seat: pnpm --filter PKG exec vitest ... is refused by this
    repo's guard (objectui#3378) because it re-roots vitest at the package dir and silently
    runs another package's suite. Run vitest from the repo root.

Generated by Claude Code

…teen members

Of the three declarations of the `filter-builder` authoring surface — the
published doc, the component, and this zod mirror — the doc is the authority
(objectui#7562, director seat, decision batch #88, 2026-09-08). The component
already follows it; the mirror was the outlier on both axes, offering seven
`type` members where the doc offers fourteen and REQUIRING a key the doc marks
optional. So a `fields` entry written against our own documentation, which the
renderer draws correctly, was refused by our own validator.

The ruling carried a precondition, measured before the enum moved: every one of
the fourteen has a renderer branch, or it comes OUT of the doc instead. One
condition row per member was driven through the real `FilterBuilder` and both
the value control and the operator bucket were read. All fourteen have a branch,
so nothing was withdrawn from the doc and the mdx is untouched by this change.

`text` is the one member whose branch is by NAME rather than by a distinct
control — it IS the unrecognised-word fallthrough target, so it measures
identical to a nonsense spelling. The renderer names it at
`valueFamilyForFieldType`'s `fieldType || "text"`, which is both why it stays and
why `type` is safe to leave optional. `string`, named nowhere, stays refused.

The mirror-test pins move with the accept set: the seven
`still refuses the live-but-unruled spelling …` assertions become
`accepts …, and the renderer draws it`, each paired with the literal bucket that
carries it, and the doc-vs-mirror assertion's closing line inverts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012W3vMLTFY9SPr2LyxhSeYi
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3483.4 KB 3512.7 KB
Main entry chunk (gzip) 144.1 KB 350 KB
Entry file index-G1k4qkDf.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 499.42KB 114.32KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 198.39KB 55.29KB
fields (index.js) 244.96KB 61.76KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.53KB 46.34KB
plugin-dashboard (index.js) 131.43KB 34.44KB
plugin-designer (index.js) 215.51KB 44.29KB
plugin-detail (index.js) 252.39KB 65.32KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 134.20KB 33.48KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 210.67KB 57.19KB
plugin-kanban (index.js) 55.50KB 15.75KB
plugin-list (index.js) 112.73KB 27.69KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.54KB 20.84KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-bill
os-bill marked this pull request as ready for review September 9, 2026 05:16
@os-bill
os-bill added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit efbd566 Sep 9, 2026
35 checks passed
@os-bill
os-bill deleted the claude/issue-7562-filter-builder-fourteen-type-members branch September 9, 2026 05:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants