Skip to content

fix(core): bare-string filter options — docs/examples stop teaching it, runtime lift warns (#4356) - #4601

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4356-shorthand-warn-now
Aug 13, 2026
Merged

fix(core): bare-string filter options — docs/examples stop teaching it, runtime lift warns (#4356)#4601
yinlianghui merged 1 commit into
mainfrom
claude/issue-4356-shorthand-warn-now

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Phases 0+1 of #4356; Phase 2 is scheduled on objectstack-ai/objectstack#7917. Not Fixes — merging this does not close the card, because removing the lift is still outstanding.

The ruling chain

  1. normalizeFilterOptions accepts a bare-string globalFilters[].options shorthand that @objectstack/spec rejects at publish #4356 recorded the divergence: @objectstack/spec's GlobalFilterSchema.options accepts only { value, label } pairs, while @object-ui/core's normalizeFilterOptions also lifted a bare-string shorthand. A dashboard authored with the shorthand rendered correctly in objectui and was refused the moment it reached the platform — the "one strict contract beats N dialects" case AGENTS.md #0.1 names, with the renderer's tolerance acting as a second de-facto contract that hides the producer's bug.

  2. Routed upstream as objectstack#7917 and ruled by the maintainer on 2026-08-12, verbatim:

    「7918 A,7917 ②,7900 收敛两扇门,7929 来源标记」

    Option ② — the spec stays strict; the runtime lift retires behind a deprecation window sized by a stored-dashboard survey.

  3. The survey was delivered and ACCEPTed on objectstack#7917. It found the shorthand's source: objectui's own docs and its schema-catalog corpus still taught the form, so the stored population was still growing. It asked for Phase 0 (stop teaching it) to ship together with the ruled Phase 1 (warn), since warning authors while the docs still teach the form is a contradiction users report as a bug.

  4. The type half already matches the ruling — objectui: dashboard KPI cards drop the translated widget title/description — the self-contained metric path bypasses tWidgetTitle, and the plugin's private resolveLabel never calls t() #4032 / PR fix(dashboard,i18n): KPI cards and dashboard filters resolve authored labels instead of dropping them (#4032) #4358 bound DashboardComponentSchema.globalFilters to the spec. Untouched here.

Phase 0 — the teaching census, re-verified

The survey named 6 non-test occurrences. Re-measured on origin/main at 92250d648 with a structural scan (bracket-match each globalFilters array, classify each options member, any-position pass so mixed arrays are caught), all 6 confirmed and corrected to the pair form:

# Occurrence Was
1 content/docs/guide/dashboard-filters.md:98 ["EMEA", "APAC", "AMER"]
2 content/docs/plugins/plugin-dashboard.mdx:165 ["EMEA", "APAC", "AMER"]
3 packages/plugin-dashboard/README.md:222 ["EMEA", "APAC", "AMER"]
4 examples/schema-catalog/.../filtered-dashboard.json:17 ["EMEA", "APAC", "AMER"]
5 examples/schema-catalog/.../filtered-dashboard-dataset-widgets.json:17 ["EMEA", "APAC", "AMER"]
6 examples/schema-catalog/.../filtered-dashboard-target-widgets.json:12 ["draft", "sent", "paid", "void"]

The true census is 7, not 6. A structural scan sees code blocks; it does not see prose. content/docs/guide/dashboard-filters.md also carried a paragraph presenting the shorthand as an equal alternative — "Static options accept the @objectstack/spec object form … or a bare-string shorthand … the runtime normalizes both". That sentence taught the form more directly than any of the six code blocks, and it is corrected here too: the canonical form is stated as the only one the platform accepts, with the shorthand demoted to a deprecation callout carrying the migration rule.

Two test occurrences were also migrated, following the precedent already set in DashboardRenderer.filters.test.tsx (which a previous card migrated with exactly this reasoning — the survey's count of 5 test hits is now stale at 3):

  • DashboardWidgetInspector.test.tsx:138 — purely incidental scenery; nothing in that suite reads the option list.
  • DashboardFilterBar.i18nLabel.test.tsx:165 — a mixed array. Its mixed-lift coverage is not lost: it is replaced by a dedicated, stronger pin in core's own suite (see below).

The third test occurrence, in packages/core/src/utils/__tests__/dashboard-filters.test.ts, is the one that pins the lift and stays — now capturing the warning instead of leaking it into the suite's output.

Phase 1 — the warning

normalizeFilterOptions keeps the lift, byte-identically. It is mechanically lossless ('EMEA' becomes { value: 'EMEA', label: 'EMEA' }), and stored dashboards carry the shorthand, so dropping it silently would turn a rendering filter into an empty one.

Design:

  • Identity. The warning names the filter, quotes the offending values, and prints the canonical replacement. The identifying context is the name local that resolveDashboardFilterDefs had already resolved — nothing new is threaded through any public signature. normalizeFilterOptions is module-private, so widening its parameter list is not a contract move.
  • Dedupe. Module-level memo keyed by filter name and offending values. Name-only would report the first dashboard carrying a shorthand status filter and stay silent about every other one, sending the author to fix one symptom; values-only would collapse two genuinely different filters that share an option list. Same reasoning warnOnUnknownActionKeys records for its own memo.
  • Once per session, not per render. resolveDashboardFilterDefs runs on every dashboard render; a warning without the memo floods the console per frame, and a warning that floods is a warning that gets muted.
  • Convention. Matches warnOnDeprecatedObjectParams in packages/core/src/actions/actionKeys.ts — the repo's existing precedent for exactly this shape (a spec-refused authoring form the runtime still reads for one version window): dev-mode-only gate, module-level Set memo, exported reset for tests. Prefix [dashboard-filters] via the module's existing warnDateFilter.
  • No false positives. Silent on canonical object options — otherwise it would fire on every healthy dashboard in the product. A mixed array names only its bare members; partial migrations happen, and re-reporting already-canonical members sends the author back to options they just fixed.
  • Quiet in our own suites. The three in-repo test fixtures that carried the shorthand either moved to the pair form or now capture the warning, so it fires only in the tests that assert it.

The guardrail, and a falsified premise

The survey suggested parsing each catalog entry with DashboardSchema and requiring safeParse to succeed. Measured, that is not implementable — and the reason is not the shorthand. All 9 plugin-dashboard entries are refused by DashboardSchema today and stay refused after this fix:

  • they are objectui SDUI component schemas, not stored platform metadata documents, so they carry no name / label identity keys — 2 issues per entry before any widget is read;
  • most of their widgets use the pre-ADR-0021 inline analytics shape (object + categoryField + aggregate) that @objectstack/spec 17 removed.

A DashboardSchema.safeParse assertion would be permanently red, and making it green would mean rewriting all 9 examples into platform-metadata shape — far larger than this card. That divergence is real and filed separately (see below); it is deliberately not smuggled in here.

So the guardrail is pinned at the exact spec sub-schema that owns the surface this card governs: GlobalFilterSchema, applied to every globalFilters[] entry in every plugin-dashboard catalog example. Same schema the platform runs, over the property that actually regressed.

Discrimination proof — the guardrail run against the pre-fix catalog JSONs (fix taken out with git checkout origin/main --, restored and sha256-verified):

❯ examples/schema-catalog/test/plugin-dashboard-global-filters-spec.test.ts (9 tests | 3 failed)
  × plugin-dashboard/filtered-dashboard globalFilters[0] is accepted by GlobalFilterSchema
  × plugin-dashboard/filtered-dashboard-dataset-widgets globalFilters[0] is accepted by GlobalFilterSchema
  × plugin-dashboard/filtered-dashboard-target-widgets globalFilters[0] is accepted by GlobalFilterSchema
+   "[options.0] Invalid input: expected object, received string",
+   "[options.1] Invalid input: expected object, received string",
+   "[options.2] Invalid input: expected object, received string",
 Test Files  1 failed (1)
      Tests  3 failed | 6 passed (9)

Red on exactly the 3 defective entries; green in both directions on filtered-dashboard-dynamic-options and filtered-dashboard-filter-types (4 filter assertions) — those are the positive controls proving the sweep reaches real filters rather than passing vacuously. A sweep actually reaches filters pin guards the it.each against reporting nothing, since it.each([]) is silently green.

Post-fix: 9 passed.

Red-first, verbatim

Warning pins, with only the warnShorthandOptions call removed (surgical — the export stays, so the red is an assertion failure and not an import error):

× still lifts a bare string, byte-identically, AND warns
× warns ONCE per offending filter across repeated renders, not once per render
× warns separately for a DIFFERENT filter — the memo is not a global mute
× names ONLY the bare members of a MIXED array
 Test Files  1 failed (1)
      Tests  4 failed | 40 passed (44)

4 of the 5 new pins go red. The fifth is honestly not a discrimination proof and is labelled as such in the file: says NOTHING for canonical options passes vacuously against a build with no warning at all. Its value is directional the other way — it goes red if the warn ever starts firing on healthy dashboards. Predicted in writing before the run, and it came out as predicted.

Must-not-change — all held

One in-repo surprise, fixed rather than absorbed: my explanatory comment quoted the expression f.name alternated with f.field, and column-identity.ratchet.test.ts is a deliberately line-level scanner, so the comment read as a second dual read and failed the ratchet count. Reworded the comment instead of bumping the inventory to 2 — inflating the count for a comment would mask a future real read.

Verification

Grading

@object-ui/core: minor. The dispatch predicted a byte-identical .d.ts and therefore patch; measured with dist and tsbuildinfo cleared both ways, that assumption is false — the emitted .d.ts gains exactly one declaration:

> /** Reset the shorthand-options warn-once memo. Exported for tests. */
> export declare function resetDashboardFilterWarnings(): void;

plus doc-comment prose. Purely additive; nothing narrowed, widened or removed, so no existing consumer breaks. Re-graded by position analysis and by the repo's own precedent: resetActionKeyWarnings — the same reset-the-warn-memo export, in the same package, alongside the same kind of deprecation warning — was graded @object-ui/core: minor in 62311b6. Never major.

plugin-dashboard, app-shell and the schema catalog need no bump of their own: README-only, test-only, and @object-ui/example-* is in the changeset ignore list. All packages are in one fixed group regardless.

Out of scope, filed separately

The whole-document spec divergence measured above — 9/9 plugin-dashboard catalog entries refused by DashboardSchema, mostly for the pre-ADR-0021 widget shape — is a real finding and strictly larger than this card. Filed unassigned as #4600 rather than fixed here.


Generated by Claude Code

…t, runtime lift warns (#4356)

Phases 0+1 of objectui#4356, under the maintainer ruling of 2026-08-12 on
objectstack#7917 (verbatim 「7917 ②」): the spec stays strict; the runtime
bare-string lift retires behind a deprecation window sized by a stored-dashboard
survey. Phase 2 (removing the lift) is scheduled on objectstack#7917 and is
deliberately NOT here.

Phase 1 — normalizeFilterOptions keeps the lift, unchanged and mechanically
lossless, and now logs a deprecation warning naming the offending filter, the
offending values, and the canonical pair form. Warn-once per offending filter
per session (this runs on every dashboard render) and dev-mode only, matching
the warnOnDeprecatedObjectParams convention in actions/actionKeys.ts. Silent on
canonical object options; a mixed array names only its bare members.

Phase 0 — all seven non-test teaching occurrences corrected to the pair form.
The survey named six; a seventh is a prose passage in the dashboard-filters
guide that presented the shorthand as an equal alternative, which a code-block
scan does not see.

Guardrail — every globalFilters[] entry in every plugin-dashboard schema-catalog
example is now parsed with the real @objectstack/spec GlobalFilterSchema, with a
non-vacuity control. The catalog previously asserted only structure and
render-without-throw, which is how a spec-invalid example got in.

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

vercel Bot commented Aug 13, 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 13, 2026 5:13pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation package: core plugin examples tests labels Aug 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-DtxyaJBo.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 25.13KB 5.40KB
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) 38.46KB 10.17KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
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) 5.02KB 0.88KB
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) 489.33KB 108.47KB
core (index.js) 3.79KB 1.52KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 163.56KB 44.83KB
fields (index.js) 230.37KB 57.17KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.84KB 1.45KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.98KB 10.85KB
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.75KB 3.80KB
plugin-calendar (index.js) 46.86KB 12.91KB
plugin-charts (index.js) 62.10KB 17.67KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 121.04KB 31.57KB
plugin-designer (index.js) 212.58KB 42.83KB
plugin-detail (index.js) 239.93KB 60.01KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.30KB 40.02KB
plugin-grid (index.js) 189.37KB 50.33KB
plugin-kanban (index.js) 52.74KB 14.53KB
plugin-list (index.js) 111.13KB 27.12KB
plugin-map (index.js) 18.16KB 5.81KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.38KB 11.09KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.09KB 20.56KB
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) 27.64KB 9.44KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.26KB 0.67KB
react (schema-input.js) 1.45KB 0.83KB
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 (dashboard-filter-alias.js) 6.23KB 2.74KB
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) 3.05KB 1.52KB
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 13, 2026 17:31
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 13, 2026
Merged via the queue into main with commit f279deb Aug 13, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4356-shorthand-warn-now branch August 13, 2026 17:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation examples package: core plugin tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants