Repository navigation
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
|
yinlianghui
marked this pull request as ready for review
August 13, 2026 17:31
This was referenced Sep 27, 2026
huangyiirene
pushed a commit
that referenced
this pull request
Sep 28, 2026
…bjectui#4356)
`normalizeFilterOptions` no longer lifts a non-object `options` member to a
`{ value, label }` pair: the member yields no option, a shorthand-only filter
resolves with no `options`, and a mixed array keeps only its object members.
The Phase 1 deprecation warning (PR #4601), whose text promised the lift, goes
with the arm; a dev-mode, once-per-filter warning names the dropped members
and the rewrite instead (ADR-0078 section 4), so `resetDashboardFilterWarnings`
keeps its memo and stays exported.
Rulings: objectstack#7917 「7917 ②」 (2026-08-12) and 「objectstack#7917
不考虑现有数据」 (2026-09-02). The docs callout and the `GlobalFilterSchema`
docblock in @object-ui/types stop describing the lift. Changeset: core minor.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
akarma-synetal
pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Oct 7, 2026
…bjectui#4356) (objectstack-ai#10930) Fixes objectstack-ai#4356 Clause-②: yes — published runtime behaviour is removed (the bare-string lift, and the exported `resetDashboardFilterWarnings` if it retires with its memo), per the execution parameters in `5511028727`. The claim's "if" resolved to **stays**: the memo survives (it now dedupes the dropped-member warning, see "The audible-skip question"), so `resetDashboardFilterWarnings` keeps its job and its export; the changeset names no removed export. Final head `a890654027a69911412efbcb146e326da8476256`. Commits on this branch since the branch point `733fd5ac6`: `bc4ea976e` (the retirement), `98623a24d` (rework round 1: W1–W3), `e0647486b` (patch round 2: W5), the two merges of `origin/main` (`1daf7edf7` at `29c76e573`, `a1058f90e` at `2eaf5be27`), and `a89065402` (W4, the dated note). Implemented in session `session_012UwY3ahMixEFkfTUxMVkYm`. ## What changed Phase 2 of objectui#4356 — the runtime bare-string `globalFilters[].options` lift retires. - `packages/core/src/utils/dashboard-filters.ts` — in `normalizeFilterOptions`, the non-object arm no longer pushes `{ value: String(o), label: String(o) }`. A member that is not an object (string, number, boolean) yields NO option: a shorthand-only filter resolves with no `options`, a mixed array keeps only its object members. The nullish / value-less skips are unchanged. The Phase 1 deprecation warning (`warnShorthandOptions`, whose text said "Still lifted here") is deleted; a dev-mode, once-per-filter `warnDroppedShorthandOptions` names the filter, the dropped members and the rewrite, and promises no lift. The `DashboardFilterDef.options` JSDoc, the `normalizeFilterOptions` docblock, the memo docblock and the `resetDashboardFilterWarnings` docblock stop describing a lift. No lift, alias, fallback or migration is added anywhere. - `packages/core/src/utils/__tests__/dashboard-filters.test.ts` — every lift-pinning test is flipped to pin the new contract (red-first, quoted under Tests): `normalizes options: spec {value,label} objects AND bare-string shorthand` became `normalizes spec {value,label} objects, and DROPS a bare-string member rather than lifting it`; the `[objectstack-ai#4356]` describe is `retired: dropped, not lifted` — a shorthand-only filter yields no `options`; a mixed array keeps only its object members; a number and a boolean are dropped too; warn ONCE across renders while dropping on every render; a different filter warns separately; canonical `{ value, label }` options and an `I18nLabel` map are untouched and silent; `resetDashboardFilterWarnings` re-arms the memo. The stale Phase 1 header above the block is removed (W3). - `content/docs/guide/dashboard-filters.md` — the "Deprecated: … still lifted by the runtime" callout became false and now reads "Not accepted": refused at publish, no longer lifted, a bare-string member yields no option, rewrite `"X"` as `{ "value": "X", "label": "X" }`. No fenced block changed. - `packages/types/src/zod/complex.zod.ts` — the JSDoc of the exported `GlobalFilterSchema` said the runtime "still LIFTS a stored bare-string option on read"; that sentence ships in `@object-ui/types`' emitted declarations and is corrected in place. Outside the claim's original surface; accepted by the seat's addendum, see "Deviations". - `packages/plugin-dashboard/README.md` (W1, ships in that package's `files`) — the `globalFilters` example's `options` comment no longer says a stored bare string "is still lifted at runtime, and warns"; it says NOT accepted, the runtime no longer lifts it (objectui#4356), a bare member yields no option, a dev-mode warning names it, rewrite each X as `{ "value": X, "label": X }`. - Three test docblocks state the retirement instead of the lift (W2): `packages/types/src/__tests__/dashboard-global-filters-spec-binding.test.ts`, `packages/types/src/__tests__/report-chart-query-spec-parity.test.ts` (its header docblock only — nothing near objectui#10916's `JoinedReportBlock` pin, which PR objectstack-ai#10934 rewrote and this branch has merged), `packages/plugin-dashboard/src/__tests__/DashboardRenderer.filters.test.tsx`. A fourth (W5), `packages/plugin-dashboard/src/__tests__/DashboardFilterBar.i18nLabel.test.tsx`, no longer says the shorthand "is deprecated and now warns" nor names the renamed lift pin; it names the current DROP pin. No assertion moved in any of the four. - `.changeset/retire-options-shorthand-lift-4356.md` — `@object-ui/core: minor`, opening with the ruled wording: "bare-string `globalFilters[].options` is no longer lifted; use `{ value, label }` objects, the spec's form". Never `major` (AGENTS.md §9 version policy: the break is stated in the body, the bump stays `minor`); `check-changeset-no-major` exit 0. - `.changeset/7759-dashboard-header-global-filters-spec.md` (W4) — a dated note appended, nothing else touched. That pending entry says "The runtime is unchanged too: `@object-ui/core` still converts a STORED bare-string option into a pair when it reads the document, and logs a deprecation warning. That conversion keeps its own retirement schedule (objectstack#7917)." — a sentence this PR makes false in the same release. The note (PR objectstack-ai#10891 form, dated 2026-09-29) says which sentences no longer hold and what ships. Authorised by the maintainer's ruling of 2026-09-29, option 「Allow the appended note (Recommended)」, recorded on the card as `5883360525`; the option reads, verbatim: "You allow developers to append dated correction notes to pending .changeset files (appended only, no existing line or frontmatter edited). The objectstack-ai#10930 developer adds the W4 note inside that PR, then it merges main, gets a final review, and merges." Measured: 16 insertions, 0 deletions; frontmatter md5 `ac1bfd0472c0d0b981d923c32d435f18` before and after; whole-file md5 `94edd5855709afe137f9184c630f298c` → `5c8f0508bed5ed6f5b76254bb74d622d`; 0 angle-bracket characters; `diff` against `origin/main` shows only the appended block. Untouched on purpose: `examples/schema-catalog/test/plugin-dashboard-global-filters-spec.test.ts` (the `GlobalFilterSchema` catalog guardrail from PR objectstack-ai#4601 — the positive control; its docblock narrates history and spec facts only, and stays true); the assertions of the two `packages/types` tests that pin the SPEC's refusal of `options: ['EMEA']` (rejection pins, not lift pins); `@objectstack/spec`; `content/docs/releases/**`; `skills/**` (no file there becomes false — measured below). ## Rulings executed - objectstack-ai/objectstack#7917, maintainer 2026-08-12, verbatim 「7917 ②」 — the spec stays strict; the runtime bare-string lift retires. - Maintainer, 2026-09-02, verbatim 「objectstack#7917 不考虑现有数据」 (director release `5511028727` on objectui#4356) — Phase 2 on release cadence alone; no live-tenant survey, no migration entry. - Maintainer direct dispatch to the `domain:spec @ objectui` seat, verbatim 「这个你也接管继续 objectstack-ai#4356」. - Maintainer, 2026-09-29, 「Allow the appended note (Recommended)」 (card comment `5883360525`) — authorises the W4 append in exactly the form landed here. ## The audible-skip question — governing text found The dispatch asked whether any governing text requires the post-removal skip to be audible. What was read, verbatim: - objectstack `AGENTS.md`, "Degradation log levels — `warn` vs `error`": "**Functional degradation → `warn` / `info`.** A screen is missing, a trigger is not armed, a capability is not enabled … The system is *visibly* smaller than it should be, and the next person to use the missing thing finds out." and "**prefer failing to falling back**, and when you must fall back, say what was lost." — A `select` filter whose members were all bare strings now renders with an empty option list (`DashboardFilterBar`: `const authored = def.options?.length ? def.options : (dynamicOptions ?? [])`): visibly smaller, functional, `warn`-level — and "say what was lost" is the operative sentence. - objectstack `docs/adr/0078-no-silently-inert-metadata.md`, §4 "Loud-not-silent at the runtime seam": "Where the runtime *skips* an inert instance … it emits a **dev-mode diagnostic** at registration rather than swallowing it silently. This is the only layer that is **authoring-tool-agnostic**". The ADR's completeness gate (§1) is about Zod-VALID instances and a bare string is Zod-invalid, but §4 is exactly the case here: a STORED dashboard is the one document no author-time gate re-reads. - objectui `AGENTS.md`: no clause on audibility; #0.1 (contract-first, no lenient fallback) is honoured — a diagnostic is not a fallback, and nothing is lifted. - The module's own convention, same file: `buildFilterCondition` — "anything else → skipped with a console warning, never silently downgraded"; `buildWidgetScopedFilter` skips an unknown default field "with a console warning instead of emitting a query". Conclusion: yes, at `warn`, dev-mode, deduped — the smallest audible path consistent with the ruling. Implemented as `warnDroppedShorthandOptions`: same `isDev()` gate and memo key as Phase 1 (no per-render flood), new sentence (DROPPED, "no longer lifted", the rewrite), no lift. Consequence: the memo and therefore `resetDashboardFilterWarnings` stay. The contract review (`5866198610`) judged this RIGHT; dev-only rather than always-on is the same review's answer for this PR. ## Measurements against the dispatch's mechanism assumptions 1. Lift live on `origin/main` `733fd5ac6` — confirmed: the `else` arm pushed `{ value: String(o), label: String(o) }` and called `warnShorthandOptions` for EVERY non-nullish primitive. Pinned now in `drops EVERY non-object member — a number and a boolean, not only a string`. 2. `resetDashboardFilterWarnings` — `git grep` repo-wide: importers are `dashboard-filters.test.ts` only (plus a `packages/core/CHANGELOG.md` paragraph); it clears only `warnedShorthandOptions`. The inference "with the memo gone it has no job" is falsified by (4): the memo does not go. It stays, with a docblock saying why. 3. Fixture recount, repo-wide over every file containing `globalFilters` (tests included; positive control: the same matcher found the callout it was meant to find): lift-pinning bare-string members existed only in `dashboard-filters.test.ts` — six members across five tests (the dispatch's list lacked the bare `'lost'` of the mixed-array test; the director's "three" and the dispatch's "five" were both stale). All six retired or flipped. The two `packages/types` occurrences pin the spec's REFUSAL and keep their assertions. 4. Skip audibility — see the section above. 5. Teaching sites — swept at the final head with `git grep -n -i -E 'lift' -- ':!**/CHANGELOG.md' ':!content/docs/releases/**' ':!pnpm-lock.yaml' | grep -i -E 'shorthand|bare[- ]string|4356'` over the whole tracked tree (a superset of every package's `files[]`, `content/docs`, `skills/`, `examples/`): 20 hits on `bc4ea976e` with `packages/plugin-dashboard/README.md`'s "still lifted at runtime" line as the found positive control; 18 on `a89065402` — every hit either states the retirement (this PR's changeset, source, docs and test text) or is the unrelated `data`-array shorthand in `plugin-grid` / `plugin-map`. The first-round sweep's `option|filter` post-filter is what hid the README line (W1) and the `i18nLabel` comment (W5); the rework sweep has no post-filter. When the sweep runs on a ref, the ref prefix is stripped before the second grep — this branch's name contains both "shorthand" and "lift". Three other sites author `options: ['…']` on `select` FIELDS (form / object field schemas: `content/docs/guide/expressions.md`, `content/docs/guide/schema-playground.md`, `skills/objectui/guides/project-setup.md`) — a different contract, out of this card, untouched; the governed `skills/**` file does not become false. 6. Release — `packages/core/package.json` is `17.6.0`; `.changeset/config.json` has `@object-ui/core` in the `fixed` group; a `minor` changeset makes the next release ≥ 17.7.0; release PR objectstack-ai#5400 is open (not touched). 7. Pending changesets — swept on the merged tree (2326 pending entries) with `git grep -n -i -E 'normalizeFilterOptions|bare[- ]string|shorthand|lift' -- '.changeset/*.md'` filtered to option / `globalFilters` / 4356 / 7917 context: exactly ONE pending sentence this PR makes false, the `7759` paragraph quoted above (W4). Its earlier sentence "the bare-string option shorthand (`options: ['EMEA']`) is refused with `invalid_type`" stays true. Every other hit is a different contract (export-options bare array, object-map / gantt `data` arrays, kanban `columns`, picklist / field `options`, widget shorthand). The nine entries the `changeset-claim-re-read` comment `5865883972` lists were re-read: none says anything about the options lift; all still true. ## Merges of `main` - `1daf7edf7` — `origin/main` `29c76e573` (PR objectstack-ai#10934, objectui#10916). No conflicts; `report-chart-query-spec-parity.test.ts` auto-merged (disjoint hunks); both sides verified — my header-docblock correction present, PR objectstack-ai#10934's pin text byte-for-byte as `main` has it. - `a1058f90e` — `origin/main` `2eaf5be27` (70 commits, among them PR objectstack-ai#10914's citation edits in `dashboard-filters.ts` and three `complex.zod.ts` commits). No conflicts; after the merge the files differing from `origin/main` are exactly this PR's ten, and `git diff origin/main` on `complex.zod.ts` and on the parity test is exactly this PR's docblock hunks. No rebase, no amend, no force-push at any point; every push was a plain `git push`. ## Tests Red-first (at `bc4ea976e`, the new pins against the UNTOUCHED source, `pnpm exec vitest run packages/core/src/utils/__tests__/dashboard-filters.test.ts`, exit 1): ```text × normalizes spec {value,label} objects, and DROPS a bare-string member rather than lifting it (objectui#4356) × DROPS a bare string: a shorthand-only filter resolves with no `options`, and says so × keeps ONLY the object members of a MIXED array, and names only the dropped ones × drops EVERY non-object member — a number and a boolean, not only a string × warns ONCE per offending filter across repeated renders, and drops on every one of them AssertionError: expected [ …(2) ] to be undefined AssertionError: expected [ Array(2) ] to deeply equal [ { value: 'won', label: 'Won' } ] AssertionError: expected [ { value: '42', label: '42' }, …(2) ] to deeply equal [ { value: 'gold', label: 'Gold' } ] Test Files 1 failed (1) Tests 5 failed | 44 passed (49) ``` The three that stayed green on the old code are the ones the block labels as non-discriminating (different-filter memo, canonical silence, memo reset). Green after the source change, same command: `Tests 49 passed (49)`. Final head `a89065402`, one `pnpm exec vitest run` from the repo root under the shared verify lock over `packages/core/` plus the two `packages/types` global-filter tests and the two `plugin-dashboard` dashboard-filter tests this PR touched: `Test Files 188 passed (188)` · `Tests 3725 passed | 27 skipped (3752)`, exit 0. The changeset script tests (`check-changeset-{presence,no-major,fixed,overwrite,claims}`, `check-pending-changeset-literals`, `changeset-polarity-census`): `Test Files 7 passed (7)` · `Tests 292 passed (292)`, exit 0. Consumer sweep at `bc4ea976e` (`resolveDashboardFilterDefs` consumers re-derived by `git grep`: `core`, `types`, `plugin-dashboard`, `plugin-designer`, `app-shell`, `examples/schema-catalog`), exit 0: `packages/core` 182 files / 3643 tests; `packages/plugin-dashboard` 147 / 1346 (6 pre-existing skips); `packages/types` 3 / 58; `packages/plugin-designer` (`DashboardDesignPage.presetAlias`) 1 / 4; `packages/app-shell` (`DashboardWidgetInspector.test.tsx`) 1 / 19; `examples/schema-catalog` (the guardrail) 1 / 9 — totals `335 passed (335)` · `5073 passed | 6 skipped (5079)`. No consumer fixture asserted the old lift. The code has not changed since that head (every later commit is prose or a merge), and the final-head run above re-covers `core`, `types` and the touched `plugin-dashboard` tests. ## Gates on the final head `a89065402` (hand-derived from root `package.json` and `.github/workflows/`; exit codes captured before any pipe) | gate | exit | note | |---|---|---| | `pnpm --filter '@object-ui/core^...' build` then `pnpm --filter @object-ui/core build` | 0 / 0 | dependency closure first, so the type-checks read rebuilt declarations | | `pnpm --filter @object-ui/core type-check` · `pnpm --filter @object-ui/types type-check` | 0 / 0 | | | `pnpm --filter @object-ui/core lint` · `pnpm --filter @object-ui/types lint` | 0 / 0 | 0 errors; the warnings are pre-existing `no-explicit-any` — on the touched files the `as any` counts equal the base (2 / 10 / 0), unchanged since `bc4ea976e` | | vitest `packages/core/` + the four touched dashboard-filter tests (under the lock) | 0 | 188 files, 3725 passed, 27 skipped | | vitest changeset script tests ×7 | 0 | 292 passed | | `node scripts/check-changeset-presence.mjs` | 0 | 11 files changed vs merge-base `2eaf5be27`, 7 published source files of 3 released packages, 1 changeset added | | `node scripts/check-changeset-no-major.mjs` · `check-changeset-fixed.mjs` · `check-changeset-claims.mjs` · `check-changeset-overwrite.mjs` | 0 ×4 | claims and overwrite are report-only; the W4 append is overwrite's documented "correcting a declaration on purpose" case | | `pnpm check:pending-changeset-literals` | 0 | "No test source names a pending changeset" | | `node scripts/check-control-bytes.mjs` | 0 | 9331 tracked text files | | `node scripts/check-new-cross-file-line-citations.mjs` | 0 | 0 new citations (the W4 note cites by quoted sentence) | | `node scripts/check-governed-queue-guard.mjs --test` (every touched path) | 0 | NOT GOVERNED | | `pnpm check:doc-fences` · `check:doc-types` · `check:doc-example-ids` · `check:spec-symbols` · `node scripts/check-doc-links.mjs` · `check:test-path-roots` · `check:vi-mock-specifiers` · `check:phantom-deps` | 0 ×8 | measured at `bc4ea976e` / `98623a24d`; the files they read have not changed since | | `pnpm check:doc-snippets` · `pnpm check:readme-exports` | 2 / 1 | NOT MEASURED locally — both `PRECONDITION NOT MET` (unbuilt packages); CI's Doc Snippet Type Check and README Exports are the readings. The docs hunk changes no fenced block; the README hunk is a JSON comment with no import binding | Not run locally, CI-owned: repo-wide `pnpm lint`, `pnpm test`, `pnpm type-check` (the public type face is unchanged — no export added or removed, no signature changed; emitted declarations differ in JSDoc text only), `pnpm check`, the i18n gates, `plugin-dashboard`'s own type-check (comment-only hunks in two test files). ## Deviations - `packages/types/src/zod/complex.zod.ts` was outside claim `5865368364`'s original file surface; edited under the bounded in-place-fix exemption (same defect class; mechanical; no other open PR on the file; inside core's type-checked closure) and accepted by the seat's addendum `5865980488`. The later addenda `5866223135`, `5866346457`, `5866480991` added the W1–W5 files and the `7759` changeset. - The first-round teaching sweep under-reported (W1, W5) because of its `option|filter` post-filter; corrected above. - The W4 append was first refused by the auto-mode classifier ("Modify Shared Resources", record `5866443287`), held and not re-routed; it landed only after the maintainer's 2026-09-29 ruling. ## Acceptance notes - `warnDroppedShorthandOptions` is dev-only, exactly as the Phase 1 warning was; the module's other skip warnings are not dev-gated. Always-on is a one-line product decision the review left to the maintainer; not this PR's. - Field-level `select` schemas teaching `options: ['…']` are a different contract and were not judged here — observation only, not filed. - `packages/core/CHANGELOG.md` and `packages/plugin-dashboard/CHANGELOG.md` narrate the lift as history; published record, not re-addressed (the CHANGELOG clause under AGENTS.md objectstack-ai#11: never re-address a paragraph in a published CHANGELOG). - PR objectstack-ai#10914 (objectui#10803 batch 6) has landed on `main` and is merged into this branch; no other open PR touches this PR's files. --- _Generated by [Claude Code](https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
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