Skip to content

fix(plugin-grid): object-grid publishes the filter key it reads — filter, singular (#4041) - #4081

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4041-grid-filter-input-spelling
Aug 10, 2026
Merged

fix(plugin-grid): object-grid publishes the filter key it reads — filter, singular (#4041)#4081
yinlianghui merged 1 commit into
mainfrom
claude/issue-4041-grid-filter-input-spelling

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4041

object-grid published plural filters as an authoring input while ObjectGrid
reads singular schema.filter, and schema.filters had zero read points in
the renderer. Both halves failed silently, in opposite directions: an author
following the published vocabulary wrote filters: [...], the save gate accepted
it (sdui-parser/src/validate.ts walks a node's props against the block's
inputs, and filters was there), the renderer never read it, and the grid
answered with the whole table — no error at authoring time and none at runtime.
Meanwhile the spelling that actually worked, filter, was undeclared, so writing
it was reported as unknown-prop. Published word and runtime read pointed at
opposite keys on a shipped surface.

Premise re-verified on current origin/main

The card was filed against 65bb513dc; main is now 8aad9fd50. All three
anchors still hold, plus one the card did not have:

Anchor Status on 8aad9fd50
plural filters declared twice packages/plugin-grid/src/index.tsx:96 (object-grid), :113 (view:grid)
zero schema.filters read points confirmed — the only filters hits in ObjectGrid.tsx are the header comment (:18) and the unrelated lookup_filters field-prop list (:226)
list-view declares the singular packages/plugin-list/src/index.tsx:52 and :93, { name: 'filter', type: 'array', label: 'Filter' }
new: the spec's own vocabulary is singular ListViewSchema.shape carries filter (not filters); probed live against the pinned @objectstack/spec/ui

What changed

1. The declaration converges on the implementation (maintainer ruling
2026-08-10 on the card, option A). Both registrations now declare
{ name: 'filter', type: 'array', label: 'Filter' }, and they share one
GRID_QUERY_INPUTS constant so the view:grid alias cannot drift from the block
it aliases. The plural is deleted rather than also taught to the renderer: with
no read point on any ref there is no working in-the-wild usage to keep
compatible, and reading it too would harden a misspelling into a second de-facto
contract for one concept (AGENTS.md #0.1).

2. The read point lowers through toFilterNode. This is the constraint
recorded on the source thread (objectstack#7119, comment of 2026-08-09T20:26Z),
quoted verbatim as filed:

若改成把声明面重命名为单数 filter(与渲染器对齐)→ 同一处仍要过 toFilterNode,否则本单会把「声明了没人读」换成「声明了、读了、发出去被 400」—— 从静默错答变成必然失败,不是修好。

Until now the only value that could arrive at schema.filter was an ObjectQL AST
synthesized by ElementDataSourceGate, and copying that onto $filter verbatim
was correct. An author writes the spec's view vocabulary instead —
ViewFilterRule[], [{ field, operator, value }] — and that shape byte-copied
onto $filter is refused on the wire: isFilterAST is false for an array of
objects and the data API answers 400 INVALID_FILTER (measured against a real
backend in objectui#3431). toFilterNode is the repo's single lowering hop
before the wire; plugin-list's buildEffectiveFilter, plugin-view's
ObjectView and plugin-detail's RelatedList already go through it, and this
read point was the last consumer on the chain that did not.

That hazard is not merely prospective — it is reachable today.
packages/react/src/spec-bridge/bridges/list-view.ts:85 builds a node of
type: 'object-grid' and copies spec.filter straight onto node.filter. And
the spec's ListView.filter accepts ViewFilterRule[] and rejects AST
triples — probed against the pinned spec:

ViewFilterRule[] accepted by ListView.filter: true
AST triple accepted: false

So a spec ListView bridged onto this block already delivers rule objects to the
read point, and they already went out raw. The rename is what makes that path
author-reachable; the lowering is what makes it correct.

Two narrow behaviour changes ride along at that read point, both toward the
shared sink's documented contract: a MongoDB-style object filter is converted
instead of silently dropped (the old Array.isArray guard read false for it, so
the grid returned every record — the same defect buildEffectiveFilter fixed one
package over), and a declared-but-empty filter: [] skips $filter rather than
sending an empty one. The fetch and the server-side export read the same lowered
value, so the downloaded file cannot disagree with the screen.

The pin

packages/plugin-grid/src/__tests__/gridFilterInputSpelling.test.tsx — 10 cases.
Every behavioural case reads the key name out of the registration
(declaredFilterInput()) instead of spelling 'filter' itself: a test that
restated the key would keep passing through exactly the drift this card is about.

  • the declared filter input is singular, array-typed, on both tags, and the two
    tags agree with each other;
  • filters is gone from both declarations;
  • a filter written under the declared name reaches $filter (the card's pin), on
    object-grid and on view:grid;
  • spec ViewFilterRule[] arrives lowered[['stage', 'equals', 'won']],
    with a second assertion that no bare rule object survives into the outgoing
    filter, since that is the half that would rot silently;
  • an already-composed AST passes through untouched (the ElementDataSourceGate
    path must not be lowered twice);
  • an empty declared filter skips $filter;
  • historical record, as the card asked: a schema written the way the old
    declaration invited (filters: [...]) produced no $filter at all — which is
    the evidence that option A deletes a key with no users rather than a contract.

Reverse verification

Predicted directions before running, both confirmed, each taken out with
git checkout / a scratch patch (never git stash):

Removed Predicted Observed
the rename only (declaration back to filters) declaration + reach cases red 7 red, incl. expected 'filters' to be 'filter' and expected undefined to deeply equal [ [ 'stage', '=', 'won' ] ]
the toFilterNode hop only (rename kept) lowering + empty cases red 2 red: expected [ { field: 'stage', …(2) } ] to deeply equal [ [ 'stage', 'equals', 'won' ] ] and expected [] to be undefined

The second row is the interesting one: the failure message shows the raw rule
objects sitting on $filter — literally the payload the wire face answers 400
to. The two halves of this PR fail independently, so neither is carried by the
other.

Generated artifacts and adjacent gates

  • sdui.manifest.json / sdui-intrinsics.d.ts are not tracked in this repo
    packages/sdui-parser/scripts/gen-manifest.ts writes them into a build
    output dir, and scripts/dump-public-manifest.mjs writes into
    packages/console/dist/. git ls-files shows neither. Nothing to regenerate;
    the rename flows into both on the next build.
  • apps/console/src/__tests__/registry-inputs-spec-parity.test.ts does not judge
    these tags: covered is derived from ComponentPropsMap, and object-grid /
    view:grid are not entries in it (verified by probing the pinned spec).
  • public-contract.test.ts pins block tags, not input names.
  • public-block-binding-reach.test.tsx asks only whether a declared objectName
    reaches the data layer; it synthesizes a plausible value per declared input and
    still reaches.
  • packages/app-shell/.../previews/block-config.ts declares no filter field for
    object-grid at all — its filters hit is prose in a comment — so nothing is
    mechanically forced there and it is left alone.

All four console gates were run anyway rather than reasoned about (below).

Verification

pnpm --filter '@object-ui/plugin-grid^...' build          # build closure first
pnpm --filter @object-ui/plugin-grid test  -- --maxWorkers=2   → 58 files, 533 passed
pnpm --filter @object-ui/plugin-grid type-check                → Done
pnpm --filter @object-ui/plugin-grid lint                      → 0 errors (583 pre-existing warnings)
npx vitest run apps/console/src/__tests__/{public-block-binding-reach,registry-inputs-spec-parity,public-contract}.test.* --maxWorkers=2
                                                               → 3 files, 76 passed
pnpm --filter @object-ui/plugin-view --filter @object-ui/plugin-designer test -- --maxWorkers=2
                                                               → 13 files, 132 passed
node scripts/check-changeset-no-major.mjs / -fixed / -presence  → all OK
node scripts/check-control-bytes.mjs                            → OK (3957 files)

Downstream consumer sweep — prefix filter, i.e. the packages that DEPEND ON
plugin-grid:
pnpm --filter '...@object-ui/plugin-grid' type-check → 8 of 46
projects, all Done (plugin-grid, plugin-report, plugin-designer,
plugin-view, app-shell, console, console-starter, byo-backend-console;
apps/site is the ninth consumer and has no type-check script). The first two
attempts at this sweep failed with Cannot find module '@object-ui/plugin-grid'
and friends — cold dist in a fresh worktree, the AGENTS.md section 9
stale-artifact trap, not the change; building the console's dependency closure
first cleared every one of them.

A .changeset/*.md scores @object-ui/plugin-grid as patch, argued from the
fact the ruling rests on: the removed key never reached the query on any released
version, so nothing that worked stops working — what changes is that a filter
written under the published name now takes effect.


Generated by Claude Code

…lter`, singular (#4041)

The registration declared plural `filters` while `ObjectGrid` reads singular
`schema.filter`, and `schema.filters` had zero read points. An author following
the published vocabulary wrote `filters: [...]`, the save gate accepted it
(`sdui-parser` validates props against `inputs`), the renderer dropped it, and
the grid answered with the whole table — no error anywhere. The spelling that
worked was reported as `unknown-prop`.

Per the maintainer ruling (2026-08-10, option A) the declaration becomes
singular `filter` on both `object-grid` and its `view:grid` alias, aligned with
the renderer's read point, with `list-view`, and with the spec's own
`ListView.filter`. The plural is deleted rather than taught to the renderer: it
has zero read points, so no working usage depends on it.

The read point now lowers through `toFilterNode`, the repo's single hop before
the wire. `ListView.filter` is spec-declared `ViewFilterRule[]` and rejects AST
triples, so the newly-reachable key delivers rule objects — which sent verbatim
are refused as `400 INVALID_FILTER`. Declaring the key without the hop would
have traded a silent wrong answer for a guaranteed failure.

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 6:05am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.2 KB 350 KB
Entry file index-CbK63FmS.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) 484.11KB 106.78KB
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) 228.51KB 56.69KB
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.84KB 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) 113.81KB 27.53KB
plugin-gantt (index.js) 162.79KB 39.67KB
plugin-grid (index.js) 187.83KB 49.82KB
plugin-kanban (index.js) 48.53KB 13.38KB
plugin-list (index.js) 109.96KB 26.64KB
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

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

object-grid 的 inputs 声明 filters(复数),渲染器只读 filter(单数):按已发布词汇写筛选条件静默无效

2 participants