Skip to content

Commit 399e3aa

Browse files
fix(examples,spec): dashboard widget options census — examples drop the options keys no dataset-bound renderer reads, and the schema doc states the read set (#21130)
Fixes #20960 Clause-②: no — the census found no key that has both a writer and a reader, so nothing is declared and no value is typed. The accept set of `DashboardWidgetOptionsSchema` is unchanged. The claim declared `yes` with the arm left to the census. The line above is the measured arm. ## What the census found No key in the census has both a writer and a reader. Every key an example writes is read by no renderer, and the one key a renderer reads (`description`) has no authored writer. So the card's rule gives these verdicts: - written but unread: each key is **removed from its producer**; - read but unwritten: **nothing is declared**. This PR therefore contains no declarations. It has three parts: - the producer removals in two example apps; - a correction to the `DashboardWidgetOptionsSchema` doc comment, which said the renderer "understands" presentation extras that no renderer reads; - a `patch` changeset, because that doc comment ships in `dist/ui/index.d.ts`. **Landing site.** This is the expected site from the dispatch, minus the declarations: the producers under `examples/**` and the `DashboardWidgetOptionsSchema` block in `packages/spec/src/ui/dashboard.zod.ts`. ## Census — every key written through the widget's open shapes How it was measured: - **Writers** were read at objectstack `2821e9f15`. The population is every dashboard producer: `examples/**` (app-crm, app-todo, and app-showcase's three dashboards), the packaged templates (`create-objectstack` blank, `os generate`'s dashboard scaffold, Studio's create seed in `metadata-create-seeds.ts`), and the platform package `platform-objects` (`system_overview`). - **Readers** were read in objectui `packages/plugin-dashboard/src/DatasetWidget.tsx` at objectui `main` `5262f7dd`. `DashboardRenderer` and `DashboardGridLayout` both route any widget that has a `dataset` to `DatasetWidget`, and the spec requires `dataset`. The objectui pin `e420df310` reads the same set (`:492`-`:501`, `:1065`, `:1694` there). - **Open shapes:** walked at runtime from the built `DashboardWidgetSchema`, the widget has exactly one open catchall: `options`. The widget itself, `layout`, `compareTo` and `chartConfig` are closed. The four maps it reaches are the `title` locale map, `chartConfig.colors`, `filter` (filter grammar keyed by field name) and `filterBindings` (filter name to field). None of the four is a renderer-extras bag. | Key | Writer | Reader on the dataset-bound path | Verdict | | :-- | :-- | :-- | :-- | | `format` | app-crm `pipeline.dashboard.ts` L46, L64, L82 (`'currency'`) | None. The metric tile formats with the dataset measure's own metadata: `formatMeasure(value, f?.format, f?.currency, …)` at `:1126`, where `f = measureField(values[0])` at `:1043`. `opportunity.dataset.ts` already declares `format: '0,0', currency: 'USD'` on `total_amount` and `avg_amount`. | Written, unread. Removed from the producer. | | `currency` | app-crm, same three widgets (`'USD'`) | None (same read as `format`) | Written, unread. Removed. | | `color` | app-todo, four metric tiles (hex colours) | None. A tile's accent is `widget.colorVariant` (`metricAccentTextClass(widget?.colorVariant)` at `:1071`). | Written, unread. Removed. | | `suffix` | app-todo `completion_rate` (`'%'`) | None | Written, unread. Removed. | | `showLegend` | app-todo pie and donut (`true`) | None. The legend comes from `widget.chartConfig` (`chartConfigPresentation(widget?.chartConfig, …)` at `:1800`), where `showLegend` defaults to `true`. | Written, unread. Removed. | | `horizontal` | app-todo bar (`true`) | None. The declared spelling is `type: 'horizontal-bar'`. | Written, unread. Removed. | | `showDataLabels` | app-todo line (`true`) | None. The declared home is `chartConfig.showDataLabels`. | Written, unread. Removed. | | `description` | No authored writer. The single-line grep and every dashboard file read by hand both found none, and the grep's control hit is `content/docs/ui/dashboards.mdx`. `translateDashboard` (`system/i18n-resolver.ts`) only overlays the key from a `subCaption` bundle entry, and this repo ships no such entry. | Metric sub-caption: `pickLocalized(options.description, …)` at `:1118` | Read, unwritten. Not declared. The seat note (5925333725) records objectui#11228 ruling C, which keeps this key refused by name as an inline-dialect key, so dropping the read is objectui's follow-up. | The declared keys (`dateGranularity`, `sortBy`, `sortOrder`, `limit`, `stageOrder`) are unchanged. `DatasetWidget` reads them at `:541`-`:551` and `:1785`. The one place outside the example apps that still writes an extra is the published skill `skills/objectstack-ui/rules/dashboards.md:185`, which writes `options: { icon: 'DollarSign' }`. That file is teaching text on a Tier H governed surface, so it is reported for the seat and not edited here (Acceptance notes). A reader for most of these keys exists only on objectui's legacy inline arm. There, `DashboardRenderer.getComponentSchema` spreads `...options` into component props, and for example `MetricWidget` reads `format`, `currency` and `suffix`. A widget that has a `dataset` never reaches that arm. That arm is the inline dialect objectui#11228 ruled C, which is out of scope here. ## Mechanism hypotheses, measured - **H1 (census first)** — held. The census table is above. - **H2 (typing follows the reader)** — moot, because `currency` has no reader to type from. A finding from the search: `packages/spec` has no shared ISO 4217 code schema. `z.string().length(3)` is spelled three times (`data/field.zod.ts:485`, `:509` and `ui/dataset.zod.ts:288`), and the `iso_4217_currency` value domain (`data/currency-fraction-digits.ts`) is a separate membership list. - **H3 (what typing breaks)** — zero. No value is typed, so nothing the catchall admitted is now refused. `node scripts/check-adr-0087-registration.mjs --base origin/main` exits 0 on a `patch` changeset that declares `no`. - **H4 (strict face)** — measured with a scratch replica of objectui `strict-authoring-face.ts`'s walker (`5262f7dd`). The replica gives every reachable object `catchall: z.never()` and walks lazy, union, array, tuple, record, set, map, intersection, pipe and the optional family. It was applied to the built spec `DashboardSchema`. This is a faithful reading of the walker, not objectui's own face: ``` control-lit tolerant=FAIL strict=FAIL(3) unrecognized_keys @ widgets.0.options keys=["zzProbe"] (+2 id-length issues of the fixture) crm-before tolerant=PASS strict=FAIL(3) unrecognized_keys @ widgets.{0,1,2}.options keys=["format","currency"] crm-after tolerant=PASS strict=PASS todo-before tolerant=PASS strict=FAIL(8) color x4, suffix, showLegend x2, horizontal, showDataLabels todo-after tolerant=PASS strict=PASS ``` app-crm's `pipeline.dashboard.ts` now carries no `options` at all, so it passes the strict face without any objectui follow-up. The run link the card's acceptance asks for is the closer's to add, from objectui's own strict-face run over this example. ## Changes - `examples/app-crm/src/dashboards/pipeline.dashboard.ts` — drop `options: { format: 'currency', currency: 'USD' }` from the three metric tiles, with a comment pointing at the dataset measures that carry the currency. - `examples/app-todo/src/dashboards/task.dashboard.ts` — drop `options` from all eight widgets, with a comment naming the declared homes (`colorVariant`, `chartConfig`, the measure's `format`). - `packages/spec/src/ui/dashboard.zod.ts` — the `DashboardWidgetOptionsSchema` doc comment, and only that comment. It no longer says the renderer "understands `icon`, `trend`, `columns`, `striped`, `density`". It states: - the measured read set: the five declared keys, plus `description` as read but unwritten; - the rule that a key is declared only with both a writer and a dataset-bound read site; - the declared homes for presentation. `.describe()` is unchanged. The generated reference page shows only the describe text, and `check:docs` is green. - `.changeset/20960-widget-options-census.md` — a `patch` for `@objectstack/spec`. The doc comment ships in `dist/ui/index.d.ts`; this was checked by grepping the built `.d.ts`. The examples are `private: true` and ship nothing. ## Verification (head `9efd8ffa5`) - **Spec build:** `pnpm --filter @objectstack/spec build` exits 0. `pnpm --filter @objectstack/spec check:generated` prints `✓ All 15 generated artifacts are up to date`. - **Gate derivation:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, with no paths, derived 80 commands from the four changed files. All 80 were run. Reconciliation with `--ran` prints `80 derived famil(ies) accounted for — 79 run, 1 NOT-MEASURED`. - NOT MEASURED: `check:dual-build-cjs-loads` exits 3 with `PREREQUISITE NOT MET` because it needs the whole workspace built (86 package dists). CI's Build Core runs it. - **Gate verdict lines:** - `check:widget-option-census`: `OK check-widget-option-census: 5 key(s) declared by DashboardWidgetOptionsSchema are all in CONSUMED_WIDGET_OPTION_KEYS (6 member(s)); 1 non-declared member(s) each still witnessed.` - `check-adr-0087-registration --base origin/main`, `check-changeset-no-major --base origin/main`, `check-empty-changeset --base origin/main`, `check:api-surface`, `check:docs`, `check:spec-docblock-symbol-anchors`, `check:objectui-pin-citations`, `check:issue-citations`, `check:doc-authoring` and `check:nul-bytes` all exit 0. - **eslint, narrowed:** run on the three touched TypeScript files with `--no-inline-config --format json`. It reported 3 files, 0 errors and 0 warnings. `--print-config` resolves a config for each file, so none is ignored. The narrowing excludes nothing: `eslint.config.mjs` enables no type-aware linting (no `parserOptions.project`, no typed rules, as stated at `:327`), so this diff cannot move the verdict on any untouched file. - **Tests:** shown in the table below. | Package | Command | Result | Tree | | :-- | :-- | :-- | :-- | | `@objectstack/spec` | `test` | 588 files, 17239 tests passed | `ef4a1f5a7` | | `@objectstack/spec` | `typecheck` (tsc, scripts, test-typecheck) | OK | `ef4a1f5a7` | | `example-crm` | `test` + `typecheck` | 45 tests passed; typecheck Done | `9efd8ffa5` | | `example-todo` | `test` + `typecheck` | 238 tests passed; typecheck Done | `9efd8ffa5` | | `@objectstack/sdui-parser` | `test` | 217 passed | `ef4a1f5a7` | | `@objectstack/service-analytics` | `test` | 3498 passed, 10 skipped | `ef4a1f5a7` | | `@objectstack/platform-objects` | `test` | 948 passed | `ef4a1f5a7` | | `@objectstack/lint` | `test` | 5452 passed | `ef4a1f5a7` | | `@objectstack/metadata-protocol` | `test` | 2921 passed, 19 skipped | `ef4a1f5a7` | | `@objectstack/objectql` | `test` | 6874 passed | `ef4a1f5a7` | | `@objectstack/metadata` | `test` | 836 passed | `ef4a1f5a7` | | `@objectstack/cli` | `vitest run --project unit`, all 4 shards | 1005 + 843 + 778 + 793 passed (60 files each) | shard 1 at `ef4a1f5a7`, shards 2–4 at `9efd8ffa5` | Notes on the test table: - `ef4a1f5a7` is this branch's change on `2821e9f15`, before `main` was merged. - The consumer packages were run in the upstream-dependency direction, with each package's dependency closure built through `turbo run build --filter=PKG...`. - After the merge, only the spec doc comment changed on this branch, so their CI rerun is the post-merge reading. - The CLI integration tier is declared to CI. ## Acceptance notes (observations, not filed) - **Teaching surfaces** still describe unread `options` extras as working: - `content/docs/ui/dashboards.mdx:138` says "keys the renderer understands (`icon`, `columns`, `striped`, `density`, …) pass through untouched". - `skills/objectstack-ui/rules/dashboards.md:185` and `:341` write `options: { icon: 'DollarSign' }` and say "presentation extras … pass through untouched". The skill is Tier H, so neither file is edited here. Both go to the seat with the report. - **Silent at validate:** in this repo, nothing runs `checkDashboardWidgetOptions` over dashboard metadata. Only SDUI page dashboard nodes reach it (`sdui-parser/src/validate.ts:318`). So a `*.dashboard.ts` carrying an unread `options` key is silent at `os validate`, which is how these 11 widgets carried them. This goes to the seat with the report. - **Mislabelled tile:** the app-todo `completion_rate` tile is titled "Completion Rate" but renders `task_count`. Its removed `suffix: '%'` never rendered. - **Not migrated:** the removed app-todo keys have declared homes, but those were not used because they would change what the example renders: `chartConfig.showDataLabels` for the line chart, and `type: 'horizontal-bar'` for the bar chart. A hex `color` has no faithful `colorVariant` mapping. - **Stale comment:** app-crm `pipeline.dashboard.ts`'s header comment still describes `categoryGranularity: 'month'` bucketing on the trend widget. That key is retired, and the dataset dimension carries `dateGranularity: 'month'`. - **`.describe()` kept:** the schema's `.describe('Widget configuration — declared query keys + open renderer extras')` is quoted in `@objectstack/sdui-parser`'s lockstep header and in objectui's copy, so rewording it is a cross-repo lockstep edit. It was left unchanged. --- _Generated by [Claude Code](https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 39ab294 commit 399e3aa

4 files changed

Lines changed: 53 additions & 20 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): the `DashboardWidgetOptionsSchema` doc comment states which widget `options` keys a renderer reads, instead of naming presentation extras (`icon`, `trend`, `columns`, `striped`, `density`) it called renderer-understood
6+
7+
Clause-②: no — no key is declared and no value is typed, so the accept set is unchanged.
8+
9+
`options` still parses any key. A widget always binds a `dataset`, so it renders through
10+
objectui's dataset-bound path, and that path reads only the five declared keys
11+
(`dateGranularity`, `sortBy`, `sortOrder`, `limit`, `stageOrder`) and the `description`
12+
sub-caption. Any other key parses and renders nothing. To format a number, set `format` and
13+
`currency` on the dataset measure. To accent a tile, set the widget's `colorVariant`. To style
14+
a chart, set the widget's `chartConfig`.

‎examples/app-crm/src/dashboards/pipeline.dashboard.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ export const PipelineDashboard: Dashboard = {
3535
columns: 12,
3636
widgets: [
3737
// --- Row 1: KPI tiles -------------------------------------------------
38+
// No `options` on these tiles: the currency face comes from the dataset
39+
// measure (`total_amount` / `avg_amount` declare `format` + `currency` in
40+
// `opportunity.dataset.ts`). A dataset-bound widget reads only the `options`
41+
// keys the spec declares (plus the `description` sub-caption), so an
42+
// `options.format` / `options.currency` here was inert.
3843
{
3944
id: 'total_pipeline',
4045
type: 'metric',
@@ -43,7 +48,6 @@ export const PipelineDashboard: Dashboard = {
4348
filter: { stage: { $nin: ['closed_won', 'closed_lost'] } },
4449
dataset: 'opportunity_metrics',
4550
values: ['total_amount'],
46-
options: { format: 'currency', currency: 'USD' },
4751
layout: { x: 0, y: 0, w: 4, h: 2 },
4852
},
4953
{
@@ -61,7 +65,6 @@ export const PipelineDashboard: Dashboard = {
6165
compareTo: { kind: 'previousPeriod' },
6266
dataset: 'opportunity_metrics',
6367
values: ['total_amount'],
64-
options: { format: 'currency', currency: 'USD' },
6568
layout: { x: 4, y: 0, w: 4, h: 2 },
6669
},
6770
{
@@ -79,7 +82,6 @@ export const PipelineDashboard: Dashboard = {
7982
compareTo: { kind: 'previousYear' },
8083
dataset: 'opportunity_metrics',
8184
values: ['avg_amount'],
82-
options: { format: 'currency', currency: 'USD' },
8385
layout: { x: 8, y: 0, w: 4, h: 2 },
8486
},
8587

‎examples/app-todo/src/dashboards/task.dashboard.ts‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ export const TaskDashboard: Dashboard = {
1717
label: 'Task Overview',
1818
description: 'Key task metrics and productivity overview',
1919

20+
// No widget carries `options`: a dataset-bound widget reads only the keys the
21+
// spec declares there (`dateGranularity`, `sortBy`, `sortOrder`, `limit`,
22+
// `stageOrder`) plus the `description` sub-caption, so a presentation key in
23+
// the bag renders nothing. A tile accent is `colorVariant`, a chart's look is
24+
// `chartConfig`, and a number's face is the dataset measure's own `format`.
2025
widgets: [
2126
// Row 1: Key Metrics
2227
{
@@ -26,7 +31,6 @@ export const TaskDashboard: Dashboard = {
2631
dataset: 'task_metrics',
2732
values: ['task_count'],
2833
layout: { x: 0, y: 0, w: 3, h: 2 },
29-
options: { color: '#3B82F6' }
3034
},
3135
{
3236
id: 'completed_today',
@@ -36,7 +40,6 @@ export const TaskDashboard: Dashboard = {
3640
dataset: 'task_metrics',
3741
values: ['task_count'],
3842
layout: { x: 3, y: 0, w: 3, h: 2 },
39-
options: { color: '#10B981' }
4043
},
4144
{
4245
id: 'overdue_tasks',
@@ -46,7 +49,6 @@ export const TaskDashboard: Dashboard = {
4649
dataset: 'task_metrics',
4750
values: ['task_count'],
4851
layout: { x: 6, y: 0, w: 3, h: 2 },
49-
options: { color: '#EF4444' }
5052
},
5153
{
5254
id: 'completion_rate',
@@ -56,7 +58,6 @@ export const TaskDashboard: Dashboard = {
5658
dataset: 'task_metrics',
5759
values: ['task_count'],
5860
layout: { x: 9, y: 0, w: 3, h: 2 },
59-
options: { suffix: '%', color: '#8B5CF6' }
6061
},
6162

6263
// Row 2: Task Distribution
@@ -75,7 +76,6 @@ export const TaskDashboard: Dashboard = {
7576
dimensions: ['status'],
7677
values: ['task_count'],
7778
layout: { x: 0, y: 2, w: 6, h: 4 },
78-
options: { showLegend: true }
7979
},
8080
{
8181
id: 'tasks_by_priority',
@@ -86,7 +86,6 @@ export const TaskDashboard: Dashboard = {
8686
dimensions: ['priority'],
8787
values: ['task_count'],
8888
layout: { x: 6, y: 2, w: 6, h: 4 },
89-
options: { horizontal: true }
9089
},
9190

9291
// Row 3: Trends
@@ -99,7 +98,6 @@ export const TaskDashboard: Dashboard = {
9998
dimensions: ['completed_date'],
10099
values: ['task_count'],
101100
layout: { x: 0, y: 6, w: 8, h: 4 },
102-
options: { showDataLabels: true }
103101
},
104102
{
105103
id: 'tasks_by_category',
@@ -110,7 +108,6 @@ export const TaskDashboard: Dashboard = {
110108
dimensions: ['category'],
111109
values: ['task_count'],
112110
layout: { x: 8, y: 6, w: 4, h: 4 },
113-
options: { showLegend: true }
114111
},
115112

116113
// The former Row 4 count-only `table` widgets (`overdue_tasks_table`,

‎packages/spec/src/ui/dashboard.zod.ts‎

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -214,15 +214,35 @@ const WIDGET_HISTORY =
214214
+ 'shipping inert metadata; a stale or mis-layered key is now a loud parse error.';
215215

216216
/**
217-
* Widget `options` — the renderer-extras escape hatch, with the keys that
218-
* reach the ANALYTICS QUERY declared explicitly (framework#3588).
219-
*
220-
* The bag stays open (`passthrough`): presentation-only settings the renderer
221-
* understands — `icon`, `trend`, `columns`, `striped`, `density`, … — are
222-
* carried through untouched and are none of the spec's business. What the
223-
* declared keys below buy is a real contract for the four that are NOT
224-
* presentation-only: they change the SQL the dataset query compiles to, so a
225-
* typo (`sortDirection`, `granularity`) is now an author-time type error
217+
* Widget `options` — an open bag, with the keys that reach the ANALYTICS QUERY
218+
* declared explicitly (framework#3588).
219+
*
220+
* The bag stays open (`passthrough`), but an undeclared key in it is NOT a
221+
* renderer setting. `dataset` is required, so every widget renders through
222+
* objectui's dataset-bound path (`DatasetWidget`), and that path reads exactly
223+
* the five keys declared below plus `description`, the metric sub-caption.
224+
* Any other key parses and reaches no renderer. The #20960 census measured
225+
* that at objectui `main` `5262f7dd` over every key this repo's examples wrote
226+
* here (`format`, `currency`, `color`, `suffix`, `showLegend`, `horizontal`,
227+
* `showDataLabels`): none is read on that path — only the legacy inline arm,
228+
* which a widget carrying a `dataset` never reaches, spreads the bag into
229+
* component props — so each was removed from its producer rather than
230+
* declared. `description` is the opposite case, read but unwritten: no
231+
* authored source writes it (only `translateDashboard` overlays it, from a
232+
* `subCaption` bundle entry — `system/i18n-resolver.ts`), and objectui#11228
233+
* ruled it out with the inline dialect, so it stays undeclared and dropping
234+
* the read is objectui's follow-up. ⛔ A key is declared here only with both
235+
* a writer and a read site on the dataset-bound path, and it then joins
236+
* `CONSUMED_WIDGET_OPTION_KEYS` in `@objectstack/sdui-parser`
237+
* (`check:widget-option-census`).
238+
*
239+
* Presentation has declared homes instead: a number's face is the dataset
240+
* MEASURE's `format` + `currency`, a tile's accent is the widget's
241+
* `colorVariant`, and a chart's look is the widget's `chartConfig`.
242+
*
243+
* What the declared keys below buy is a real contract for the four that are
244+
* NOT presentation-only: they change the SQL the dataset query compiles to, so
245+
* a typo (`sortDirection`, `granularity`) is now an author-time type error
226246
* instead of an option that reads as if it works and silently does nothing.
227247
*
228248
* The dashboard/report renderer lowers these into the `DatasetSelection` it

0 commit comments

Comments
 (0)