fix(core): bare-string filter options — docs/examples stop teaching it, runtime lift warns (#4356) - #4601
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
normalizeFilterOptionsaccepts a bare-stringglobalFilters[].optionsshorthand that@objectstack/specrejects at publish #4356 recorded the divergence:@objectstack/spec'sGlobalFilterSchema.optionsaccepts only{ value, label }pairs, while@object-ui/core'snormalizeFilterOptionsalso 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.Routed upstream as objectstack#7917 and ruled by the maintainer on 2026-08-12, verbatim:
Option ② — the spec stays strict; the runtime lift retires behind a deprecation window sized by a stored-dashboard survey.
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.
The type half already matches the ruling — objectui: dashboard KPI cards drop the translated widget title/description — the self-contained
metricpath bypassestWidgetTitle, and the plugin's privateresolveLabelnever callst()#4032 / PR fix(dashboard,i18n): KPI cards and dashboard filters resolve authored labels instead of dropping them (#4032) #4358 boundDashboardComponentSchema.globalFiltersto the spec. Untouched here.Phase 0 — the teaching census, re-verified
The survey named 6 non-test occurrences. Re-measured on
origin/mainat92250d648with a structural scan (bracket-match eachglobalFiltersarray, classify eachoptionsmember, any-position pass so mixed arrays are caught), all 6 confirmed and corrected to the pair form:content/docs/guide/dashboard-filters.md:98["EMEA", "APAC", "AMER"]content/docs/plugins/plugin-dashboard.mdx:165["EMEA", "APAC", "AMER"]packages/plugin-dashboard/README.md:222["EMEA", "APAC", "AMER"]examples/schema-catalog/.../filtered-dashboard.json:17["EMEA", "APAC", "AMER"]examples/schema-catalog/.../filtered-dashboard-dataset-widgets.json:17["EMEA", "APAC", "AMER"]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.mdalso carried a paragraph presenting the shorthand as an equal alternative — "Staticoptionsaccept the@objectstack/specobject 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
normalizeFilterOptionskeeps 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:
namelocal thatresolveDashboardFilterDefshad already resolved — nothing new is threaded through any public signature.normalizeFilterOptionsis module-private, so widening its parameter list is not a contract move.statusfilter 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 reasoningwarnOnUnknownActionKeysrecords for its own memo.resolveDashboardFilterDefsruns 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.warnOnDeprecatedObjectParamsinpackages/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-levelSetmemo, exported reset for tests. Prefix[dashboard-filters]via the module's existingwarnDateFilter.The guardrail, and a falsified premise
The survey suggested parsing each catalog entry with
DashboardSchemaand requiringsafeParseto succeed. Measured, that is not implementable — and the reason is not the shorthand. All 9plugin-dashboardentries are refused byDashboardSchematoday and stay refused after this fix:name/labelidentity keys — 2 issues per entry before any widget is read;object+categoryField+aggregate) that@objectstack/spec17 removed.A
DashboardSchema.safeParseassertion 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 everyglobalFilters[]entry in everyplugin-dashboardcatalog 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):Red on exactly the 3 defective entries; green in both directions on
filtered-dashboard-dynamic-optionsandfiltered-dashboard-filter-types(4 filter assertions) — those are the positive controls proving the sweep reaches real filters rather than passing vacuously. Asweep actually reaches filterspin guards theit.eachagainst reporting nothing, sinceit.each([])is silently green.Post-fix: 9 passed.
Red-first, verbatim
Warning pins, with only the
warnShorthandOptionscall removed (surgical — the export stays, so the red is an assertion failure and not an import error):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 optionspasses 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
metricpath bypassestWidgetTitle, and the plugin's privateresolveLabelnever callst()#4032 type pin (dashboard-global-filters-spec-binding.test.ts) green, untouched.I18nLabelmap options still carried through unresolved (I18nLabelnow admits an inline per-locale map — audit every read the compiler cannot see, and give Studio a way to author one #4163 data-loss regression guard).smoke.test.tsxrender/structural test unmoved.@objectstack/specuntouched; type bindings untouched (finding(types): two competing SchemaNode declarations — core's interface vs types' union #4580's seat);content/docs/releases/untouched.One in-repo surprise, fixed rather than absorbed: my explanatory comment quoted the expression
f.namealternated withf.field, andcolumn-identity.ratchet.test.tsis 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
pnpm --filter '@object-ui/core^...' --filter '@object-ui/core' build— green.resolveDashboardFilterDefs(normalizeFilterOptionsis module-private, so it has no direct importers): 5 packages —core,types,plugin-dashboard,plugin-designer,app-shell. Swept with the catalog: 575 files / 7506 tests passed, one unrelated-looking failure that turned out to be the ratchet above, now green....@object-ui/core= consumers, not dependencies): 72/72 tasks successful.check-control-bytes,check-phantom-dependencies,check-changeset-presence,check-changeset-no-major,check-changeset-fixed,check-type-check-coverage,check-lint-coverage,check-doc-links— all PASS.origin/mainon the same files: +1, theas anythe new test block needs to author a bare-string fixture at all (the objectui: dashboard KPI cards drop the translated widget title/description — the self-containedmetricpath bypassestWidgetTitle, and the plugin's privateresolveLabelnever callst()#4032 binding now rejects it at the type level) — the same cast the existing spec rc.6 refines a dateGlobalFilter.defaultValueto reject{ preset }— the exact shape objectui's normalizer produces and stored dashboards carry #4165 block uses. The new catalog test file is 0/0.Grading
@object-ui/core: minor. The dispatch predicted a byte-identical.d.tsand thereforepatch; measured withdistandtsbuildinfocleared both ways, that assumption is false — the emitted.d.tsgains exactly one declaration: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: minorin 62311b6. Never major.plugin-dashboard,app-shelland the schema catalog need no bump of their own: README-only, test-only, and@object-ui/example-*is in the changesetignorelist. All packages are in onefixedgroup regardless.Out of scope, filed separately
The whole-document spec divergence measured above — 9/9
plugin-dashboardcatalog entries refused byDashboardSchema, 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