Skip to content

fix(components): publish element:record_picker.filter, the A-class key #3808's triage dropped (#3830) - #4112

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3830-record-picker-filter-input
Aug 10, 2026
Merged

fix(components): publish element:record_picker.filter, the A-class key #3808's triage dropped (#3830)#4112
yinlianghui merged 1 commit into
mainfrom
claude/issue-3830-record-picker-filter-input

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3830

filter appears in objectui#3808's raw key dump for element:record_picker and then in none of its A / B / C lists, so PR #3841 — the change that added the repo-wide parity gate — exempted it by name instead of declaring it. This declares it. It is the fourth gap of exactly the shape that PR fixed four times over: the spec declares ElementRecordPickerProps.filter, the renderer has read it all along, and the registry inputs never mentioned it.

Premise re-checked on current origin/main (877385a76)

All three legs still hold; two line numbers have drifted since the card was written, and one read expression has been rewritten:

card says today verdict
read at record-picker.tsx:78, ds.filter ?? props.filter :105, composed?.filter ?? props.filter holds — the useElementDataSource rework (objectstack#6953) renamed the binding half; the precedence is unchanged
into query.$filter at :103 :129, plus the filterKey memo at :116 holds
inputs at :180-186 omits it :224-230 omits it holds
exemption entry in the parity gate present, citing this issue holds — removed here

What landed

1. The declaration (packages/components/src/renderers/basic/record-picker.tsx). The description is derived from what the renderer does, per PR #3841's precedent, because the one thing an author cannot read off the spec is which of the two places they may write a filter wins:

PRECEDENCE: a node-level dataSource binding wins outright. The renderer reads dataSource.filter ?? filter, so when the binding — or the saved view its view names, which AND-combine with each other because the spec calls the binding's filter additional — supplies a filter, THIS key is dropped entirely rather than merged into it.

2. type: 'object', not the sketch's 'array'. Taken from the spec's actual shape on the resolved pin (@objectstack/spec@17.0.0-rc.5): filter is FilterConditionSchema, which is z.record(z.string(), z.unknown()) intersected with the $and / $or / $not group — an object. Measured, not read:

object cond  {status:"open"}                     => success: true
object $and  {$and:[{a:1}]}                      => success: true
array AST    [["a","=",1]]                       => false  expected record, received array
array rules  [{field:"a",operator:"eq",value:1}] => false  expected record, received array
string / number / boolean                        => false

No ComponentInput compromise was needed, and that is itself the reportable data point for #3832. checkType's 'object' arm in sdui-parser/src/validate.ts accepts a non-null non-array object and warns type-mismatch on everything else — the same partition safeParse draws above. So unlike element:text_input.defaultValue (spec string | number, published as 'string', narrowing disclosed in prose), this key has no narrowing to disclose. #3832's union problem is real and untouched here; this key is simply the case where the coarse type costs nothing. The card's own warning about z.unknown()-style wide spec types did not materialise — worth knowing when #3832 is scoped, since it means the union limit is not uniform across the family.

3. The exemption is deleted from apps/console/src/__tests__/registry-inputs-spec-parity.test.ts, as that file's own carries no stale unpublished-key exemption assertion demands, and the key joins #3808's four in the by-name "declared, not merely not-failing" pin (now five).

4. A per-block parity test, packages/components/src/__tests__/record-picker-inputs-spec-parity.test.ts, following text-input-inputs-spec-parity.test.ts exactly — plus the one thing that precedent argued in prose but never executed: the JSX-page path, end to end.

The JSX-page path, actually exercised

element:record_picker is not in PUBLIC_BLOCKS ("record picking is a field widget, not a page block"), so the gap was never in sdui.manifest.json. It was in the JSX-page compiler's prop whitelist, which renderers/layout/page.tsx builds from getKnownTypes() plus these same inputs. The new test assembles the manifest that way from the live registration and compiles a page that writes filter.

It carries its own non-vacuity control in the same compile() call: searchFields — a spec key this block deliberately does not publish (an ADR-0087 tombstone upstream) — must still come back as unknown-prop. Without that control, "no unknown-prop for filter" is also what a broken manifest, an unregistered tag or a silent parse failure looks like.

Reverse verification

Declaration reverted, exemption removal kept — the direction the card predicts:

× element:record_picker publishes every top-level key its spec props schema declares
    AssertionError: expected [ 'filter' ] to deeply equal []
× the five A-class keys objectui#3808 / #3830 declared are discoverable, block by block
    AssertionError: element:record_picker does not publish filter
× a JSX page writing `filter` no longer gets `unknown-prop` from the compiler
    Received: 'has no prop "filter" | has no prop "searchFields"'
      (the control still fires, so the probe is not silently vacuous)

Tests  7 failed | 50 passed (57)      -> restored:  57 passed

The third line is the harm the card describes, reproduced verbatim: the compiler warning an author off filter while the renderer filters the entire candidate set by it.

Verification

vitest packages/components/ + registry-inputs-spec-parity
     + public-block-binding-reach + record-block-record-reach + public-contract
                                       -> 116 files, 1111 tests passed
vitest packages/sdui-parser/ + RefComponentWidget ->   5 files,   24 tests passed
type-check @object-ui/components + @object-ui/console -> Done
eslint (3 touched files)  -> 0 errors (12 pre-existing warnings, untouched effect body)
check-control-bytes -> OK (3966 tracked text files)
check-changeset-presence / -no-major / -fixed -> OK

type-check needed pnpm --filter '@object-ui/console^...' build first; the initial run's TS2882 Cannot find module or type declarations errors were stale artifacts, not a regression.

Changeset: @object-ui/components patch — the published authoring surface gains a declared input, the same argument class PR #3841 used.

Out of scope, deliberately

The #3832 observation above is reported as a data point on the existing card, not acted on. No new issue filed: nothing new was found that is not already owned by #3832 (ComponentInput union typing) or #3809 (tombstone recognition).


Generated by Claude Code

…key #3808's triage dropped (#3830)

`filter` appears in objectui#3808's raw key dump for this block and then in none
of its A / B / C lists, so the change that added the repo-wide parity gate
exempted it by name instead of declaring it. It is the fourth gap of exactly the
same shape as the four that PR #3841 fixed: `@objectstack/spec` declares
`ElementRecordPickerProps.filter`, the renderer has read it all along
(`composed?.filter ?? props.filter` -> `query.$filter`), and the registry
`inputs` never mentioned it.

`element:record_picker` is not in `PUBLIC_BLOCKS` ("record picking is a field
widget, not a page block"), so the gap was not in `sdui.manifest.json` — it was
in the JSX-page compiler's prop whitelist, which `renderers/layout/page.tsx`
builds from `getKnownTypes()` plus these same `inputs`. Verified end to end
rather than argued: with the declaration reverted, `compile()` over the live
registration returns `<element:record_picker> has no prop "filter"` on a key the
renderer then filters the entire candidate set by.

The description is derived from what the renderer DOES, because the one thing an
author cannot read off the spec is which of the two places they may write a
filter wins: a node-level `dataSource` filter (itself AND-combined with any saved
`view` it names) is taken and this top-level `filter` is DROPPED, not merged.

`type: 'object'`, from the spec's actual shape on the resolved pin and not the
`'array'` the issue's landing sketch guessed. `filter` is
`FilterConditionSchema`, i.e. `z.record(z.string(), z.unknown())` intersected
with the `$and`/`$or`/`$not` group, so a rule array is rejected — measured with
`safeParse`, and `sdui-parser`'s `checkType` object arm draws exactly the same
partition. This is the one key in the family where `ComponentInput`'s coarse
typing costs nothing, so unlike `element:text_input.defaultValue` there is no
narrowing to disclose in the description (data point for objectui#3832, not a
widening of this card).

The gate's explicit exemption for this key is deleted in the same change, as its
own `carries no stale unpublished-key exemption` assertion demands, and the key
joins #3808's four in the by-name "declared, not merely not-failing" pin — now
five.

Reverse verification (declaration reverted, exemption removal kept):

  element:record_picker publishes every top-level key its spec props schema
  declares -> AssertionError: expected [ 'filter' ] to deeply equal []
  the five A-class keys objectui#3808 / #3830 declared -> does not publish filter
  a JSX page writing `filter` -> `has no prop "filter"` (control: `searchFields`
  still reported, so the probe is not silently vacuous)
  7 failed | 50 passed, restored to 57 passed.

Verified:

  vitest packages/components/ + 4 console gates -> 116 files, 1111 tests passed
  vitest packages/sdui-parser/ + RefComponentWidget -> 5 files, 24 passed
  type-check @object-ui/components + @object-ui/console -> Done (after building
    the console dependency closure; the first run's TS2882s were stale artifacts)
  eslint on the three touched files -> 0 errors (12 pre-existing warnings in the
    untouched effect body)
  check-control-bytes / check-changeset-presence / -no-major / -fixed -> OK

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 10, 2026 10:46am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.3 KB 350 KB
Entry file index-CcETve-r.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.66KB 3.13KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
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) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 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.65KB 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) 485.06KB 107.21KB
core (index.js) 3.04KB 1.15KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 140.66KB 36.25KB
fields (index.js) 229.40KB 56.93KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.87KB 10.80KB
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.74KB
mobile (index.js) 1.50KB 0.62KB
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.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 45.23KB 12.45KB
plugin-charts (index.js) 61.49KB 17.48KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 118.50KB 30.66KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 237.80KB 59.48KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 162.81KB 39.67KB
plugin-grid (index.js) 188.04KB 49.91KB
plugin-kanban (index.js) 48.60KB 13.41KB
plugin-list (index.js) 110.04KB 26.67KB
plugin-map (index.js) 17.00KB 5.32KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 26.21KB 7.52KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
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.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.71KB 7.95KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.71KB 1.34KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
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 (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 10, 2026 10:57
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit bfdf3d4 Aug 10, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3830-record-picker-filter-input branch August 10, 2026 10:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants