Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

Commit e356c39

Browse files
os-justinclaude
andauthored
fix(plugin-dashboard,cli): converge the bare dashboard key on plugin-dashboard, retire view:dashboard with a by-name tombstone (#9803)
Fixes #9533 Ruled shape, executed: director seat summon #24, batch #152 item 5, **letter 1**, maintainer 「其他同意」 2026-09-18 (issue comment 5724939774). Routes 2 and 3 were rejected on the record and are not reopened here. ## The reproduction came first, and it confirmed the card The card and the ruling both required the never-cleared stub to be reproduced against a real `Registry` before acting, because it had been read off `Registry.register`'s clearing lines rather than executed. It was, on a fresh `Registry` fed this package's REAL declared metadata read back out of the registry: ```text === fresh Registry, stub then eager (what the console does) === after stub : hasLazy('dashboard','plugin-dashboard') = true after stub : getMeta('dashboard')?.namespace = "plugin-dashboard" after load : hasLazy('dashboard','plugin-dashboard') = true <-- THE CLAIM after load : getMeta('dashboard')?.namespace = "view" after load : get('dashboard','plugin-dashboard') = undefined === collision warnings seen === 0 ``` Both consequences reproduce. The stub is never cleared (`register()` clears the stub of the type IT registers, and that type was `view:dashboard`), the whitelisted key resolves to `undefined` forever, and the bare key's declared namespace flips mid-sequence. The zero collision warnings are the triage seat's point measured: this shape came in through `registerLazy`, which takes the same fallback branch with no collision check at all. Nothing falsified the ruling. ## What letter 1 required, and where each clause landed | clause | where | | --- | --- | | package registers `plugin-dashboard:dashboard`; the `view` namespace registration goes | `packages/plugin-dashboard/src/index.tsx` | | authored `view:dashboard` refused BY NAME with a tombstone pointing at `plugin-dashboard:dashboard` | `packages/plugin-dashboard/src/retired-node-types.tsx`, registered last with `skipFallback: true` | | `content/docs/plugins/plugin-dashboard.mdx` row regenerates | that file, plus the package README's twin table | | CLI whitelist regenerates | `packages/cli/src/utils/known-schema-types.ts`, one line: `view:dashboard` leaves | | `check-registry-bare-name-collisions.mjs`'s row for this pair goes | the shrink-only ledger is now **empty** | | ownership pinned in BOTH orders, `hasLazy` false after load | `packages/plugin-dashboard/src/__tests__/dashboardBareKeyOwnership.test.tsx` | The ledger row was **deleted, not reworded** — the gate now prints `0 contested; ledger holds 0`. ## Why the tombstone spelling is withheld from the key universe A literal `register('dashboard', Tombstone, { namespace: 'view' })` would have put `view:dashboard` straight back into `deriveRegistryKeys`' universe and therefore back into `KNOWN_SCHEMA_TYPES` — and objectui#8760 graded exactly that as the wrong side of the line: *"an unknown key is refused loudly at authoring time; a registered-but-unfulfilled key passes every check, is taught by the documentation, and fails only at render in front of a user."* Blessing the retired spelling in the CLI would have rebuilt the defect this card removes, one spelling over. So the tombstone registrar is collection-keyed and declared `excluded` in `INDIRECT_REGISTRATIONS`, the same disposition `RETIRED_FIELD_TYPES`' tombstones already take (objectui#9717 holds the open question; both are answered the same way so they cannot drift apart). Net effect: the runtime points the author at the replacement, and `objectui check` names the retired spelling instead of blessing it. ## The pin, and the reverse verification that proves it can fail Both ablation legs ran against the COMMITTED fix, each with an on-disk mutation proof and a restore proven by `git diff HEAD` being empty: - **Leg 1 — namespace back to `view`** (occurrences of the new spelling 8 to 7, blob `cf38965` to `ee4c498`): the pin goes **7 of 10 red**; `check:registry-bare-names` exits 1 and reprints the card's own finding verbatim, naming all three claimants; `regenerate-known-schema-types.mjs --check` exits 1. - **Leg 2 — tombstone registrar deleted** (registrar call 1 to 0, blob `cf38965` to `36f5964`): **5 of 10 red**, including `view:dashboard` resolves to the TOMBSTONE, not to the renderer and renders the tombstone, carrying the migration text the table declares. Leg 2 exists because the refusal is the assertion most easily faked. The pin asserts the refusal **and** the tombstone's own text (read from the table the component reads, never restated), the `role="alert"` element, the `data-retired-node-type` attribute and the `console.error` carrying the same string — plus a control that bare `dashboard` and `plugin-dashboard:dashboard` still paint the real grid, so "nothing rendered" cannot pass for a refusal. ## Clause-②: yes — and what the mechanical floor does NOT prove Declared by the ruling: the published full name moves. The shape is unusual and worth stating so it does not confuse a reviewer's measurement — **the accept set narrows** (an authored `view:dashboard` stops resolving) and the clause is `yes` anyway, on the published-name move rather than on a widened surface. ⇒ the `^\+export` floor is useless here **in both directions**. Counted against this branch's merge-base `42234b1a`: it adds **5** `^\+export` lines — four in the new `retired-node-types.tsx` (`RETIRED_DASHBOARD_NODE_TYPES`, `reportRetiredDashboardNodeType`, `resetRetiredDashboardNodeTypeReports`, `RetiredDashboardNodeTombstone`) and one `export { … } from './retired-node-types'` block in `index.tsx`. ⚠️ **An earlier revision of this paragraph said 2, and named only two of the symbols — that was wrong**; the at-tier contract review measured the floor at 5 off the head diff, and re-measuring it here reproduces 5 (same number at `588e45950f` and at head). That count proves neither half of the clause. It does not prove `yes`, because those five lines are a NEW surface rather than the moved published name the clause is actually about — and to be exact about what kind of surface: the four symbols are **published**, not internal. `src/index.tsx` is the file this package's `exports["."]` publishes, so anything that resolves `@object-ui/plugin-dashboard` can import them, and dropping one later is a breaking change like any other export. (They are declared in the changeset as of `96b6ee2820`.) It could not have proven `no` either, because the thing that moved — the string `plugin-dashboard:dashboard` replacing `view:dashboard` inside a `register(...)` call — adds no export line at all and would score 0 on a floor that only counts them. The clause rests on the registry key, which is a runtime string, and the instrument that re-derives it is `dashboardBareKeyOwnership.test.tsx` plus the regenerated whitelist, not a diff-line count. Changeset marked `minor` per AGENTS.md's 版本号策略 (objectui's own breaking changes take `minor`, with the breaking semantics written out), naming the tombstone under 「Breaking for authored metadata」. ## Verification Ordinary-direction result throughout; nothing reversed or fanned out. - `pnpm exec vitest run packages/plugin-dashboard/ packages/cli/ packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx` plus the four affected `scripts/__tests__/` files — **143 files, 1806 tests, all passing** after the one collateral repair below (first run caught it: 1 failed / 1805 passed). - `pnpm --filter @object-ui/plugin-dashboard type-check`, `pnpm --filter @object-ui/cli type-check`, `tsc -p tsconfig.test.json` in `@object-ui/app-shell` — all exit 0, each on a tree whose dependency closure was built first (an unbuilt closure answers `TS2307` for every workspace import, which is a missing prerequisite and not a verdict). - Gates exit 0: `check:registry-bare-names`, `check:doc-types`, `check:side-effects-array`, `check:changeset-claims`, `check:changeset-presence`, `check:changeset-no-major`, `check:control-bytes`, `check:new-line-citations`, `check:entry-guard`, `check:prompt-keys`, `check:doc-fences`, `check:doc-example-ids`, `check:component-surface-parity`, `check:test-path-roots`, `check:unreferenced-sources`, `check:pending-changeset-literals`. **Collateral repair, in scope because this branch caused it.** `dashboardAuthoredInputs.test.tsx` looked the `inputs` list up under the `view` namespace. That key now answers the tombstone, which publishes no `inputs` at all — so the objectui#5742 "publishes neither" half would have read an empty list and passed while measuring nothing. Its own `label` / `widgets` controls caught it; the row moved to `plugin-dashboard` rather than being dropped. **Other call sites updated to the new full type**: `DashboardRenderer.domProps.test.tsx` (the canary node and its `data-obj-type` assertion), `widget-dom-leak-sweep.test.tsx` (the sweep target moved under the `plugin-dashboard:` prefix case; its `OWNED_VIEW_ALIASES` entry is now empty, with the reason written down — `view:dashboard` is still a registered key but what answers it destructures and spreads nothing, so there is no pass-through surface for that sweep to measure), and the doc-gate test's table-row pin. ## Not built here, reported instead - `Registry.registerLazy` has **no collision check** while `Registry.register` warns, and this shape came in through that unguarded door. True, and a **different card** — the dispatch is explicit that it is reported and not built. - `packages/types` was deliberately not touched; the tombstone needed nothing there. ## Update after `main` was merged in Re-run at `ecf5fc69f`: **143 test files, 1806 tests, 0 failures**, and `check:registry-bare-names`, `check:doc-types`, `check:side-effects-array`, `check:control-bytes`, `check:changeset-presence`, `check:new-line-citations` and `regenerate-known-schema-types.mjs --check` all exit 0 on that same tree. ⚠️ **`ecf5fc69f` is NOT this PR's head, and every figure under Verification above is measured on `ecf5fc69f` and on nothing later.** An earlier revision of this paragraph called it "final head … the sha every count in Verification above is measured on"; the first half of that has been false since the branch moved. It moved twice: `588e45950f` (the one-line unused-`eslint-disable` deletion described below, plus a merge of `main`) and then `96b6ee2820` (the three items the at-tier contract review listed as owed before merge — the corrected `excluded` sentence in `check-doc-component-types.mjs`, the changeset's two missing declarations, and this body). The 143/1806 figures were **not** re-measured at either, and this body does not claim they were. What stands for head is CI, plus the gates re-run per commit: at `96b6ee2820`, `check:doc-types`, `check:registry-bare-names`, `check:changeset-claims`, `check:changeset-presence`, `check:changeset-no-major`, `check:pending-changeset-literals`, `check:control-bytes`, `check:new-line-citations`, `check:doc-fences`, `check:doc-example-ids`, `check:comment-mask-corpus`, `check:shell-escape-residue`, `check:unreferenced-sources` and `regenerate-known-schema-types.mjs --check` each exit 0, `pnpm exec eslint scripts/check-doc-component-types.mjs --format json` judges 1 file with 0 errors and 0 warnings, and `pnpm exec vitest run scripts/__tests__/check-doc-component-types.test.ts scripts/__tests__/known-schema-types-derivation-5115.test.ts packages/cli/src/__tests__/retired-view-dashboard-key-9533.test.ts` reports 3 files / 136 tests passed. The `excluded` correction was measured before it was written: dropping that line yields `stale-indirect-registration` (both commands exit 1, nothing written), not the re-admission the sentence used to claim. Two gates that need a BUILT tree were also run once the closures were built: `check:sdui-registration-pins` exits 0 (16 promised registrations present across 2309 weighed chunks, the 3 ruled controls in the derived set) and `check:eager-closure` exits 0, both against a freshly built `apps/console`. `check:doc-snippets`, `check:doc-examples` and `check:readme-exports` remain **NOT MEASURED**: each exits on a missing prerequisite (7 packages outside the closures this branch needed are still unbuilt), and none of their unjudgeable entries names a file this branch edits. ⚠️ **Corrected by the claiming seat after CI went red — the reading this paragraph used to carry could not have failed.** An earlier revision reported `pnpm exec eslint . --no-inline-config --format json` judging **5079 files** with **0 errors**. That reading was blind to the very error class that turned CI red: `--no-inline-config` makes eslint skip inline configuration comments entirely, so an **unused** `eslint-disable` directive cannot be reported at all. The flag did not add noise — it removed a rule's ability to fire — and it is the sister repository's spelling; this repo's `lint` script takes no flags. CI's `Lint` job failed on exactly that: one error, `Unused eslint-disable directive (no problems were reported from 'no-console')` at `packages/plugin-dashboard/src/retired-node-types.tsx:72`. The directive is removed — one deletion, nothing else; no `console` call added back and no flag added anywhere. **The honest reading, taken with the instrument CI actually runs:** `pnpm --filter @object-ui/plugin-dashboard lint` exits **0** with `501 problems (0 errors, 501 warnings)` — CI's 502 minus the one error — and the full `pnpm lint` (`turbo run lint`, 47 tasks) exits **0**, every package's summary line reading `0 errors`, `//:lint:root` included, with `Unused eslint-disable` appearing nowhere in the tree. The warning split below stands, measured per file at base versus head: **17 pre-existing, 5 new**, every new one `react-refresh/only-export-components` — 2 from `index.tsx`'s new re-export block and 3 from `retired-node-types.tsx`, which exports a component beside its table exactly as `@object-ui/fields` does for `RetiredFieldTombstone`. No package's `lint` script sets `--max-warnings`. **One repair this branch owed itself.** `check-doc-component-types.mjs`'s header ran its objectui#5106 namespace-blindness example over the literal pair `view:dashboard` / `plugin-dashboard:dashboard` and told the reader to flip `namespace: 'view'` on the dashboard registration. This branch made that false — there is no `namespace: 'view'` on that registration any more — so the example is re-pointed with a note saying what moved. Not opportunistic tidying: a claim this diff falsified, in a file this diff already edits. Seat session, in prose so it survives an edit: `session_012EpHzwH4wTy5sd7ibkD2yq`. `needs:contract-review` is hung on **both** carriers — the claiming seat hung the PR-side half after this PR was opened, since that half is the PM's — and `check-clause2-carriers.mjs --pair 9803` now exits **0**. Arranging the clause-② review belongs to the claiming seat; ⛔ this branch does not wait on it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_012EpHzwH4wTy5sd7ibkD2yq --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent cbf5cdf commit e356c39

15 files changed

Lines changed: 853 additions & 43 deletions
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
---
2+
'@object-ui/plugin-dashboard': minor
3+
'@object-ui/cli': minor
4+
---
5+
6+
Converge the bare `dashboard` key on `plugin-dashboard`, and retire
7+
`view:dashboard` with a by-name tombstone (objectui#9533).
8+
9+
**Breaking for authored metadata.** `{ "type": "view:dashboard" }` no longer
10+
renders a dashboard. It now resolves to a refusal that names the spelling and
11+
names its replacement, and `objectui check` reports it as an unknown schema
12+
type. `{ "type": "dashboard" }` and `{ "type": "plugin-dashboard:dashboard" }`
13+
both render `DashboardRenderer`, unchanged and newly-working respectively.
14+
15+
**What was wrong.** `apps/console` declares the lazy stub for bare `dashboard`
16+
under `plugin-dashboard` — twice, in `preview-gallery.tsx` and
17+
`register-plugins.ts` — while this package registered the renderer as
18+
`view:dashboard`. Neither site passed `skipFallback`, so both also claimed the
19+
bare key (`Registry.register` and `Registry.registerLazy` share the
20+
`meta?.namespace && !meta?.skipFallback` branch, and `registerLazy` has no
21+
collision check at all, so nothing warned). Two consequences followed, and both
22+
were reproduced against a real `Registry` before anything here was written:
23+
24+
1. **The bare key was double-claimed, phase-dependently.** After the stub step
25+
bare `dashboard` declared namespace `plugin-dashboard`; after the chunk
26+
loaded the same key declared `view`. Which answer a host got depended on when
27+
it asked — the objectui#6416 shape, which converged `plugin-report` off it.
28+
2. **The namespaced stub was never cleared.** `register()` clears the lazy stub
29+
of the type IT registers, and that type was `view:dashboard`. So
30+
`hasLazy('dashboard', 'plugin-dashboard')` read `true` after the package had
31+
fully loaded and `get('dashboard', 'plugin-dashboard')` read `undefined`,
32+
while the generated CLI whitelist listed that spelling as renderable. A node
33+
authored with it could only ever paint `Loading plugin-dashboard:dashboard…`
34+
forever: `Registry.loadLazy` resolves whether or not the loaded module
35+
registered the expected type, and `SchemaRenderer` re-checks `hasLazy` on
36+
every pass. That is the objectui#8760 shape — a key that passes every
37+
authoring check and fails only in front of a user.
38+
39+
**What changed.** The renderer registers as `plugin-dashboard:dashboard`, the
40+
namespace every sibling plugin, both console stubs and the CLI whitelist already
41+
use, so the console stubs are cleared on load and all three claimants of the bare
42+
key name ONE full type. The retired `view:dashboard` key answers
43+
`RetiredDashboardNodeTombstone` — an inline refusal, plus a `console.error`
44+
carrying the same text — registered with `skipFallback: true` so it claims no
45+
bare key. Its spelling is withheld from the derived key universe by declaration
46+
in `scripts/check-doc-component-types.mjs`, the same disposition the
47+
`RETIRED_FIELD_TYPES` tombstones take, so `objectui check` names it rather than
48+
blessing it; `packages/cli/src/utils/known-schema-types.ts` regenerates and loses
49+
that one entry.
50+
51+
**Also published: four new symbols on the package entry.** `src/index.tsx`
52+
re-exports `RETIRED_DASHBOARD_NODE_TYPES`, `RetiredDashboardNodeTombstone`,
53+
`reportRetiredDashboardNodeType` and `resetRetiredDashboardNodeTypeReports` from
54+
`./retired-node-types`, and `exports["."]` is what publishes them — so these are
55+
**published** surface, not an internal one, and removing or renaming one later is
56+
a breaking change like any other export. They exist for the same reason
57+
`@object-ui/fields` publishes its tombstone: the pin has to import the refusal
58+
text it asserts rather than restate it.
59+
60+
**Also visible in the DOM: the `data-obj-type` value on a dashboard grid.** That
61+
attribute carries the type the node was AUTHORED with, so the spelling that puts
62+
a dashboard grid on the page moves with the registration.
63+
`[data-obj-type="view:dashboard"]` no longer selects one — that node now renders
64+
the tombstone, which publishes `data-retired-node-type` and no `data-obj-type` at
65+
all — while `[data-obj-type="plugin-dashboard:dashboard"]` now does.
66+
`{ "type": "dashboard" }` is unchanged and still reads `dashboard`. A stylesheet,
67+
a selector or a DOM assertion keyed on the retired value stops matching, and
68+
stops matching silently, so it is declared here rather than left to be found.
69+
70+
**Ruled, not chosen.** Director seat summon #24, batch #152 item 5 letter 1,
71+
maintainer 「其他同意」 2026-09-18. The two rejected routes are on the record:
72+
converging on `view` would leave the whitelist advertising a dead
73+
`plugin-dashboard:dashboard`, and standing one side down resolves the naming
74+
contest but not the unsatisfiable stub.
75+
76+
**`Clause-②: yes`**, declared by that ruling: the published full name moves. Note
77+
the shape, because it is unusual — the accept set **narrows** (an authored
78+
`view:dashboard` stops resolving) and the clause is `yes` anyway, on the
79+
published-name move rather than on a widened surface. `minor` per AGENTS.md's
80+
版本号策略: objectui's own breaking changes take `minor`, with the breaking
81+
semantics written out here.
82+
83+
`packages/plugin-dashboard/src/__tests__/dashboardBareKeyOwnership.test.tsx` is
84+
the half that outlives the fix. It reads the package's real declared metadata
85+
back out of the registry and replays it into a fresh `Registry` in **both**
86+
orders — eager-then-lazy and lazy-then-eager — checking the bare key's declared
87+
namespace after every step, so order- and phase-independence are properties under
88+
test rather than properties of the file it imports. It also asserts `hasLazy` is
89+
false after the load, and asserts the `view:dashboard` refusal **together with**
90+
the tombstone's own migration text, because a pin that asserts only "no dashboard
91+
rendered" passes identically against a registration that was simply deleted —
92+
the one outcome the ruling refuses. `objectui#9264`'s shrink-only bare-name
93+
collision ledger loses its row: it is now empty.

‎content/docs/plugins/plugin-dashboard.mdx‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,13 @@ import '@object-ui/plugin-dashboard';
9090
### What the side-effect import registers
9191

9292
That single import is the whole of registration — there is no components map to
93-
iterate over. Importing the entry runs the eight `ComponentRegistry.register(...)`
93+
iterate over. Importing the entry runs the eight live `ComponentRegistry.register(...)`
9494
calls in `packages/plugin-dashboard/src/index.tsx`, which claim exactly these
9595
schema types. The keys below are read off those calls.
9696

9797
| Namespaced key | Bare-name fallback | Renderer behind it |
9898
| --- | --- | --- |
99-
| `view:dashboard` | `dashboard` | `DashboardRenderer` — the widget container |
99+
| `plugin-dashboard:dashboard` | `dashboard` | `DashboardRenderer` — the widget container |
100100
| `plugin-dashboard:metric` | `metric` | `MetricWidget` — one KPI value |
101101
| `plugin-dashboard:metric-card` | `metric-card` | `MetricCard` — KPI with trend and icon |
102102
| `plugin-dashboard:object-metric` | `object-metric` | internal wrapper around `ObjectMetricWidget` — a metric aggregated over an object |
@@ -107,9 +107,23 @@ schema types. The keys below are read off those calls.
107107

108108
`ComponentRegistry.register` publishes `namespace:type`, and — unless the call
109109
passes `skipFallback: true` — the bare `type` as a back-compat fallback
110-
(`packages/core/src/registry/Registry.ts:194`, fallback branch at `:226`). No
111-
call in this package passes `skipFallback`, so each type above resolves under
112-
both spellings; the schema snippets on this page use the bare one.
110+
(`packages/core/src/registry/Registry.ts:194`, fallback branch at `:226`). Every
111+
call behind the table above leaves `skipFallback` unset, so each type there
112+
resolves under both spellings; the schema snippets on this page use the bare one.
113+
114+
⛔ One further registration is deliberately absent from the table above. Until
115+
objectui#9533 the dashboard renderer was published as `view:dashboard`, while
116+
`apps/console`'s lazy stubs and the CLI's known-type whitelist already spelled it
117+
`plugin-dashboard:dashboard`; the bare `dashboard` key therefore declared one
118+
namespace before the chunk loaded and the other after it, and the
119+
`plugin-dashboard:dashboard` stub was never cleared, so that spelling could never
120+
resolve. The renderer now registers under `plugin-dashboard`, and the retired
121+
`view:dashboard` key is answered by a tombstone widget that refuses BY NAME and
122+
names `plugin-dashboard:dashboard` as its replacement — so an authored
123+
`view:dashboard` gets a visible refusal carrying its own migration, never a
124+
silent fall-through. That tombstone registration passes `skipFallback: true`, so
125+
it claims no bare key, and `view:dashboard` is deliberately NOT a renderable key:
126+
`objectui check` reports it as unknown.
113127

114128
The two `object-*` rows name a wrapper rather than an export on purpose.
115129
`ObjectMetricBlock` and `ObjectPivotBlock` are internal to `src/index.tsx`: each

‎packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@
4949
* - `plugin-chatbot:chatbot` / `chatbot-enhanced` — 14 attributes each,
5050
* objectui#4431 / PR #4485, which also lifted `toDomProps` to
5151
* `@object-ui/core` so later cards consume one executor.
52-
* - `view:dashboard` — `DashboardRenderer`'s widget-grid container, 13
52+
* - `plugin-dashboard:dashboard` (spelled `view:dashboard` until
53+
* objectui#9533) — `DashboardRenderer`'s widget-grid container, 13
5354
* attributes, objectui#4432 / this file's most recent edit.
5455
*
5556
* The three packages now reading 0 are NOT clean for the same reason, and the
@@ -875,7 +876,9 @@ const TARGETS: Readonly<Record<string, readonly Target[]>> = {
875876
{ type: 'plugin-dashboard:object-pivot', schemaExtras: { objectName: 'accounts' }, ready: '[data-testid="pivot-empty-state"]' },
876877
{ type: 'plugin-dashboard:object-data-table', schemaExtras: { objectName: 'accounts' }, ready: '[data-testid="table-empty-state"]' },
877878
// `DashboardRenderer`'s widget grid — the element its `{...props}` lands on.
878-
{ type: 'view:dashboard', ready: '.grid.auto-rows-min' },
879+
// Spelled `view:dashboard` until objectui#9533 converged the bare `dashboard`
880+
// key onto this package's own namespace; the renderer is the same one.
881+
{ type: 'plugin-dashboard:dashboard', ready: '.grid.auto-rows-min' },
879882
],
880883
// objectui#5574 — 159 targets, built above rather than spelled here because
881884
// 140 of them need nothing but the shared readiness class.
@@ -916,7 +919,16 @@ const OWNED_VIEW_ALIASES: Readonly<Record<string, readonly string[]>> = {
916919
'plugin-charts': ['view:chart'],
917920
'plugin-calendar': ['view:calendar'],
918921
'plugin-chatbot': [],
919-
'plugin-dashboard': ['view:dashboard'],
922+
// objectui#9533 — EMPTY, and measured rather than defaulted. This package's
923+
// dashboard renderer moved out of the shared `view:` namespace onto
924+
// `plugin-dashboard:dashboard`, so it is covered by the prefix case above.
925+
// `view:dashboard` is still a registered key, but what answers it is a
926+
// retirement TOMBSTONE (a refusal alert naming the migration), not a widget:
927+
// it destructures nothing and spreads nothing onto its element, so there is no
928+
// pass-through surface for this sweep to measure. Its contract — the refusal
929+
// and the text of it — is pinned in
930+
// `packages/plugin-dashboard/src/__tests__/dashboardBareKeyOwnership.test.tsx`.
931+
'plugin-dashboard': [],
920932
};
921933

922934
/* ════════════════════════════════════════════════════════════════════════════
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* `objectui check` on the two spellings objectui#9533 moved — the
11+
* AUTHORING-TIME half of that convergence, on the published surface.
12+
*
13+
* ## Why this pin and not only the derivation's
14+
*
15+
* `scripts/__tests__/known-schema-types-derivation-5115.test.ts` holds the
16+
* generated snapshot against the derivation, which is the MECHANISM. This holds
17+
* the outcome an author observes from outside the repository, in both
18+
* directions, because objectui#9533 moved a key in each:
19+
*
20+
* - `plugin-dashboard:dashboard` was WHITELISTED AND UNRENDERABLE. The console
21+
* declared the lazy stub under that name while the package registered
22+
* `view:dashboard`, so `register()` never cleared the stub and nothing was
23+
* ever stored under it. This command was silent on a document that could
24+
* only ever paint `Loading plugin-dashboard:dashboard…` — the objectui#8760
25+
* shape, whose grading is the line this pin sits on: "an unknown key is
26+
* refused loudly at authoring time; a registered-but-unfulfilled key passes
27+
* every check, is taught by the documentation, and fails only at render in
28+
* front of a user."
29+
* - `view:dashboard` was the package's published full name and is now RETIRED.
30+
* It stays registered at runtime — a tombstone that refuses by name and
31+
* names its replacement — but its keys are WITHHELD from the derived
32+
* universe by declaration (`INDIRECT_REGISTRATIONS` in
33+
* `scripts/check-doc-component-types.mjs`, the disposition objectui#9717
34+
* holds open), precisely so that this command NAMES it instead of blessing
35+
* it. Blessing it here would rebuild the defect the card removed, one
36+
* spelling over.
37+
*
38+
* ## The controls
39+
*
40+
* Silence is one of the two readings under test, so the silent cases must be
41+
* individually non-zero — a command that judged nothing at all would pass them.
42+
* Bare `dashboard` and `plugin-dashboard:dashboard` are each asserted to be IN
43+
* the shipped vocabulary before their documents are checked.
44+
*
45+
* Fixtures live under `os.tmpdir()` and carry the `className` marker, for the
46+
* two reasons `check-known-types.test.ts` states at length: a fixture inside
47+
* this workspace would be scanned by the repo's own `pnpm check`, and a file
48+
* with no ObjectUI marker key is not judged at all (objectui#5127) — which would
49+
* leave the silence assertions green while measuring nothing.
50+
*/
51+
52+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
53+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
54+
import { join } from 'node:path';
55+
import { tmpdir } from 'node:os';
56+
57+
import { check } from '../commands/check.js';
58+
import { KNOWN_SCHEMA_TYPES } from '../utils/known-schema-types.js';
59+
60+
let cwd: string;
61+
let lines: string[];
62+
let restoreLog: () => void;
63+
64+
/** The escape byte chalk opens a CSI sequence with, spelled rather than typed. */
65+
const ESC = String.fromCharCode(27);
66+
const ANSI = new RegExp(`${ESC}\\[[0-9;]*m`, 'g');
67+
68+
function writeSchema(name: string, type: string): void {
69+
writeFileSync(join(cwd, name), JSON.stringify({ className: 'p-0', type, widgets: [] }));
70+
}
71+
72+
function unknownTypeWarnings(): string[] {
73+
return lines.map((l) => l.replace(ANSI, '')).filter((l) => l.includes('Unknown schema type'));
74+
}
75+
76+
beforeEach(() => {
77+
cwd = mkdtempSync(join(tmpdir(), 'objectui-check-9533-'));
78+
lines = [];
79+
const original = console.log;
80+
console.log = (...args: unknown[]) => {
81+
lines.push(args.map(String).join(' '));
82+
};
83+
restoreLog = () => {
84+
console.log = original;
85+
};
86+
});
87+
88+
afterEach(() => {
89+
restoreLog();
90+
rmSync(cwd, { recursive: true, force: true });
91+
});
92+
93+
describe('objectui#9533 — the retired `view:dashboard` spelling is named, not blessed', () => {
94+
it('`objectui check` reports it by name', async () => {
95+
writeSchema('retired.json', 'view:dashboard');
96+
await check(cwd);
97+
expect(unknownTypeWarnings()).toEqual([
98+
expect.stringContaining('Unknown schema type "view:dashboard" in retired.json'),
99+
]);
100+
});
101+
102+
it('it is gone from the shipped vocabulary', () => {
103+
// Non-vacuity for the assertion beside it: the snapshot must be a real,
104+
// populated set, or "does not contain" is true of everything.
105+
expect(KNOWN_SCHEMA_TYPES.length).toBeGreaterThan(100);
106+
expect(KNOWN_SCHEMA_TYPES).not.toContain('view:dashboard');
107+
});
108+
});
109+
110+
describe('objectui#9533 — CONTROL: the spellings that now really resolve stay silent', () => {
111+
it.each(['dashboard', 'plugin-dashboard:dashboard'])(
112+
'`%s` is in the vocabulary and draws no warning',
113+
async (type) => {
114+
// Individually non-zero: each names a key the plugin really registers.
115+
// `plugin-dashboard:dashboard` was in this list BEFORE the fix too — what
116+
// changed is that it now names a component rather than a stub nothing ever
117+
// fulfilled, which is why the runtime half is pinned next to the plugin
118+
// (`packages/plugin-dashboard/src/__tests__/dashboardBareKeyOwnership.test.tsx`)
119+
// and not here.
120+
expect(KNOWN_SCHEMA_TYPES).toContain(type);
121+
writeSchema('live.json', type);
122+
await check(cwd);
123+
expect(unknownTypeWarnings()).toEqual([]);
124+
},
125+
);
126+
});

‎packages/cli/src/utils/known-schema-types.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,6 @@ export const KNOWN_SCHEMA_TYPES: readonly string[] = [
646646
'view-switcher',
647647
'view:calendar',
648648
'view:chart',
649-
'view:dashboard',
650649
'view:detail',
651650
'view:drawer',
652651
'view:filter-ui',

‎packages/plugin-dashboard/README.md‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,13 @@ const schema = {
6666
### What the side-effect import registers
6767

6868
That single import is the whole of registration — there is no components map to
69-
iterate over. Importing the entry runs the eight `ComponentRegistry.register(...)`
69+
iterate over. Importing the entry runs the eight live `ComponentRegistry.register(...)`
7070
calls in `src/index.tsx`, which claim exactly these schema types. The keys below
7171
are read off those calls:
7272

7373
| Namespaced key | Bare-name fallback | Renderer behind it |
7474
| --- | --- | --- |
75-
| `view:dashboard` | `dashboard` | `DashboardRenderer` — the widget container |
75+
| `plugin-dashboard:dashboard` | `dashboard` | `DashboardRenderer` — the widget container |
7676
| `plugin-dashboard:metric` | `metric` | `MetricWidget` — one KPI value |
7777
| `plugin-dashboard:metric-card` | `metric-card` | `MetricCard` — KPI with trend and icon |
7878
| `plugin-dashboard:object-metric` | `object-metric` | internal wrapper around `ObjectMetricWidget` — aggregates over an object |
@@ -83,13 +83,27 @@ are read off those calls:
8383

8484
`ComponentRegistry.register` publishes `namespace:type`, and — unless the call
8585
passes `skipFallback: true` — the bare `type` as a back-compat fallback
86-
(`packages/core/src/registry/Registry.ts:194`, fallback branch at `:226`). No
87-
call in this package passes `skipFallback`, so each type above resolves under
88-
both spellings. The two `object-*` types are served by internal wrappers that
86+
(`packages/core/src/registry/Registry.ts:194`, fallback branch at `:226`). Every
87+
call behind the table above leaves `skipFallback` unset, so each type there
88+
resolves under both spellings. The two `object-*` types are served by internal wrappers that
8989
first resolve the spec's per-element `dataSource` binding (through
9090
`ElementDataSourceGate` from `@object-ui/react`) and then render the exported
9191
component, which is why those rows name a wrapper rather than an export.
9292

93+
⛔ One further registration is deliberately absent from the table above. Until
94+
objectui#9533 the dashboard renderer was published as `view:dashboard`, while
95+
`apps/console`'s lazy stubs and the CLI's known-type whitelist already spelled it
96+
`plugin-dashboard:dashboard`; the bare `dashboard` key therefore declared one
97+
namespace before the chunk loaded and the other after it, and the
98+
`plugin-dashboard:dashboard` stub was never cleared, so that spelling could never
99+
resolve. The renderer now registers under `plugin-dashboard`, and the retired
100+
`view:dashboard` key is answered by a tombstone widget that refuses BY NAME and
101+
names `plugin-dashboard:dashboard` as its replacement — so an authored
102+
`view:dashboard` gets a visible refusal carrying its own migration, never a
103+
silent fall-through. That tombstone registration passes `skipFallback: true`, so
104+
it claims no bare key, and `view:dashboard` is deliberately NOT a renderable key:
105+
`objectui check` reports it as unknown.
106+
93107
### Registering a component under your own key
94108

95109
To serve one of this package's components under a key of your own, register the

0 commit comments

Comments
 (0)