@object-ui/plugin-dashboard 17.6.0 → 17.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +4519 -0
  2. package/README.md +424 -83
  3. package/dist/DashboardConfigPanel.d.ts +1 -1
  4. package/dist/DashboardFilterBar.d.ts.map +1 -1
  5. package/dist/DashboardGridLayout.d.ts +3 -2
  6. package/dist/DashboardGridLayout.d.ts.map +1 -1
  7. package/dist/DashboardRenderer.d.ts +32 -2
  8. package/dist/DashboardRenderer.d.ts.map +1 -1
  9. package/dist/DashboardWithConfig.d.ts +1 -1
  10. package/dist/DashboardWithConfig.d.ts.map +1 -1
  11. package/dist/DatasetWidget.d.ts +42 -14
  12. package/dist/DatasetWidget.d.ts.map +1 -1
  13. package/dist/DrillDownDrawer.d.ts +14 -5
  14. package/dist/DrillDownDrawer.d.ts.map +1 -1
  15. package/dist/MetricCard.d.ts +1 -1
  16. package/dist/MetricWidget.d.ts +27 -2
  17. package/dist/MetricWidget.d.ts.map +1 -1
  18. package/dist/ObjectDataTable.d.ts +407 -18
  19. package/dist/ObjectDataTable.d.ts.map +1 -1
  20. package/dist/ObjectMetricWidget.d.ts +35 -8
  21. package/dist/ObjectMetricWidget.d.ts.map +1 -1
  22. package/dist/ObjectPivotTable.d.ts +46 -7
  23. package/dist/ObjectPivotTable.d.ts.map +1 -1
  24. package/dist/PivotTable.d.ts +20 -3
  25. package/dist/PivotTable.d.ts.map +1 -1
  26. package/dist/RecordDetailDrawer.d.ts +8 -0
  27. package/dist/RecordDetailDrawer.d.ts.map +1 -1
  28. package/dist/WidgetConfigPanel.d.ts +2 -2
  29. package/dist/WidgetConfigPanel.d.ts.map +1 -1
  30. package/dist/WidgetEmptyState.d.ts +27 -0
  31. package/dist/WidgetEmptyState.d.ts.map +1 -0
  32. package/dist/domPassthroughPins.d.ts.map +1 -1
  33. package/dist/index.d.ts +22 -21
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1733 -1570
  36. package/dist/index.umd.cjs +2 -2
  37. package/dist/legacyRetiredWidget.d.ts +5 -3
  38. package/dist/legacyRetiredWidget.d.ts.map +1 -1
  39. package/dist/measureRefusal.d.ts +50 -0
  40. package/dist/measureRefusal.d.ts.map +1 -0
  41. package/dist/recordFields.d.ts +119 -8
  42. package/dist/recordFields.d.ts.map +1 -1
  43. package/dist/retired-node-types.d.ts +29 -0
  44. package/dist/retired-node-types.d.ts.map +1 -0
  45. package/dist/useDashboardAutoRefresh.d.ts +47 -0
  46. package/dist/useDashboardAutoRefresh.d.ts.map +1 -0
  47. package/dist/utils.d.ts +36 -7
  48. package/dist/utils.d.ts.map +1 -1
  49. package/dist/widgetDispatch.d.ts +144 -18
  50. package/dist/widgetDispatch.d.ts.map +1 -1
  51. package/dist/widgetSubCaption.d.ts +40 -0
  52. package/dist/widgetSubCaption.d.ts.map +1 -0
  53. package/package.json +15 -16
package/CHANGELOG.md CHANGED
@@ -1,5 +1,4524 @@
1
1
  # @object-ui/plugin-dashboard
2
2
 
3
+ ## 17.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0651e7a: The grid summary footer and the dashboard metric tile take a currency amount's
8
+ decimal places from the currency, never from `scale`, and the field designer no
9
+ longer offers `Scale` on a currency field (objectui#10221).
10
+
11
+ The currency arm of the grid column-summary footer (`useColumnSummary` in
12
+ `@object-ui/plugin-grid`) and the currency face of `ObjectMetricWidget` in
13
+ `@object-ui/plugin-dashboard` each took the number of decimal places from the
14
+ field's `scale`, with none when `scale` was absent. The currency list cell
15
+ never read `scale`. So a USD column declaring no `scale` totalled
16
+ `1234.5` as `$1,235` under cells reading `$1,234.50`, and a JPY field carrying
17
+ `scale: 4` showed `¥1,234.5000`.
18
+
19
+ Both faces now format an amount in a resolved currency with `formatCurrency`
20
+ from `@object-ui/fields`, the list cell's own formatter, so footer, tile and
21
+ cell agree. The width is the resolved currency's own ISO 4217 minor-unit count: two
22
+ decimals for USD or CNY, none for JPY, three for KWD. A whole amount shows no
23
+ fraction (`$1,234`), as the cell does. When no currency resolves, the amount is
24
+ a plain number with two decimals, or none when it is whole, as the cell renders
25
+ it. Neither `scale` nor
26
+ `precision` is read on a currency. This follows the maintainer ruling recorded
27
+ on objectstack-ai/objectstack#19910 that a currency's decimal places are the
28
+ currency's, not a setting, and the ruling on objectstack-ai/objectstack#19629
29
+ that takes `scale` off the `currency` type. The footer keeps its existing
30
+ tenant-locale fallback for a currency code `Intl` does not accept, and a metric
31
+ tile with an authored `format` pattern still renders that pattern.
32
+
33
+ The field designer's numeric section (`ObjectFieldInspector` in
34
+ `@object-ui/app-shell`) no longer offers a `Scale` control on a `currency`
35
+ field. `number` and `percent` fields keep it. The `Precision` control stays on
36
+ all three: it writes the field-level `precision`, the total digit count of the
37
+ stored decimal, not a number of decimal places.
38
+
39
+ **Behaviour change** for currency amounts on the footer and the tile, filed as
40
+ `minor` because this repo's fixed release group forbids `major`. Where the
41
+ field's `scale` differed from what the cell renders, the output moves:
42
+
43
+ - `scale: 2` on a JPY field used to show `¥1,234.50` and now shows `¥1,235`.
44
+ - A USD field with no `scale` used to show `$1,235` for `1234.5` and now shows
45
+ `$1,234.50`.
46
+ - A whole USD amount under `scale: 2` used to show `$1,234.00` and now shows
47
+ `$1,234`, like the cells.
48
+
49
+ Percent columns and fields still take their width from `scale`.
50
+
51
+ **Migration.** Nothing to restate: a currency amount's decimal places follow its
52
+ currency. Delete `scale` from currency fields. A dashboard that wants a fixed
53
+ pattern on a metric tile declares it with `format`.
54
+
55
+ ⚠️ **Dated note, 2026-10-02 — the whole-amount sentences above were made false in this same release — objectui#11444.**
56
+ The sentences saying a whole amount shows no fraction (`$1,234`, "as the cell does"), that an amount with no currency resolved has no decimals when it is whole, and the bullet saying a whole USD amount under `scale: 2` now shows `$1,234`, no longer hold. Later in this release triage ruled that a currency's declared width, its ISO 4217 minor-unit count, is the protocol's convention, and `formatCurrency` stopped dropping the fraction of a whole amount (objectui#11444, comment 5946462862). The footer and the tile still take `formatCurrency` by reference, so they move with the cell: a whole USD amount reads `$1,234.00`, a whole amount with no currency resolved reads `1,234.00`, and a whole JPY amount still reads `¥1,234`. The rest of this entry is kept as the reading of this change.
57
+ - 6dc82a3: fix(plugin-dashboard): a metric tile counting rows over a currency field shows a plain number, not money
58
+
59
+ `ObjectMetricWidget` decided to dress its number as money from the aggregated
60
+ field's TYPE alone, so `aggregate: { field: 'amount', function: 'count' }` over a
61
+ `currency` field showed `$3` for three records — a number of rows read as an
62
+ amount. The grid footer already reads the same count on a currency column as a
63
+ plain `3`.
64
+
65
+ The field's unit now applies only to an aggregate that answers in it: `sum`,
66
+ `avg`, `min` and `max` keep the currency face exactly as before (the list cell's
67
+ own `formatCurrency`), while `count` and `count_distinct` render as plain,
68
+ locale-formatted numbers. The same rule withholds a `percent` field's pattern
69
+ from a count, so three rows no longer read `3%` either — the footer's rule for a
70
+ percent column.
71
+
72
+ Visible behaviour change for dashboards that count over a currency or percent
73
+ field: the tile loses its currency symbol or percent sign. An authored `format`
74
+ or `currency` on the tile is the author's declaration and is not second-guessed.
75
+ - 37140f4: refactor(plugin-dashboard)!: retire the `dashboard-grid` node type key, and register the `metric` / `metric-card` node keys without a bare fallback (objectui#10859, batch 8 phase 2b)
76
+
77
+ **BREAKING (authoring):**
78
+
79
+ - **`dashboard-grid` is unregistered** (with `plugin-dashboard:dashboard-grid`). It was builder chrome published as a node key: `objectui validate` refused it at `type`, and nothing authored or emitted it. `DashboardGridLayout` stays a named export; mount it directly.
80
+ - **`metric` and `metric-card` register with `skipFallback: true`.** Both dashboard surfaces (`DashboardRenderer`, `DashboardGridLayout`) now hand `SchemaRenderer` `plugin-dashboard:metric` / `plugin-dashboard:metric-card` through one table, `DASHBOARD_NODE_TYPES` in `widgetDispatch.ts`. That covers the static metric tile, the `metric-card` widget-slot entry and a `metric` / `metric-card` node in a widget's legacy `component` envelope. The dashboard WIDGET vocabulary is unchanged: `widgets[].type: 'metric'` and the 2026-08-14 `metric-card` slot entry author exactly as before.
81
+ - **`dashboardComponents`** drops `dashboard-grid`, and its `metric` / `metric-card` entries are keyed `plugin-dashboard:metric` / `plugin-dashboard:metric-card`, the type each now serves, so iterating the map cannot re-create the retired bare keys.
82
+ - A rendered KPI tile's `data-obj-type` reads `plugin-dashboard:metric` (or `plugin-dashboard:metric-card`) instead of the bare spelling.
83
+
84
+ Migration:
85
+
86
+ - `{ "type": "dashboard-grid", "widgets": [ … ] }` → `{ "type": "dashboard", "widgets": [ … ] }`, or mount `DashboardGridLayout` directly for the drag/resize editor.
87
+ - a standalone `{ "type": "metric", … }` node → `{ "type": "plugin-dashboard:metric", … }`; inside a dashboard, keep authoring the `metric` widget.
88
+ - a standalone `{ "type": "metric-card", … }` node → put it in a dashboard's `widgets[]` (the slot entry), or write `plugin-dashboard:metric-card`.
89
+ - `dashboardComponents['metric']` / `dashboardComponents['metric-card']` → `dashboardComponents['plugin-dashboard:metric']` / `dashboardComponents['plugin-dashboard:metric-card']`; `dashboardComponents['dashboard-grid']` → import `DashboardGridLayout`.
90
+ - a selector `[data-obj-type="metric"]` / `[data-obj-type="metric-card"]` → `[data-obj-type="plugin-dashboard:metric"]` / `[data-obj-type="plugin-dashboard:metric-card"]`.
91
+
92
+ **Clause-②: yes** — registrations leave the runtime (narrowing), released as `minor` with this banner.
93
+
94
+ ⚠️ **Dated note, 2026-10-02 — `metric-card` leaves the widget vocabulary — objectui#11483.** At this change, "The dashboard WIDGET vocabulary is unchanged" held, and that vocabulary named `metric-card`. Now the widget `type` vocabulary (`DashboardWidgetTypeSchema` / `DashboardWidgetTypeName`) names no component type. The `metric-card` slot entry is still authored as before when it carries its `value`; without one it is refused. `.changeset/11483-metric-card-needs-value.md` states what ships. The rest of this entry is kept as the reading of this change.
95
+ - cc4e476: `drillDown` is retired on the bare `pivot` node: author `object-pivot` to drill (objectui#10932)
96
+
97
+ **Breaking for authored metadata, graded `minor` by this repo's convention (AGENTS.md: a
98
+ breaking change here is `minor`, never `major`):** a `pivot` node no longer accepts
99
+ `drillDown` on either published face. The TypeScript `PivotTableSchema.drillDown` is a
100
+ `?: never` tombstone, and the `pivot` arm of the zod mirror, the door `objectui validate` /
101
+ `safeValidateSchema` run, refuses the key by name with `invalid_type` at `drillDown`. The
102
+ refusal names the remedy. Migration: delete the key, or, for a pivot whose cells open the
103
+ records behind a value, author an `object-pivot` (`objectName` plus the same `rowField` /
104
+ `columnField` / `valueField`) and put the `drillDown` there. Every other `pivot` member is
105
+ accepted exactly as before.
106
+
107
+ Nothing ever honoured the key on this node. `PivotTable` fired a drill only when its host
108
+ passed an `onDrillDown` handler. The one host that does is `ObjectPivotTable`, which renders
109
+ `object-pivot`, and the `pivot` registration passes none. So an authored `drillDown`
110
+ validated and clicking a cell did nothing, with no signal. A pivot over inline `data` also
111
+ names no object to list the drilled records from. No shipped or example document authors
112
+ the key on a `pivot` node.
113
+
114
+ This supersedes what three pending entries say about the `pivot` arm's `drillDown`: the
115
+ objectui#10859 (batch 2) entry ("`drillDown` is the shared `DrillDownConfigSchema`"), and the
116
+ dated notes on the objectui#7352 and objectui#10685 entries, which read that key as the shared
117
+ mirror (a third referencing declaration, and `mode` accepted there). As of this change the
118
+ arm refuses the key.
119
+
120
+ - `@object-ui/types`: `PivotTableSchema.drillDown` is `?: never`, and the zod `pivot` arm's
121
+ `drillDown` is a `retirementTombstone()`. The content-channel refusal on `body` / `children`
122
+ no longer lists `drillDown` among what a `pivot` renders.
123
+ - `@object-ui/plugin-dashboard`: `PivotTable` reads nothing off `schema` for its drill.
124
+ **Behaviour change for React callers:** its `onDrillDown` prop is now the only switch. A
125
+ caller that passes a handler gets interactive cells, headers and totals, and a
126
+ `schema.drillDown` no longer gates them (the type refuses that key anyway). A caller that
127
+ wants no drill passes no handler. `ObjectPivotTable` already passed its handler only when
128
+ its own `drillDown` was enabled, so `object-pivot` behaves exactly as before, and its
129
+ `schema.drillDown` keeps the `ObjectPivotDrillDownConfig` type.
130
+ - 27ae632: A `record:path` whose stage labels are per-locale maps now shows the viewer's language instead of failing to render, and five label inputs on `object-metric`, `object-grid` and `record:related_list` now accept the per-locale map their renderers already resolve (objectui#10993, batch 2).
131
+
132
+ **`record:path`, `stages[].label`.** `@objectstack/spec` types each stage's `label` as `I18nLabel`: a plain string or an inline per-locale map such as `{ en: 'Draft', 'zh-CN': '草稿' }`. `RecordPathRenderer` read it raw, as the stage's visible text and inside the stage's accessible name, so a map threw "Objects are not valid as a React child" and the node rendered `Component "record:path" failed to render` instead of the path. It now resolves each map with `pickLocalized` against the active UI language (`useObjectTranslation().language`) before the stages are translated, classified or drawn:
133
+
134
+ - Both rows (desktop and mobile) paint the entry for the viewer's language, and each stage's accessible name carries the same text beside its state words, in one language.
135
+ - A stage with no `terminal` is still classified from its label: a lost stage whose label is a map (for example `{ en: 'Closed Lost', 'zh-CN': '丢单' }`) still reads as lost.
136
+ - A plain string renders exactly as authored. A picklist translation of the status field still outranks the authored label, as it did for a string.
137
+
138
+ **The registry inputs.** These inputs declared `type: 'string'` only, while their renderers already resolve a per-locale map, so the manifest built from `ComponentRegistry.getPublicConfigs()` made `validateTree` report `type-mismatch` on a map the spec accepts. Each now declares both arms, `type: ['string', 'object']`, with a description that teaches the map:
139
+
140
+ - `@object-ui/plugin-dashboard`: `object-metric`'s `label` (the tile heading), `description` (the sub-caption under the value) and `title` (the drill-down panel heading), resolved against the active UI language.
141
+ - `@object-ui/plugin-grid`: `object-grid`'s `label` (the table caption, the export title and the record-detail overlay heading), resolved against the display locale: the workspace's regional default when one is configured, otherwise the active UI language. `view:grid` registers the same inputs, so its declared `label` carries the same two arms.
142
+ - `@object-ui/plugin-detail`: `record:related_list`'s `title` (the list heading), resolved against the active UI language.
143
+
144
+ A value that matches neither arm, such as a number, is still reported. No renderer changes for these five: each already resolved the map.
145
+
146
+ **Clause-②: yes** — five published registry inputs widen from `'string'` to `['string', 'object']`, so `validateTree` accepts a locale map on them (and `view:grid`'s `label`, which shares `object-grid`'s inputs, declares the same arms). Nothing that was accepted before is refused now.
147
+ - b28bde8: fix(plugin-dashboard): a served dashboard draws its own title, description and sub-caption, not the packaged catalog's (objectui#11295)
148
+
149
+ A dashboard read from the server's `/meta` route is already translated for the
150
+ request's locale. Since objectstack#20680 the server also keeps a published edit
151
+ over the packaged catalog: an explicit override beats the packaged default.
152
+ `DashboardRenderer` still ran the client bundle over the served strings. It passed
153
+ each served widget title, description and sub-caption to the bundle as the
154
+ fallback, and a bundle entry beats its fallback. So the packaged catalog won again
155
+ on the client, and a published edit drew as the shipped string, in `en` as well as
156
+ `zh-CN`.
157
+
158
+ `DashboardRenderer` gains a `localized` prop. A host that read its document from
159
+ `/meta` sets it, and the console's dashboard page does. With it, these texts are
160
+ drawn as given: the widget `title`, `description` and sub-caption
161
+ (`options.description`), and the renderer's own header `label` and `description`.
162
+ An inline per-locale map is still collapsed to the active language.
163
+ `useWidgetSubCaption` takes the same flag as an optional second argument.
164
+
165
+ What does not change:
166
+
167
+ - Without the prop, the bundle composition is unchanged. That covers a page's
168
+ inline `dashboard` block, a preview and a design surface, none of which the
169
+ server translated.
170
+ - Header-action labels keep their bundle lookup either way, because the server
171
+ does not translate them.
172
+ - 1a88ce2: **BREAKING (rendering):** a dataset-bound dashboard widget no longer reads `chartConfig.series`, `chartConfig.xAxis` or `chartConfig.yAxis` (objectui#11315).
173
+
174
+ `@objectstack/spec` 17.5.0 refuses these keys on a dashboard widget's chart config
175
+ (`DashboardWidgetChartConfigSchema`, ADR-0021 · ADR-0049 D2), together with `chartConfig.type`, which
176
+ the widget never read. Until now the dataset widget still merged their presentation half onto the
177
+ dataset-derived chart: a per-series mark and axis side from `series[]`, and axis titles, number
178
+ formats, min/max and a second value axis from `xAxis` / `yAxis`.
179
+
180
+ - A widget that still carries the keys (stored metadata that was never re-validated) now renders
181
+ the dataset's own derivation: one series per measure in `values`, the category axis from
182
+ `dimensions`, and no authored axis.
183
+ - A `combo` widget still draws as a combo, but each series takes the chart renderer's own mark
184
+ rather than an authored `series[].type`. A `bar` / `line` / `area` widget that declared one series
185
+ as a different mark draws as its own family.
186
+ - A comparison overlay (`compareTo`) no longer copies its measure's authored mark or axis; there is
187
+ none to copy.
188
+ - The chart chrome in `chartConfig` (`title`, `subtitle`, `description`, `colors`, `height`,
189
+ `showLegend`, `showDataLabels`, `annotations`, `interaction`) is lowered exactly as before.
190
+
191
+ What to do: run `os validate` (or the spec's `os migrate meta --from 17`) on dashboards that author
192
+ these keys, delete them, and select `dimensions` / `values` on the widget. A chart that needs
193
+ per-series marks or a second value axis belongs on an `object-chart` node with inline `data`, where
194
+ the spec keeps `series`, `xAxis` and `yAxis` authorable.
195
+
196
+ This is released as `minor`, following this repository's version policy: breaking semantics are
197
+ marked `minor` and described here.
198
+ - 00ccdf7: The `object-pivot` registration publishes `drillDown` as an input (objectui#11440).
199
+
200
+ `ObjectPivotTable` has always read `drillDown` (whether the drill is on, its filter, title, target, columns, row cap and report), and the `pivot` node's refusal of `drillDown` names `object-pivot` as where a pivot drill is authored. The registration did not list it, so the `sdui-parser` manifest and the designer did not offer it. Its description says `drillDown.mode` does not apply: every click point on a pivot is an aggregated bucket, so the pivot always drills through. `@object-ui/types/zod`'s new `object-pivot` arm declares the same member (`.changeset/11440-arm-passing-types.md`). Nothing else about the block changes.
201
+ - 83e3f83: The dashboards' metric card node, `plugin-dashboard:metric`, is a declared node type, and both dashboard surfaces hand `SchemaRenderer` declared nodes with no cast (objectui#11466).
202
+
203
+ - **New export `DashboardMetricNodeSchema`:** the node `MetricWidget` renders under `plugin-dashboard:metric`, with the keys it reads off the node, typed by `MetricWidgetProps`. This package enters it in `@object-ui/types`' `CustomNodeRegistry` under that key, so a program that loads this package's typings has the node as a member of `DeclaredNode`: authorable in a node slot and at `SchemaRenderer`'s `schema` prop, with its keys checked.
204
+ - **`DashboardGridLayout`'s static `pivot` node** (a `pivot` widget drawn from inline rows) states the keys `PivotTableSchema` declares and `PivotTable` draws, read from `options`: `title`, `rowField`, `columnField`, `valueField`, `aggregation`, `showRowTotals`, `showColumnTotals`, `format`, `columnColors` and `className`. It used to spread `options` whole, so every option key became a key of the node, and `SchemaRenderer` handed each one on as a React prop. Now no other option key reaches the node: not a host prop of `PivotTable`'s (such as `rowLabels`), and not a `BaseSchema` node key other than `className` (such as `id`, `style` or `hidden`). Each key listed above draws as before.
205
+ - **The two surfaces' casts before `SchemaRenderer` are gone** (`DashboardGridLayout`'s `as BaseSchema | string | null | undefined` and `DashboardRenderer`'s `as BaseSchema`). Each surface's node builder returns `SchemaRenderer`'s own prop type, so every node is checked against its declared type where it is built. No runtime change.
206
+ - 83e3f83: **An `object-metric` node in a dashboard widget's legacy `component` envelope now draws the retired-format prompt instead of its number (objectui#11466).** This follows the maintainer's ruling A on objectui#11466, which extends ruling C on objectui#11525 (a dashboard metric takes its number only through a `dataset`) to this last inline metric form.
207
+
208
+ **What changes on screen.** The change covers a stored widget written in objectui's legacy envelope format, `{ id, component: { type: 'object-metric', objectName, aggregate, … }, layout }`, which the spec's widget has no member for:
209
+
210
+ - **Before:** `DashboardRenderer` and `DashboardGridLayout` drew the aggregated number in the tile, and on `DashboardRenderer` the dashboard filter bar's value was merged into the node as a flat `filter`.
211
+ - **Now:** both surfaces draw "This widget uses a retired data format. Edit it to bind a dataset." and send no query. The same holds when the node is written under its registration's full name, `plugin-dashboard:object-metric`. The filter bar's broadcast no longer covers `object-metric`.
212
+
213
+ Breaking for stored dashboards that still carry this form; `minor` because objectui never declares `major`. No producer of the form was measured in objectui or objectstack outside the pin that covered it (the measurement is on objectui#11466); stored customer dashboards are unmeasured, the gap ruling C accepted.
214
+
215
+ **The fix.** Replace the envelope with a dashboard widget bound to a dataset: set `dataset` and select its `values` by name, as for any metric widget. A dataset-bound metric draws its number.
216
+
217
+ **What does not move:**
218
+
219
+ - Every other node in a `component` envelope draws as before, and the filter bar still scopes an envelope's `object-chart` and `object-data-table`.
220
+ - An `object-metric` block authored on a page (`{ type: 'object-metric', properties: { … } }`) draws as before.
221
+ - The zod and TypeScript authoring faces are unchanged.
222
+ - f560ded: Dashboard percent faces read the storage they are told, never a storage guessed
223
+ from the value (objectui#11475).
224
+
225
+ - **`object-metric` over a `percent` field** renders its `sum` / `avg` / `min`
226
+ / `max` at the field's own storage (`percentCellScale`, the spec's
227
+ `percentScaleOf`), through the list cell's `formatPercent`, the way it already
228
+ renders a currency. A field declaring `max: 100` that averages `50` reads
229
+ `50%`. The width is the authored `%` pattern's decimals, or else the
230
+ field's width.
231
+ - **`metric` with a `%` pattern** holds no field, so the pattern states the
232
+ storage: numeral's `%` multiplies by 100, so the value is a fraction. A ratio
233
+ of exactly `1` reads `100%` again. A stored value of `1` or more is
234
+ multiplied by 100 now, where the old guess passed it through.
235
+ - **`renderFieldValue`** (the data table and the record drawer) reads a
236
+ `percent` or `progress` field's storage from its declaration. `FieldMeta` gains
237
+ `max`, copied from the schema field def by `buildFieldMeta`. As a `FieldMeta`
238
+ member it joins `ObjectDataTable`'s read-side refusal band, so an authored
239
+ column cannot restate how its field stores. Any other field with a `%`
240
+ pattern reads its value as a fraction, numeral's reading.
241
+ - 8366acc: The drill-down drawer scopes a dataset-bound drill report by `runtimeFilter`, and the pre-9.0 object-bound drill report is retired (objectui#11506).
242
+
243
+ **Fixed (`@object-ui/plugin-dashboard`) — a drilled report showed the whole dataset.** The drawer opened by an `object-pivot` or `object-metric` `drillDown.report` wrote the widget's resolved filter into the report as `filter`. `ReportSchema` refuses that key and `DatasetReportRenderer` does not apply it (objectui#5137; it only warns in development), so a drill from a filtered widget queried the dataset unscoped. The drawer now writes the filter as the report's `runtimeFilter`, joined to the report's own `runtimeFilter` with `$and` when it has one, the same single join the `filter` write used. A `filter` that an unvalidated stored report still carries is not read as a scope, and it is not removed: the renderer still says in development that it was not applied.
244
+
245
+ **Changed (`@object-ui/plugin-dashboard`) — which value the drawer draws as a report.** The drawer used to treat a `report` as a report when it carried a `columns` array or an `objectName` key, both markers of the pre-9.0 object-bound form. It now treats it as a report when it is dataset-bound: a non-empty `dataset`, or a `joined` report with a block that binds one (the reading `@object-ui/plugin-report`'s `isDatasetReport` uses). A tabular, summary or joined dataset report, which carries no `columns`, is now drawn as a report; it used to list the records. A `{ name }` reference and the retired object-bound form list the records, scoped by the drill filter, as a drill with no `report` does.
246
+
247
+ BREAKING (`@object-ui/types`): `DrillDownConfig.report` narrows to the dataset-bound report. (The bump is `minor` by this repo's release model: objectui's major follows the `@objectstack` family major, and its own breaking changes ship as `minor` with the breaking semantics stated here.)
248
+
249
+ - FROM: `report?: { name: string; objectName: string; type?: 'tabular' | 'summary' | 'matrix' | 'joined'; columns: Array of unknown; [k: string]: unknown } | { name: string }`
250
+ - TO: `report?: SpecReportInput | { name: string }`, where the inline arm is `@objectstack/spec`'s `ReportSchema` author shape by reference, the same schema the `report` node's `report` member declares (objectui#11440).
251
+
252
+ The zod mirror, `DrillDownConfigSchema.report`, takes the spec's `ReportSchema` the same way, ahead of the unchanged `{ name }` reference arm, so the spec's required keys and refusals apply to an inline drill report: `name` and `label` are required, a report that is not `joined` needs a `dataset` and a non-empty `values` (measure names), a `joined` one needs non-empty `blocks`, and the inline arm refuses `filter` and `objectName`, each naming its replacement. The retired form had no producer when this change was measured (this repository, and `@objectstack`'s examples and docs), and through the real drawer it drew an empty presentation and issued no query, so it applied no filter under either key. There is no alias window. Migration: rewrite an object-bound drill report as a dataset-bound one, a `dataset` with `rows`, `values` and, for a matrix, `columns` as dimension names, and put a fixed scope in `runtimeFilter`.
253
+
254
+ Which face refuses the retired form: the TypeScript declaration does (`objectName` is a member of neither arm), and so does the strict authoring face, `StrictAnyComponentSchema`, which refuses it at the report, naming `objectName`. `objectui validate` does not: it runs `safeValidateSchema`, the tolerant face, where a value that fails the inline arm and carries a string `name` still parses as a `{ name }` reference, stripped to that member, as before, and the document is reported valid. At runtime the drawer then lists the records, as for any `{ name }` reference.
255
+
256
+ ⚠️ **Dated note, 2026-10-03 — the `{ name }` reference arm is retired — objectui#11517.** At this change `DrillDownConfig.report` was `SpecReportInput | { name: string }`, the zod mirror kept the `{ name }` reference arm unchanged, and `objectui validate` read the retired object-bound form as a `{ name }` reference and reported the document valid; now, later in this same release, the reference arm is retired on the TypeScript face and both zod faces. So the TO line above reads `report?: SpecReportInput`, a bare `{ name }` is refused by name, and the tolerant face refuses the object-bound form at its `objectName`, naming this entry's retirement; "`objectName` is a member of neither arm" reads "is no report member". The drawer is unchanged: it lists the records for either form. `.changeset/11517-drill-report-name-retired.md` states what ships. The rest of this entry is kept as the reading of this change.
257
+ - 9d7419b: `DashboardRenderer`'s `onWidgetsReorder` hands back the slot's own array type, `DashboardComponentSchema['widgets']`, instead of `DashboardWidgetSchema[]`; the dashboard's `object-chart` producers name `ObjectChartSchema`; and a widget with no `type`, or with a `type` that names nothing, draws what the spec says it is (objectui#11514). `minor`, per this repository's version alignment: a reorder handler typed `(widgets: DashboardWidgetSchema[]) => void` stops compiling, because the component arm of a `widgets[]` entry is no longer assignable to `DashboardWidgetSchema`. Type the handler's parameter as `DashboardComponentSchema['widgets']`.
258
+
259
+ - **A widget with no `type` is a `metric` widget.** `@objectstack/spec`'s `DashboardWidget.type` defaults to `metric`; both surfaces read that default from the spec and draw the widget exactly as the same widget with `type: 'metric'`: inline, bound to a dataset (where a typeless widget with a dimension drew a bar chart), and in `DashboardRenderer`'s mobile metric row. It used to reach the slot-component passthrough and draw the registry's red "Unknown component type" (OBJUI-001) panel.
260
+ - **A `type` that names no family** (and no component type) draws the labelled placeholder "「type」chart type is not supported yet", as a known family with no renderer does, instead of that red panel. Both validator faces already refuse such a widget at `type`.
261
+ - **The slot-component passthrough** serves the slot's component arm (`metric-card`) alone, unchanged for it.
262
+ - **Slot-entry reads.** `DashboardRenderer`, `DashboardGridLayout`, `DashboardWithConfig` and the retired-widget detector read a `widgets[]` entry by the slot's element type, `DashboardComponentSchema['widgets'][number]`, which is what lets `@object-ui/types` drop the component type from `DashboardWidgetSchema['type']`.
263
+ - **The `object-chart` producers.** A series dispatch carries the family as a literal union whose members are all families `ObjectChartSchema.chartType` declares, so the node both surfaces build for a `provider: 'object'` series widget satisfies `ObjectChartSchema` with no cast.
264
+ - **README.** "Reading a widget key off `widgets[]`" narrows an entry on `type` before reading a widget key, and a new section states what a widget with no `type`, or an unknown one, draws.
265
+ - **What does not move.** Every widget that names a known family draws as before: the dispatch routes the same families, and each `object-chart` node carries the same `chartType` it carried. A legacy `component` envelope with no `type` is not the spec's widget and is not given its default; it draws under its card heading as before, and a number or `true` in its `component` draws the same text, now forwarded through `toRenderableSchema`.
266
+
267
+ ⚠️ **Dated note, 2026-10-03 — a dataset-less `provider: 'object'` metric is retired — objectui#11525.** At this change, "Every widget that names a known family draws as before" held for the single-value family's inline `provider: 'object'` widget, which drew its number through a flat `object-metric` node. Now a `metric`, `gauge`, `solid-gauge`, `kpi` or `bullet` widget, or a typeless one, with no `dataset` and an `options.data` (or widget-level `data`) of `{ provider: 'object', … }` draws the retired-format placeholder on both surfaces, by the maintainer's ruling C on objectui#11525. A typeless widget still draws exactly as the same widget with `type: 'metric'`, so it draws that placeholder too. `.changeset/11525-inline-metric-retired.md` states what ships. The rest of this entry is kept as the reading of this change.
268
+
269
+ ⚠️ **Dated note, 2026-10-03 — the grid's static pivot node states its keys — objectui#11466.** At this change, "Every widget that names a known family draws as before" held for a `pivot` widget drawn from inline rows on `DashboardGridLayout`, whose node spread the widget's `options` whole. Now that node states the keys `PivotTableSchema` declares and `PivotTable` draws, read from `options`, and no other option key reaches it. `.changeset/11466-dashboard-metric-node.md` states what ships. The rest of this entry is kept as the reading of this change.
270
+
271
+ ⚠️ **Dated note, 2026-10-03 — an envelope's `object-metric` draws the placeholder — objectui#11466.** At this change, a legacy `component` envelope "draws under its card heading as before" whatever node it held. Now an `object-metric` node in it draws the retired-format placeholder under that heading and sends no query, by the maintainer's ruling A on objectui#11466; every other envelope node draws as before. `.changeset/11466-envelope-object-metric-retired.md` states what ships. The rest of this entry is kept as the reading of this change.
272
+ - 160c6c6: **A dashboard metric widget bound inline to an object now shows the
273
+ retired-format prompt instead of its number (objectui#11525).** This follows
274
+ the maintainer's ruling C on objectui#11525.
275
+
276
+ **What changes on screen.** The change covers a stored single-value widget:
277
+ `metric`, `gauge`, `solid-gauge`, `kpi`, `bullet`, or a widget with no `type`,
278
+ which draws as `metric`. If it has no `dataset` and its `options.data` (or
279
+ widget-level `data`) is `{ provider: 'object', object, aggregate }`, it is
280
+ affected:
281
+
282
+ - **Before:** both `DashboardRenderer` and `DashboardGridLayout` aggregated the
283
+ object and drew the number in the tile.
284
+ - **Now:** the tile shows "This widget uses a retired data format. Edit it to
285
+ bind a dataset." and sends no query. This is the same placeholder a `pivot`
286
+ widget with that config already showed (objectui#10528).
287
+
288
+ Breaking for stored dashboards that still carry this form; `minor` because
289
+ objectui never declares `major`. Under ADR-0021 a widget binds a
290
+ semantic-layer `dataset`, and both validator faces already refuse a widget
291
+ without one, so nothing authored against the current contract is affected. No
292
+ example, objectui authoring surface, cloud dashboard or AI Studio path was
293
+ measured emitting the form (the measurements are on objectui#11525).
294
+
295
+ **The fix.** Rebind the widget to a dataset: set `dataset` and select its
296
+ `values` by name. A dataset-bound metric draws its number as before.
297
+
298
+ **What does not move:**
299
+
300
+ - The inline `provider: 'object'` chart and table widgets still draw.
301
+ - A static metric (`options.value` or an inline row array) still draws its value.
302
+ - The zod and TypeScript authoring faces are unchanged.
303
+ - An `object-metric` node an author places in a widget's legacy `component`
304
+ envelope still receives the dashboard filter bar's values.
305
+
306
+ The flat `object-metric` node the two surfaces used to build carried an
307
+ ObjectQL-dialect `filter` that no node type declares; it is gone.
308
+
309
+ ⚠️ **Dated note, 2026-10-03 — the envelope's `object-metric` is retired too — objectui#11466.** At this change, "An `object-metric` node an author places in a widget's legacy `component` envelope still receives the dashboard filter bar's values" held, because `object-metric` stayed in the filter broadcast's filterable set for that node. Now, by the maintainer's ruling A on objectui#11466 (extending this change's ruling C), that envelope node draws the same retired-format placeholder on both surfaces and sends no query, and `object-metric` left the filterable set. `.changeset/11466-envelope-object-metric-retired.md` states what ships. The rest of this entry is kept as the reading of this change.
310
+ - 64dae8e: Six user-visible fixes across the maker surface, the assistant rail and the
311
+ dataset captions.
312
+
313
+ **The maker's start chips now promise only what ADR-0112 v1 builds
314
+ (cloud#1984).** Two of the five asked for automation the first version has no
315
+ flows or actions for — the ticket chip said 「状态流转」, the inventory chip said
316
+ 「低库存预警」 — and the measured behaviour was not a refusal but a silent
317
+ degrade: a status kanban and a low-stock view. The chip promised an alert and
318
+ delivered a page. All five are reworded in all ten packs (and in the call-site
319
+ `defaultValue` fallbacks, which are a second copy of the same strings) to ask
320
+ for objects, fields, views, pages, dashboards and sample data, keeping each a
321
+ real business scenario — the ticket chip now asks for a status field and a board
322
+ grouped by it, the inventory chip for a view that filters below the reorder
323
+ point. A note beside the keys says to revert when v2 re-adds flows.
324
+
325
+ **Five newer AI tools get their step labels (objectui#7481).** A zh conversation
326
+ read `✓ Get authoring rules 已完成` between 「读取元数据结构」 and 「列出对象」:
327
+ `get_authoring_rules` (cloud#1837), plus `load_tools`, `open_record`,
328
+ `test_flow` and `toggle_flow`, are registered by the cloud AI runtime but are
329
+ newer than the pinned spec's tool registry, so they had no `chatbot.tool.*`
330
+ entry in any pack and fell through to the English title-caser.
331
+
332
+ **The assistant rail follows the thread when you send (objectui#7480).** The
333
+ rail and the full-page maker are the same component; what differs is width. A
334
+ reply that still ends on screen in the wide column runs two or three times
335
+ taller in a ~360px rail, so `StickToBottom`'s lock is escaped by the time the
336
+ user types and the new bubble, the tool steps and the streaming answer all land
337
+ below the fold. Every send path now re-arms the lock — including the plan-card
338
+ "Build it" and 确认修改 approvals, whose own code comments already named this
339
+ miss. Message APPENDS deliberately do not, so a user reading back through the
340
+ thread mid-answer is never yanked to the bottom.
341
+
342
+ **Console toasts move off the assistant composer (objectui#7482).** 「客户更新
343
+ 成功」 sat on the ChatDock composer's send button and stayed there. One defect,
344
+ two symptoms: `apps/console` pinned the toaster to `bottom-right` — an override
345
+ that predates ADR-0057 P3a — so a toast both covered the button and, because
346
+ sonner pauses a toast's dismiss timer while the pointer is inside the toaster
347
+ region, never got to run its 4s timer with a pointer resting on the composer
348
+ underneath. The override is gone; the console takes `ConsoleToaster`'s own
349
+ documented top-right anchor, and the 4s success duration is now pinned.
350
+
351
+ **Built-in aggregate captions follow the locale everywhere (objectui#7534).**
352
+ objectui#7258 taught `buildChartSeries()` to resolve a server-minted default
353
+ measure through the locale map, so a chart legend read `计数` while the table
354
+ beneath it, the KPI caption, the pivot header and the dataset preview still
355
+ printed the server's hard-coded English `Count`. `buildDatasetFieldHelpers()`
356
+ takes the same optional `builtinAggregateLabels`, resolving through the one
357
+ `resolveMeasureLabel` order, and the five call sites pass it. Omitting the
358
+ argument reproduces the previous output byte for byte, and an author-declared
359
+ measure still keeps its own label verbatim (objectui#4106).
360
+
361
+ **The activity feed stops asking for an object the environment does not have
362
+ (objectui#7476).** A tenant environment has no `sys_activity`, so every page
363
+ load issued a request that 404'd. Everything downstream was already correct —
364
+ the adapter memoizes the missing collection, its logger demotes the failure, the
365
+ feed retires as an ANSWER and the panel renders its earned empty state — so what
366
+ is left is the request itself, and `data-objectstack` states the rule for it:
367
+ the cure for a doomed request is not issuing it. New `useObjectPresence` reads
368
+ the object registry the shell loads for the nav anyway; only a registry that has
369
+ ANSWERED and lists other objects without this one skips the read. Every
370
+ uncertainty — no provider, empty registry, still loading, errored — reads as
371
+ before, because a wrong skip would cost a real deployment its feed.
372
+ - 8a6ecac: Retire `FieldMeta.referenceTo` and withdraw the README line documenting it as an
373
+ author-facing column override (objectui#6597, enforce-or-remove — withdraw branch).
374
+
375
+ **The measurement.** The README documented `referenceTo` as an author override you could
376
+ pass on a `object-data-table` column to "bypass auto-detection" of a lookup's related
377
+ object. Two `keyof FieldMeta` populations exist, kept separate per the card's own trap
378
+ warning: `DatasetRelationship.referenceTo` (a resolver's *output*, unrelated) and this
379
+ package's `FieldMeta.referenceTo` (the card's actual subject). For the latter, neither the
380
+ schema-derived value nor an authored column override ever reached a reader:
381
+ `LookupCellRenderer` (`@object-ui/fields`) resolves its lookup target from
382
+ `field.reference_to` / `field.reference` — never `field.referenceTo` — and
383
+ `computeLookupExpand` builds `$expand` from the OBJECT SCHEMA's field types, never from an
384
+ authored column key. Re-measured on this branch's base (`881d5c292`) with the
385
+ `referenceTo`-vs-`options` positive control already in
386
+ `ObjectDataTable.overrideSource-6425.test.tsx`: `options` (a live override) separates two
387
+ equal-valued columns; `referenceTo` does not — an authored override renders
388
+ byte-identical to its absence.
389
+
390
+ **No authoring story survived the search either.** `ObjectGrid`'s own relational-meta
391
+ pass-through (`applyRelationalMeta`, `plugin-grid/src/ObjectGrid.tsx`) copies
392
+ `reference_to` / `reference` / `display_field` / etc. from the SCHEMA field def only, at
393
+ all three of its call sites — never from an authored column override. No doc, example, or
394
+ fixture in this repo shows a table column pinning a lookup's target away from what its
395
+ schema field already says. Under the maintainer's standing startup-stage rule
396
+ (2026-08-27: deprecated/alias spellings retire immediately, no transition windows), no
397
+ measured demand selects withdraw.
398
+
399
+ **Both `keyof FieldMeta` seam bands, both in scope.** `ObjectDataTable` derives two
400
+ refusal bands from `keyof FieldMeta` — `EnrichedColumn`'s write-side tombstones
401
+ (objectui#6373) and `AuthoredColumnOverrides`' read-side band (objectui#6425) — so
402
+ deleting the member would have dropped `referenceTo` from both as a side effect, silently
403
+ un-enforcing objectui#6425's "not declared as spelled, still HELD" verdict. A new
404
+ hand-written `ObjectDataTableRetiredReferenceToTombstone` (`{ referenceTo?: never }`) is
405
+ intersected into both halves of the seam, the exact sibling of
406
+ `ObjectDataTableRetiredDecimalsTombstone` (objectui#6625) — same mechanism, same reason.
407
+ `ObjectDataTableColumnHolds` — the interface that carried the HELD verdict — is now empty
408
+ (kept, not deleted, as the documented extension point a future ruling holds a new key
409
+ onto).
410
+
411
+ **Ablation.** Removing the tombstone intersection (replacing it with the old
412
+ `{ referenceTo?: unknown }` HELD shape) turns two `@ts-expect-error` directives unused
413
+ (TS2578) — `tsc -p tsconfig.test.json` exits 2. Restoring the intersection returns a clean
414
+ exit 0. This proves the tombstone, not the derived band or a lingering hold, is what
415
+ refuses the key now.
416
+
417
+ **Behaviour is unchanged** — pinned by the unchanged `referenceTo reaches NOTHING on this
418
+ path` runtime assertion, and by two new counter-control tests (mirroring the ones
419
+ objectui#6625 added for `decimals`) proving the tombstone specifically is what refuses the
420
+ key at both seam bands.
421
+
422
+ Marked `minor` per this repo's version-alignment rule (AGENTS.md 版本号策略), which
423
+ reserves `major` for following `@objectstack` across a major. Scope note, measured rather
424
+ than assumed: `FieldMeta`, `AuthoredColumnOverrides`, `EnrichedColumn` and
425
+ `ObjectDataTableColumnHolds` are absent from `dist/index.d.ts` — `plugin-dashboard`'s
426
+ barrel re-exports only the `ObjectDataTable` component, and the package's `exports` map
427
+ publishes only `"."`. No downstream type moves; this is a package-internal contract change
428
+ plus a README correction, not a removal from a published type surface.
429
+
430
+ ⚠️ **Dated note, 2026-09-30 — `reference_to` has since left the cell and the grid's copy — objectui#11070.** The measurement above read `LookupCellRenderer` resolving its target from `field.reference_to` / `field.reference`, and `applyRelationalMeta` copying `reference_to`. Later in this same release objectui#11070 (round 4) narrowed the cell to `reference` alone and dropped `reference_to` from both copy sets. The verdict on `referenceTo` is unchanged. `.changeset/11070-reference-to-round4.md` states what ships; the text above is kept as the reading of this change.
431
+ - 891a188: Retire `FieldMeta.decimals` and the now-unfed `BuildFieldMetaParams.overrides.decimals`
432
+ from `plugin-dashboard`'s shared field-rendering helpers (objectui#6625,
433
+ enforce-or-remove).
434
+
435
+ `buildFieldMeta` computed `decimals: overrides.decimals ?? meta?.decimals ?? meta?.scale`
436
+ on **every** call and the value **reached nothing**. Re-measured on this branch's base
437
+ (`efdc6c62`): **zero `.decimals` member reads** across `@object-ui/fields`,
438
+ `@object-ui/i18n`, `@object-ui/components`, `@object-ui/core` and `plugin-dashboard`
439
+ itself — the only non-comment occurrence was the write being removed here. The
440
+ **positive control in the same query shape** fires: `.scale` member reads hit
441
+ `NumberField.tsx`, `GridField.tsx` and `fields/src/index.tsx`. So the zero is a
442
+ finding, not a broken query. The `overrides.decimals ??` head of that chain had already
443
+ lost its only feeder when objectui#6425's ruling removed the authored read from
444
+ `ObjectDataTable.enrich()`; `RecordDetailDrawer`, the only other `buildFieldMeta`
445
+ caller, passes no overrides at all. Both halves retire together, so the key leaves in
446
+ one move.
447
+
448
+ Behaviour is unchanged — no reader existed to notice, pinned by the unchanged runtime
449
+ assertion in `ObjectDataTable.overrideSource-6425.test.tsx` that an authored `decimals`
450
+ renders byte-identical to its absence.
451
+
452
+ **The refusal did NOT leave with the member.** `ObjectDataTable` derives *two* refusal
453
+ bands from `keyof FieldMeta` — `EnrichedColumn`'s write-side tombstones (objectui#6373)
454
+ and `AuthoredColumnOverrides`' read-side band (objectui#6425) — so deleting the member
455
+ would have dropped `decimals` from both as a side effect, silently un-enforcing
456
+ objectui#6425's retire. A new hand-written `ObjectDataTableRetiredDecimalsTombstone`
457
+ (`{ decimals?: never }`) is intersected into both halves of the seam, the same shape and
458
+ for the same reason as `ObjectGrid`'s `ObjectGridRetiredOptionsTombstone`. The verdict
459
+ is unchanged since 2026-08-27; only the artefact enforcing it moved, from derived to
460
+ hand-written.
461
+
462
+ Marked `minor` per this repo's version-alignment rule (AGENTS.md 版本号策略), which
463
+ reserves `major` for following `@objectstack` across a major. Scope note, measured rather
464
+ than assumed: `FieldMeta`, `BuildFieldMetaParams`, `AuthoredColumnOverrides` and
465
+ `EnrichedColumn` are **absent from `dist/index.d.ts`** — they are not re-exported by the
466
+ package barrel, and the `exports` map publishes only `"."`, so no consumer can name them.
467
+ No downstream type moves; this is a package-internal contract change.
468
+ - 220c18d: Dashboard/analytics widgets get a self-explaining DEFAULT empty state, stated
469
+ once for the surface (objectui#7063).
470
+
471
+ Maintainer ruling 2026-08-31 (hotcrm#1212, following hotcrm#1203): a widget that
472
+ renders a bare row-placeholder on an empty result is the PLATFORM's defect and
473
+ must be fixed uniformly — apps must not compensate widget by widget
474
+ (objectstack#13848). The measured scenario is a fresh flagship-demo install:
475
+ eleven populated tiles and one reading exactly `暂无数据行` mid-page, which reads
476
+ as "the dashboard failed to load" even though the widget, its declaration and
477
+ its (not yet produced) data are all legitimate.
478
+
479
+ - New `WidgetEmptyState` is the seam the three dashboard surfaces now share.
480
+ There was no shared placeholder to fix: `DatasetWidget` wrote
481
+ `dashboard.noRows`, while `ObjectDataTable` and `PivotTable` wrote
482
+ `dashboard.noDataAvailable` — three renders, two strings, no common code.
483
+ - The default now reads as a STATE, not a failure: `role="status"` (the empty
484
+ branches previously carried no role at all, while the failure branches beside
485
+ them are `role="alert"`), muted treatment with an inbox glyph rather than a
486
+ warning triangle, and a title plus an explanation where the placeholder was a
487
+ single terse fragment.
488
+ - It names WHAT is empty with zero authored copy — the widget's data source,
489
+ which is the half the reader cannot already see (the tile's title is rendered
490
+ by the card header directly above). That is `widget.dataset` on the dataset
491
+ path and `schema.objectName` on the object-bound table/pivot; `PivotTable`
492
+ takes it as a new optional `sourceLabel` prop, which `ObjectPivotTable`
493
+ forwards.
494
+ - Copy is platform i18n: `dashboard.empty.title` / `.message` / `.sourceLabel`
495
+ added to `en` and all nine sibling packs. No inline `defaultValue` and no
496
+ interpolation — the source renders as a labelled value, so no separator is
497
+ concatenated in code and every pack spells its own punctuation.
498
+
499
+ No new authoring obligation and no new spec key. Note that the `emptyState`
500
+ override the card assumes for this surface does not exist: `emptyState` is a
501
+ LIST-view contract, and `@objectstack/spec`'s `DashboardWidgetSchema` declares no
502
+ such key — so there is nothing here for an author to override, and adding one
503
+ would be a contract question rather than a rider.
504
+ - 2ceb43a: Retire the widget `dataProvider` key. It was declared and written, but nothing
505
+ read it (objectui#7353, ADR-0049 remove arm).
506
+
507
+ Both dashboard surfaces build a node for the table and pivot widgets from a
508
+ `provider: 'object'` widget. They copied the whole provider config onto that node
509
+ as `dataProvider`, next to `objectName`. The widgets read only `objectName`, so
510
+ `dataProvider` repeated the same information in a second place that no code used.
511
+
512
+ **`@object-ui/plugin-dashboard` — BREAKING (declared `minor` per the repo's
513
+ version policy).** Compared with the released 17.6.0:
514
+
515
+ - `DashboardRenderer` no longer writes `dataProvider` on the `object-data-table`
516
+ node, and `DashboardGridLayout` no longer writes it on any node it builds for a
517
+ `provider: 'object'` widget. Since objectui#10528 the grid builds that same
518
+ self-fetching `object-data-table` node for such a table widget, and shows the
519
+ retired-widget placeholder for such a pivot widget. The `object-data-table`
520
+ node still writes `objectName` on both surfaces, and the object-backed table
521
+ widget fetches through it.
522
+ - In 17.6.0, the `schema` prop types of `ObjectPivotTable` and `ObjectDataTable`
523
+ both declare `dataProvider?: { provider: string; object?: string }`. Both now
524
+ declare it as a retirement tombstone (`dataProvider?: never`). TypeScript code
525
+ that sets `dataProvider` on these props no longer compiles.
526
+ - To migrate, delete `dataProvider` and set `objectName` to the object the widget
527
+ should load. `ObjectPivotTable`'s node has no zod schema, so for that widget
528
+ the type check is the only place the key is refused.
529
+
530
+ **`@object-ui/types`.** Released 17.6.0 declares neither `ObjectDataTableSchema`
531
+ nor `dataProvider`, so this part changes no released declaration and no released
532
+ validator's verdict. `ObjectDataTableSchema` itself is new after 17.6.0
533
+ (objectui#6576). With this change it ships with `dataProvider` retired on both
534
+ faces:
535
+
536
+ - the TS interface declares `dataProvider?: never`;
537
+ - the zod mirror (`ObjectDataTableSchema` in `@object-ui/types/zod`) refuses the
538
+ key by name, with a message that says to write `objectName`.
539
+
540
+ The refusal applies to any value, well-formed or malformed. Other undeclared keys
541
+ are still accepted, as they are on every `BaseSchema` node.
542
+
543
+ **Why a tombstone rather than a plain deletion.** Both of these types extend
544
+ `BaseSchema`. Its TypeScript index signature and its `.passthrough()` zod schema
545
+ accept any key that is not declared. A deleted member would therefore still
546
+ compile and still parse, and it would still do nothing. The tombstone turns that
547
+ silent no-op into a named refusal that points at `objectName`. It reads nothing,
548
+ so no reader was added.
549
+
550
+ **DOM.** All three producer paths were rendered for real before the change. The
551
+ key never reached a DOM attribute: there was no `dataprovider` or
552
+ `data-provider` attribute and no React unknown-prop warning. So this change does
553
+ not close any objectui#4357-class leak.
554
+ - 0dc2c93: `compareTo` on a `scatter` chart is no longer supported — scatter joins pie / donut /
555
+ funnel on the list of chart families that ignore it (objectui#7402, maintainer ruling
556
+ 2026-09-03).
557
+
558
+ **This removes a published capability, deliberately.** Until now a `chartType: 'scatter'`
559
+ chart (and the dashboard widget types `scatter` and `bubble`, which both render as one)
560
+ with `compareTo` set synthesised a muted "previous period" overlay series. It drew the
561
+ wrong picture: a scatter binds ONE measure, and the renderer reads y through the single
562
+ `YAxis dataKey={series[0].dataKey}`, so the overlay was plotted on the PRIMARY series' y
563
+ — "previous period" painted exactly on top of "current" (objectui#7194).
564
+
565
+ Enforce-or-remove: rather than keep drawing that, the capability is removed until it can
566
+ be drawn honestly. Drawing a real second measure on a scatter needs the multi-measure
567
+ projection recorded as option A of objectui#7194, which is not built (zero authored
568
+ callers). **If and when that projection lands, `compareTo` on a scatter returns with
569
+ it** — it is the same missing mechanism, one payment.
570
+
571
+ What changes for authors:
572
+
573
+ - A `compareTo` on a scatter is now IGNORED rather than drawn. The primary series still
574
+ renders exactly as before — nothing refuses, nothing goes blank, and no comparison
575
+ query is issued on the inline chart path.
576
+ - No `<measure>__comparison` (inline chart) / `<measure>__compare` (dashboard) series is
577
+ appended for a scatter, so a compare-to scatter document also never reaches the
578
+ two-or-more-series scatter refusal being added under objectui#7194.
579
+ - Charts that keep the overlay: line, area, bar, horizontal-bar, combo. Charts that
580
+ ignore `compareTo`: pie, donut, funnel and — as of this change — scatter (and the
581
+ `bubble` widget type that renders as a scatter).
582
+
583
+ Reachability at the time of the change: **0** authored scatter/bubble instances in-repo
584
+ across both spellings (control `"type": "bar"` fires at 5 example files); incidence in
585
+ deployed tenant metadata is not measurable from this repo.
586
+ - 721d1e0: One declaration of which chart families ignore `compareTo` (objectui#7495).
587
+
588
+ **New in `@object-ui/core`:** `chartTypeIgnoresCompareTo(chartType)`, exported from
589
+ `chart-presentation`. It answers `true` for `pie`, `donut`, `funnel` and `scatter` and
590
+ `false` for every other chart family, an unknown string, and `undefined` — the answers
591
+ `@object-ui/plugin-charts`' ObjectChart already gave through its own local predicate.
592
+ ObjectChart now reads the core predicate instead of keeping that list, so the inline chart
593
+ path behaves exactly as before.
594
+
595
+ **Behaviour change in `@object-ui/plugin-dashboard`:** a dataset chart widget whose chart
596
+ family ignores `compareTo` no longer asks the executor for a comparison. That covers the
597
+ widget types `pie`, `donut`, `funnel`, `pyramid` (renders as a funnel), `scatter` and
598
+ `bubble` (renders as a scatter). The dashboard used to carry its own, narrower copy of the
599
+ list (scatter only), and it gated only the overlay series, never the query: every one of
600
+ these widgets still forwarded `compareTo`, still had its date window lowered into
601
+ `timeDimensions`, and so made the executor run the comparison pass and attach
602
+ `MEASURE__compare` columns. A pie, donut or funnel then appended an overlay series the
603
+ renderer dropped; a scatter discarded the columns. Those widgets now send the same selection as the same
604
+ widget without `compareTo`: one pass on the server instead of two, and nothing on screen
605
+ changes for a widget whose filter carries one bounded date window. One case does change on
606
+ screen, for the better: the dataset executor refuses a `compareTo` it has no single dated
607
+ window to shift, and the widget showed that refusal in place of the chart — a compare-to
608
+ pie, donut, funnel or scatter widget with no dated window now draws its chart, since no
609
+ comparison is asked for.
610
+
611
+ Unchanged: line / area / bar / horizontal-bar / combo chart widgets keep their comparison
612
+ overlay, and metric, gauge, table and pivot widgets keep their comparison. The gate is the
613
+ chart branch, not the widget type, so a `pie` widget with no dimensions — which renders as
614
+ a metric tile and shows the comparison as a delta — keeps it too.
615
+
616
+ Reachability at the time of the change: `compareTo` appears in no example app metadata in
617
+ this repository; incidence in deployed tenant metadata is not measurable from here.
618
+ - 1cca678: Retire the dashboard-**root** `title` read across all five surfaces (objectui#7509,
619
+ maintainer ruling 2026-09-04, decision batch #29, option C, under ADR-0049).
620
+
621
+ **What changes for an operator.** A stored dashboard whose header came from a legacy
622
+ root `title` now shows its `label`. `label` is the only header source, then the raw
623
+ `name`.
624
+
625
+ Per surface:
626
+
627
+ - Console dashboard page (`DashboardView`) — header falls to `label`, then `name`.
628
+ - Standalone dashboard embed (`DashboardRenderer`) — `header` shows `label`; a document
629
+ with no `label` now shows no header title at all.
630
+ - The `dashboard-grid` SDUI component (`DashboardGridLayout`) — heading falls to
631
+ `label`, then the generic `Dashboard`.
632
+ - Studio dashboard designer (`DashboardEditor` preview panel, `DashboardDesignPage`
633
+ heading) — both fall to `label`, then `name` / the generic heading.
634
+
635
+ **Why now.** `@objectstack/spec`'s `DashboardSchema` refuses a root `title` **by name**
636
+ (`unrecognized_keys(title)`), and the save route answers `422 INVALID_METADATA` — so no
637
+ authored dashboard can acquire the key, and what retires is compatibility with documents
638
+ stored before that refusal existed. Until now five surfaces read the legacy spelling
639
+ independently, which meant a legacy document could show one header in the console and a
640
+ different one in the designer. One spelling now answers everywhere.
641
+
642
+ **Migration.** `label` is REQUIRED on `DashboardSchema`, so a spec-valid stored dashboard
643
+ already carries it and needs no change — it simply starts showing that `label` instead of
644
+ the legacy `title`. A document carrying `title` and no `label` was already invalid; give
645
+ it a `label`. No in-repo document needed migrating: a sweep of all 627 tracked JSON found
646
+ 9 dashboard-shaped nodes, and the 6 carrying a root `title` are `type: 'dashboard'`
647
+ component examples that declare no `header`, so none of them rendered a header title
648
+ either before or after.
649
+
650
+ **Not affected: widget titles.** `DashboardWidget.title` is a different, spec-**declared**
651
+ key (the spec's `I18nLabel`) on a different receiver, and is untouched — widget headings,
652
+ the designer's widget-title input and its per-locale write path all behave exactly as
653
+ before. Root and widget arms were separated by receiver, and the retirement's pins carry
654
+ widget-level controls on every surface for that reason.
655
+
656
+ ⚠️ **Dated note, 2026-10-02 — the `dashboard-grid` node key is retired — objectui#10859.**
657
+ Later in this same release objectui#10859 batch 8 (phase 2b) unregistered the `dashboard-grid`
658
+ node key, so `DashboardGridLayout` is no longer an SDUI component; the heading rule above holds
659
+ for it wherever a host mounts it directly. The rest of this entry is kept as the reading of this
660
+ change.
661
+ - edea22a: **BREAKING** — `SchemaRendererProvider`'s `dataSource` prop, and the context
662
+ type every `useSchemaContext()` consumer reads back, are the published
663
+ `DataSource` contract instead of `any`.
664
+
665
+ **FROM** `dataSource={anything}` **TO** `dataSource={adapter}` — a `DataSource`
666
+ from `@object-ui/types`, or `null` / `undefined` when the host has no adapter
667
+ bound.
668
+
669
+ ```ts
670
+ // before — compiled, and failed at runtime on the first find()
671
+ <SchemaRendererProvider dataSource={'not-an-adapter'}>
672
+ // before — compiled, and no reader can do anything with it
673
+ <SchemaRendererProvider dataSource={{}}>
674
+ // after
675
+ <SchemaRendererProvider dataSource={adapter}> // a DataSource
676
+ <SchemaRendererProvider dataSource={undefined}> // "I have no adapter"
677
+ ```
678
+
679
+ Both sites are typed `DataSource | null | undefined` — the spelling
680
+ `useSettledSchema` in this same package already used. The two absences are part
681
+ of the contract, not a weakening of it: a Studio preview, a `kind:'react'` page
682
+ rendered before the host's adapter connects, and a widget probe driving
683
+ `apiFetch` alone all render with nothing bound, and every reader in the tree
684
+ already guards for it. What the union refuses is everything that is not an
685
+ adapter: a string, an empty object, a plain data bag, a partial adapter missing
686
+ a required member.
687
+
688
+ The measured cost, both halves, because the two `any`s have different blast
689
+ radii (measured separately on `origin/main`, whole-repo type-check over the 33
690
+ packages that depend on `@object-ui/react`):
691
+
692
+ - the **context type** — the `any` that reaches every `useSchemaContext()`
693
+ reader — reds **7 diagnostics at 7 sites in 4 packages**, all of them
694
+ production code or a mocked module factory.
695
+ - the **provider prop** — the injection points — reds **52 diagnostics at 27
696
+ sites in 11 packages**, all but one of them test doubles.
697
+
698
+ That ordering is the reverse of the prediction on the card: the context `any`
699
+ was expected to be the expensive one because it infects the whole tree, and it
700
+ is the cheap one, because every reader in the tree already guarded and none of
701
+ them ever reached past `find` / `getObjectSchema`. The prop is the expensive
702
+ one, because the injection points are overwhelmingly test doubles that were
703
+ never complete adapters. The full accounting is on objectui#7912.
704
+
705
+ Two runtime behaviours change, both in the "no adapter" direction and both
706
+ strictly closer to what the surrounding code already intended:
707
+
708
+ - `@object-ui/components`' `kind:'react'` page passed an empty object as its
709
+ "no adapter yet" stand-in. An empty object is TRUTHY, so it walked past every
710
+ `if (!dataSource)` guard written to catch exactly that state and failed later,
711
+ at the call. It now passes the absent adapter itself, so the guard fires where
712
+ it was meant to. The module-constant identity that stand-in existed for is
713
+ preserved: `null` is a primitive, so the provider's memo is unaffected.
714
+ - `@object-ui/plugin-calendar`, `@object-ui/plugin-gantt` and
715
+ `@object-ui/plugin-kanban` collapse a `null` adapter from the context to
716
+ `undefined` before handing it to their widget, whose prop declares the single
717
+ spelling `dataSource?: DataSource`.
718
+
719
+ Nothing else moves at runtime: no value flowing through this key changes, and
720
+ no data path is touched. A TypeScript consumer outside this repo that handed
721
+ this prop something other than an adapter now gets a compile error naming the
722
+ key (TS2322 / TS2739 / TS2740), which is why the FROM/TO is spelled out above.
723
+
724
+ Five `as any` reads of this context in `@object-ui/fields` are gone — they were
725
+ redundant the moment the seam became honest — and `LookupField`'s local
726
+ re-declaration of the imported context as a `Context` of `any`, which laundered
727
+ its `dataSource` read while looking typed, is gone with them. Both directions of
728
+ the contract are pinned against the real compiler in
729
+ `SchemaRendererContext.dataSourceType.pin.test.ts`, and the card's planted
730
+ documentation probe (a bare string in `packages/react/README.md`'s provider
731
+ example) now fails `pnpm check:doc-snippets`, where it used to exit 0 with zero
732
+ diagnostics.
733
+
734
+ objectui#7912.
735
+ - 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate
736
+ the direction so the next one cannot land (objectui#8198).
737
+
738
+ `check:phantom-deps` judges imports that are not declared; nothing judged the reverse,
739
+ so a declaration could outlive its last consumer indefinitely. That is what happened to
740
+ `recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it
741
+ was removed by hand on objectui#7625, and nothing would have reported the next one. The
742
+ new `pnpm check:unused-deps` asks the reverse question over `dependencies` and
743
+ `optionalDependencies` of every released package.
744
+
745
+ **Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship
746
+ changes: their Vite `external` predicates are path-based and never read `dependencies`,
747
+ so no built artifact moves. What changes is the install graph — a project that imports
748
+ one of the removed packages while depending only on the ObjectUI package that used to
749
+ drag it in will no longer resolve it. Declare it directly; that is the correct
750
+ dependency edge in either case. The removals, by package:
751
+
752
+ - `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields`
753
+ - `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`)
754
+ - `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge`
755
+ - `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod`
756
+ - `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge`
757
+ - `@object-ui/core`: `lodash`, `zod`
758
+ - `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply
759
+ - `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect
760
+ - `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge`
761
+ - `@object-ui/fields`: `clsx`, `tailwind-merge`
762
+ - `@object-ui/console`: `@object-ui/react-runtime`, `sucrase`
763
+ - `@object-ui/auth`: `@object-ui/types`
764
+ - `@object-ui/plugin-calendar`: `@object-ui/fields`
765
+ - `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react`
766
+ - `@object-ui/react`: `react-hook-form`
767
+
768
+ Every one was verified by a whole-package grep before removal — the name appeared nowhere
769
+ under the package but its own manifest and CHANGELOG — and the whole workspace builds,
770
+ type-checks and tests green afterwards.
771
+ - 270f282: Six hand-rolled em-dash placeholders now draw the shared `EmptyValue` from
772
+ `@object-ui/components` (objectui#8504), closing the *no accessible name* half
773
+ of the class objectui#8491 / PR #8503 opened.
774
+
775
+ **The accessibility defect.** Each site built its own `<span>` holding a bare em
776
+ dash. `EmptyValue` carries three things none of them had: a `data-slot` of
777
+ `empty-value`, an `aria-label` resolved through the i18n label hook, and
778
+ `select-none` / `no-underline` / `pointer-events-none`. So a screen-reader user
779
+ reaching one of these cells heard a naked punctuation mark, while a neighbouring
780
+ cell drawn by a type-aware renderer was announced as "No value". Two of the
781
+ sites make the inconsistency reachable inside one surface: the metadata list
782
+ renders column 0's placeholder *inside the row's `<Link>`*, where the
783
+ hand-rolled span inherited the link colour and stayed selectable, and the
784
+ dashboard record drawer sits next to renderers that already returned the shared
785
+ component.
786
+
787
+ The six: the metadata list's `defaultCell` and the Audit tab's lock column
788
+ (`@object-ui/app-shell`), the dashboard record drawer's empty `<dd>`
789
+ (`@object-ui/plugin-dashboard`), the import wizard's saved-mapping transform
790
+ cell (`@object-ui/plugin-grid`), the AI-approvals `JsonBlock`
791
+ (`@object-ui/plugin-chatbot`), and the Public Forms object column
792
+ (`@object-ui/console`).
793
+
794
+ **A deliberate visual change, not a no-op.** Five sites drop
795
+ `text-muted-foreground` (or `/60`) for the shared `text-muted-foreground/50`, so
796
+ every placeholder in the workspace is now one colour. The glyph is unchanged
797
+ everywhere. The sixth, `JsonBlock`, keeps its `text-xs` through `className`
798
+ because it stands where a `text-xs <pre>` would and has no shared neighbour to
799
+ match — its delta is the accessible name and the three affordances only.
800
+
801
+ **Two adjacent lines in the same file, taken deliberately.** The AI-approvals
802
+ drawer's `proposed_by` / `decided_by` fields fell back to a bare `'—'` text node
803
+ inside a plain `<div>` — a different source spelling of the same rendered
804
+ defect, individually verified on the card rather than swept up. They are
805
+ converted too. `formatRelative`'s `if (!s) return '—'` is not: that helper is
806
+ declared `: string`, so its fallback is not a node.
807
+
808
+ Filled values are untouched in every path.
809
+ - 0a174f3: fix(plugin-dashboard,core,plugin-charts): route a structured `aggregate.groupBy` on `object-metric` to the spec-shape wire
810
+
811
+ An authored `aggregate.groupBy` is a union — a bare field name, or the
812
+ structured date-bucketing node `{ field, dateGranularity?, alias? }` — and the
813
+ two need different queries. The spec-shape
814
+ `{ groupBy: GroupByNode[], aggregations, where }` reaches `engine.aggregate` and
815
+ runs the server-side date-bucket engine; the legacy
816
+ `{ field, function, groupBy, filter }` query reaches the cube/analytics wire,
817
+ whose `dimensions` the contract declares as an array of dimension NAMES and
818
+ which does not honour `dateGranularity` at all.
819
+
820
+ `ObjectChart.runAggregate` has routed between the two since objectui#7946.
821
+ `ObjectMetricWidget.computeOne` had no such branch: it forwarded the authored
822
+ value straight through, so a metric widget carrying a structured node posted
823
+ `dimensions: [{ field: 'closed_at', dateGranularity: 'month' }]` — an object
824
+ where a name is declared. Nothing refused it and nothing reported it, so the
825
+ author asked for one question and the platform answered another (objectui#8613).
826
+
827
+ - The routing test and the payload are now one function,
828
+ `objectAggregateSpecQuery` (with `isStructuredGroupBy`) in `@object-ui/core`.
829
+ `ObjectChart.runAggregate` and `ObjectMetricWidget.computeOne` both call it,
830
+ so the two renderers cannot post different wires for one authored shape. The
831
+ measure alias is `chartMeasureKey`'s answer, i.e. what the chart's own
832
+ `aggregateValueKey` already delegated to — the chart's posted payload is
833
+ unchanged.
834
+ - `ObjectMetricWidgetProps.aggregate.groupBy` said `string`. That was a claim
835
+ about the author which nothing upstream backed: the value crosses two `any`
836
+ seams on the way in, so the declaration refused the node at neither compile
837
+ time nor runtime. It is now the contract's union, taken by reference through
838
+ `ObjectChartSchema['aggregate']`.
839
+
840
+ Unchanged, and pinned as controls: a plain string `groupBy` and an absent one
841
+ (floored at the single `'_all'` bucket) still take the legacy query byte for
842
+ byte, and an ARRAY `groupBy` still travels there too — it is not this union's
843
+ object arm, and it must keep reaching the producer-side refusal `503cd8b89`
844
+ landed in the adapter.
845
+ - 093af32: Take the `@objectstack/*` line to 17.4.0 and follow every contract it moved
846
+ (objectui#8772, with #7783 · #7663 · #8172 · #7845 · #8785).
847
+
848
+ **Breaking on one authored key, deliberately.** A dashboard node's
849
+ `refreshInterval` is now `refreshIntervalSeconds` — the value is unchanged
850
+ (seconds), and the spec refuses the old spelling by name with a message naming
851
+ the new one, so an existing document fails loudly at parse rather than silently
852
+ losing its auto-refresh. `os migrate meta --from 17` lists the mechanical edits.
853
+ The report component's own `refreshInterval` is a different key and is NOT
854
+ affected. (Scored `minor`, not `major`: this repo's fixed group tracks the
855
+ `@objectstack` major — AGENTS.md §版本号策略.)
856
+
857
+ Also authored-surface changes, all following a published spec move rather than a
858
+ local decision:
859
+
860
+ - `element:record_picker`'s `filter` input is now the `ViewFilterRule` array
861
+ form `[{ field, operator, value }, …]`. The MongoDB-style record form is
862
+ refused by the contract (objectstack#14406 converged the last record-form
863
+ `filter` in the map), so a JSX page writing the array form no longer draws a
864
+ false `type-mismatch` and one writing the record form is told.
865
+ - `object-kanban` now publishes `limit`, the row cap its renderer has always
866
+ lowered to `$top`. The spec declares it as of objectstack#16503, so the key
867
+ the docs teach is finally one the save gate stores.
868
+ - `object-gantt`'s ten extension keys (`timeSegments`, `interactions`,
869
+ `lockField`, …) are derived from the spec's `GanttConfigSchema` instead of
870
+ being re-declared locally. Same accept set on the flat face; the nested
871
+ `gantt` block narrows to the spec's, which now refuses an undeclared sub-key
872
+ by name where its `.passthrough()` window used to admit one.
873
+ - The console's preview-gallery samples — the worked examples an author copies —
874
+ move with two spec changes of their own: a job's `timeout` is now `timeoutMs`
875
+ (same unit-in-the-key-name ruling as the dashboard key above), and a flow's
876
+ end node writes `outcome: 'completed'`, the enum having narrowed to
877
+ `completed | refused`.
878
+ - 2596b1b: Route `object-metric`'s drill-down through the shared `DrillDownDrawer` (objectui#8970).
879
+
880
+ `ObjectMetricWidget` built its own drill panel inline — a Radix `Dialog` when
881
+ `drillDown.target` was the literal `dialog`, a `Sheet` otherwise, with a record-list
882
+ body it constructed itself. That copy read `enabled`, `target`'s two in-place arms,
883
+ `title` and `report`, and had no read site for the rest of the `DrillDownConfig` the
884
+ designer inspector offers. Three members were therefore accepted from the author and
885
+ discarded on this block alone, while acting on every other block that shares the same
886
+ config — no diagnostic, no rejection, and the identical tile on a `dataset` widget in a
887
+ metric presentation worked.
888
+
889
+ **Now delivered on `object-metric`:**
890
+
891
+ - **`target: 'navigate'`** — opens the object's full list page through the host's drill
892
+ navigation, scoped by the metric's own filter. Previously it fell into the panel
893
+ branch and drew the side sheet *unconditionally*. That mattered beyond the missing
894
+ feature: `DrillDownConfig.target` documents this arm as falling back to `'drawer'`
895
+ "when none is available", so the old behaviour read as the documented one while being
896
+ a different one. The fallback is now genuinely conditional.
897
+ - **`columns`** — column whitelist for the drilled record list.
898
+ - **`maxRows`** — page size of the drilled record list.
899
+
900
+ **Two deliberate behaviour changes that come with the shared component:**
901
+
902
+ - The drilled record list no longer shows a search box. The inline panel never chose
903
+ this; it inherited the record table's default while the shared drawer opts out, so a
904
+ metric drill is now the same self-contained peek as a pivot, chart or dataset drill.
905
+ - The drill sheet's widest breakpoint narrows by one step, from `lg:max-w-5xl` to the
906
+ shared drawer's `lg:max-w-4xl`. The centred `dialog` arm is unchanged.
907
+
908
+ The list's default page size is **unchanged at 25** — the value the inline panel
909
+ hard-coded is kept as this block's fallback, so a config that never authored `maxRows`
910
+ drills exactly as before rather than dropping to the record table's default of 10.
911
+
912
+ **Unchanged, and left to objectui#8970's own open question:** `drillDown.filter` and
913
+ `drillDown.mode` still have no read site on this block. The drilled list stays scoped by
914
+ the metric's resolved filter, which is what the block's registration promises ("the same
915
+ filter narrows the drill-down list, so the number and the records behind it always
916
+ agree"); a metric also has no click event for `${event.*}` to interpolate against. The
917
+ shared drawer honours neither member either, so routing through it does not settle them.
918
+ - 8120866: Converge the metric tile's percent `format` branch onto the repo's single percent
919
+ display rule (objectui#9165).
920
+
921
+ `formatMetricValue` in `MetricWidget.tsx` held a **second, drifted copy** of the
922
+ percent display rule. It was spelled inside out — a pass-through guarded by a
923
+ greater-than-one test with the multiply in the else arm — so it scaled everything
924
+ at or below `1` and passed through everything above, where the declared source
925
+ (`percentDisplayValue` in `@object-ui/core`) is the symmetric
926
+ `value > -1 && value < 1`. The negative half was unguarded and the boundary sat
927
+ on the wrong side of `1`. Because of that spelling, objectui#9071's census could
928
+ not see it: "the only `num <= 1` spelling in the repo" was true of the SPELLING
929
+ and false of the DRIFT.
930
+
931
+ The branch now hands the raw stored value to `formatPercent` — the identical call
932
+ this package's own `renderFieldValue` already makes (objectui#5607) and the one
933
+ the list-view percent cell makes — so it takes BOTH halves the shared helper's doc
934
+ comment demands of a third surface: the SCALING and the locale's percent
935
+ CONVENTION. The decimal count still comes from this surface's numeral pattern
936
+ (`'0.00%'` renders two decimals), which is an author declaration on the widget
937
+ rather than a guess about the value.
938
+
939
+ **BREAKING — stored data renders differently on a KPI tile after this change.**
940
+ Nothing about the stored value moves; what moves is how the tile prints it. On a
941
+ `metric-card` with a percent pattern such as `'0%'`, measured against the
942
+ list-cell path in the same run:
943
+
944
+ | stored | before | after | why |
945
+ |---|---|---|---|
946
+ | `1` | `100%` | `1%` | exactly `1` is percentage points, not a fraction |
947
+ | `-1` | `-100%` | `-1%` | the negative half was unguarded |
948
+ | `-5` | `-500%` | `-5%` | same |
949
+ | `1234.5` | `1235%` | `1,235%` | the locale's grouping (objectui#4553's move) |
950
+ | `1234.5` (`de-DE`) | `1235%` | `1.235 %` | the locale's percent affix, no-break space included |
951
+ | `0.25` | `25%` | `25%` | unchanged — a fraction, scaled as before |
952
+ | `12.3` | `12%` | `12%` | unchanged — already percentage points |
953
+
954
+ The first three rows are the defect: the same stored number read `100%` on a KPI
955
+ tile and `1%` in the list cell beside it, which users report as a data bug rather
956
+ than a formatting one. The next two are the convention half, which brings the tile
957
+ in line with the list cell, the summary chip and the dataset measure formatter.
958
+
959
+ **Level.** `minor`, not `major` and not `patch`. This repo keeps its major aligned
960
+ with `@objectstack`'s (see AGENTS.md, "版本号策略") and ships breaking changes as
961
+ `minor` with a written breaking note; `scripts/check-changeset-no-major.mjs`
962
+ enforces that mechanically, and all packages sit in one `fixed` group so a split
963
+ changeset could not produce split levels anyway. `patch` would be wrong because
964
+ values that render correctly today (four-digit percents, every non-`en` session)
965
+ render differently afterwards.
966
+
967
+ **Migration.** If a dashboard stored whole-percent values in the `0`–`1` band and
968
+ relied on the tile's old scaling to print them (a stored `1` shown as `100%`), it
969
+ was already disagreeing with every other percent surface in the console; store the
970
+ fraction (`0.01`) or the points (`1`) consistently and both surfaces now agree.
971
+
972
+ ⚠️ **Dated note, 2026-10-02 — the magnitude guess is retired in this same release — objectui#11475.**
973
+ Later in this same release `percentDisplayValue` (`@object-ui/core`) stopped
974
+ inferring a percentage's storage from the value: it takes the storage as a
975
+ required argument, and every percent face passes the storage the field
976
+ declares (`percentScaleOf` in `@objectstack/spec/data`: a fraction unless the
977
+ field declares a `max` above 1). So a `metric` tile's `%` pattern states a fraction, numeral's own reading, and
978
+ a stored `1` reads `100%` on that tile again; an `object-metric` tile over a
979
+ `percent` field renders at that field's storage. The `1` → `1%` row above no
980
+ longer describes the tile. The rest of this entry is kept as the reading of this change.
981
+ - e356c39: Converge the bare `dashboard` key on `plugin-dashboard`, and retire
982
+ `view:dashboard` with a by-name tombstone (objectui#9533).
983
+
984
+ **Breaking for authored metadata.** `{ "type": "view:dashboard" }` no longer
985
+ renders a dashboard. It now resolves to a refusal that names the spelling and
986
+ names its replacement. `objectui validate` refuses the document at `type`
987
+ (`invalid_union`). `{ "type": "dashboard" }` and
988
+ `{ "type": "plugin-dashboard:dashboard" }` both render `DashboardRenderer`,
989
+ unchanged and newly-working respectively.
990
+
991
+ ⚠️ **Dated note, 2026-09-25 — `objectui check` names the spelling only in a
992
+ file it recognises — objectui#10606.** This entry first said `objectui check`
993
+ reports `{ "type": "view:dashboard" }` as an unknown schema type, and **What
994
+ changed** below said, unscoped, that `check` names the spelling. `check` checks
995
+ the `type` of each file it recognises, and a file whose root carries an ObjectUI
996
+ structural key (`children`, `className`, `body`, …) is recognised by that key
997
+ alone. A file with none of those keys is parsed against the schema and
998
+ recognised only if it validates; one that does not validate is listed by name
999
+ when its root `type` is on the known-type list `check` reads, and is otherwise
1000
+ counted as skipped. `view:dashboard` is withheld from that list, so the document
1001
+ above is counted as skipped ("no ObjectUI recogniser admitted") and gets no
1002
+ unknown-type line, while `{ "type": "view:dashboard", "className": "h-64" }` gets one.
1003
+ `check` exits non-zero on unreadable JSON only; the verdict is
1004
+ `objectui validate`'s.
1005
+
1006
+ **What was wrong.** `apps/console` declares the lazy stub for bare `dashboard`
1007
+ under `plugin-dashboard` — twice, in `preview-gallery.tsx` and
1008
+ `register-plugins.ts` — while this package registered the renderer as
1009
+ `view:dashboard`. Neither site passed `skipFallback`, so both also claimed the
1010
+ bare key (`Registry.register` and `Registry.registerLazy` share the
1011
+ `meta?.namespace && !meta?.skipFallback` branch, and `registerLazy` has no
1012
+ collision check at all, so nothing warned). Two consequences followed, and both
1013
+ were reproduced against a real `Registry` before anything here was written:
1014
+
1015
+ 1. **The bare key was double-claimed, phase-dependently.** After the stub step
1016
+ bare `dashboard` declared namespace `plugin-dashboard`; after the chunk
1017
+ loaded the same key declared `view`. Which answer a host got depended on when
1018
+ it asked — the objectui#6416 shape, which converged `plugin-report` off it.
1019
+ 2. **The namespaced stub was never cleared.** `register()` clears the lazy stub
1020
+ of the type IT registers, and that type was `view:dashboard`. So
1021
+ `hasLazy('dashboard', 'plugin-dashboard')` read `true` after the package had
1022
+ fully loaded and `get('dashboard', 'plugin-dashboard')` read `undefined`,
1023
+ while the generated CLI whitelist listed that spelling as renderable. A node
1024
+ authored with it could only ever paint `Loading plugin-dashboard:dashboard…`
1025
+ forever: `Registry.loadLazy` resolves whether or not the loaded module
1026
+ registered the expected type, and `SchemaRenderer` re-checks `hasLazy` on
1027
+ every pass. That is the objectui#8760 shape — a key that passes every
1028
+ authoring check and fails only in front of a user.
1029
+
1030
+ **What changed.** The renderer registers as `plugin-dashboard:dashboard`, the
1031
+ namespace every sibling plugin, both console stubs and the CLI whitelist already
1032
+ use, so the console stubs are cleared on load and all three claimants of the bare
1033
+ key name ONE full type. The retired `view:dashboard` key answers
1034
+ `RetiredDashboardNodeTombstone` — an inline refusal, plus a `console.error`
1035
+ carrying the same text — registered with `skipFallback: true` so it claims no
1036
+ bare key. Its spelling is withheld from the derived key universe by declaration
1037
+ in `scripts/check-doc-component-types.mjs`, the same disposition the
1038
+ `RETIRED_FIELD_TYPES` tombstones take, so `objectui check` names it in a file it
1039
+ recognises rather than blessing it; `packages/cli/src/utils/known-schema-types.ts`
1040
+ regenerates and loses that one entry.
1041
+
1042
+ **Also published: four new symbols on the package entry.** `src/index.tsx`
1043
+ re-exports `RETIRED_DASHBOARD_NODE_TYPES`, `RetiredDashboardNodeTombstone`,
1044
+ `reportRetiredDashboardNodeType` and `resetRetiredDashboardNodeTypeReports` from
1045
+ `./retired-node-types`, and `exports["."]` is what publishes them — so these are
1046
+ **published** surface, not an internal one, and removing or renaming one later is
1047
+ a breaking change like any other export. They exist for the same reason
1048
+ `@object-ui/fields` publishes its tombstone: the pin has to import the refusal
1049
+ text it asserts rather than restate it.
1050
+
1051
+ **Also visible in the DOM: the `data-obj-type` value on a dashboard grid.** That
1052
+ attribute carries the type the node was AUTHORED with, so the spelling that puts
1053
+ a dashboard grid on the page moves with the registration.
1054
+ `[data-obj-type="view:dashboard"]` no longer selects one — that node now renders
1055
+ the tombstone, which publishes `data-retired-node-type` and no `data-obj-type` at
1056
+ all — while `[data-obj-type="plugin-dashboard:dashboard"]` now does.
1057
+ `{ "type": "dashboard" }` is unchanged and still reads `dashboard`. A stylesheet,
1058
+ a selector or a DOM assertion keyed on the retired value stops matching, and
1059
+ stops matching silently, so it is declared here rather than left to be found.
1060
+
1061
+ **Ruled, not chosen.** Director seat summon #24, batch #152 item 5 letter 1,
1062
+ maintainer 「其他同意」 2026-09-18. The two rejected routes are on the record:
1063
+ converging on `view` would leave the whitelist advertising a dead
1064
+ `plugin-dashboard:dashboard`, and standing one side down resolves the naming
1065
+ contest but not the unsatisfiable stub.
1066
+
1067
+ **`Clause-②: yes`**, declared by that ruling: the published full name moves. Note
1068
+ the shape, because it is unusual — the accept set **narrows** (an authored
1069
+ `view:dashboard` stops resolving) and the clause is `yes` anyway, on the
1070
+ published-name move rather than on a widened surface. `minor` per AGENTS.md's
1071
+ 版本号策略: objectui's own breaking changes take `minor`, with the breaking
1072
+ semantics written out here.
1073
+
1074
+ `packages/plugin-dashboard/src/__tests__/dashboardBareKeyOwnership.test.tsx` is
1075
+ the half that outlives the fix. It reads the package's real declared metadata
1076
+ back out of the registry and replays it into a fresh `Registry` in **both**
1077
+ orders — eager-then-lazy and lazy-then-eager — checking the bare key's declared
1078
+ namespace after every step, so order- and phase-independence are properties under
1079
+ test rather than properties of the file it imports. It also asserts `hasLazy` is
1080
+ false after the load, and asserts the `view:dashboard` refusal **together with**
1081
+ the tombstone's own migration text, because a pin that asserts only "no dashboard
1082
+ rendered" passes identically against a registration that was simply deleted —
1083
+ the one outcome the ruling refuses. `objectui#9264`'s shrink-only bare-name
1084
+ collision ledger loses its row: it is now empty.
1085
+ - 2e471a6: `dashboard` now publishes the authoring inputs its renderer already honours — `widgets`, `label`, `description`, `header`, `globalFilters`, `dateRange`, `refreshIntervalSeconds` — so `validateTree`, the generated `sdui.manifest.json` and `sdui-intrinsics.d.ts` stop warning authors off keys that work (previously only `columns`/`gap`/`className` were published, and every other honoured key drew `unknown-prop`). Each declared key is accepted by the spec's strict `DashboardSchema`, so the manifest never offers a key the save gate refuses. The legacy `title` spelling and the retired `aria` key stay deliberately unpublished and are pinned as such; the `schema.title || schema.label` fallback read is unchanged, so documents in the wild keep rendering their header title.
1086
+ - 91af675: dashboard: honour a widget's declared `chartConfig` on the inline chart relays, not only on the dataset path
1087
+
1088
+ `DashboardWidget.chartConfig` is declared on **every** dashboard widget, but only the ADR-0021 dataset path (`DatasetWidget`) read it. The two inline relays — `DashboardRenderer` and `DashboardGridLayout`, which compose the chart node for a widget bound to inline rows or to a `provider: 'object'` aggregate — mentioned `chartConfig` zero times, so an author who wrote `chartConfig.title` / `.subtitle` / `.description` / `.colors` / `.height` / `.showLegend` / `.showDataLabels` / `.annotations` / `.interaction` on such a widget parsed clean and got nothing on screen.
1089
+
1090
+ Both relays now lower those keys through the same `chartConfigPresentation` whitelist `DatasetWidget` uses (`@object-ui/core`), so one authored chart config means the same thing on every dashboard surface.
1091
+
1092
+ **Behaviour change, stated explicitly** — this is why the bump is `minor` and not a patch: a dashboard whose stored metadata ALREADY carries `chartConfig` on an inline-bound chart widget renders differently after this change. It draws the authored titles, accessible description, palette, plot height, data labels, annotations and interaction toggles that were previously dropped. Widgets that declare no `chartConfig` compose exactly what they composed before.
1093
+
1094
+ Five of the fourteen keys of the spec's `ChartConfigSchema` are not forwarded, and `@objectstack/spec` 17.5.0 refuses all five on a dashboard widget's chart config at parse. `type` is refused because the widget's own `type` already picks the chart family on this path. `xAxis` / `yAxis` / `series` are refused because the widget's derived bindings own the chart's structure: the spec's `DashboardWidgetChartConfigSchema` refuses all three (ADR-0021 · ADR-0049 D2), which answered the precedence question objectstack-ai/objectstack#17385 raised. `aria` is refused because nothing on this path reads it in EITHER spelling — measured by forwarding it anyway, as the nested object and again flattened onto the node's own `ariaLabel` / `ariaDescribedBy` / `role`: neither changed a single attribute on screen, because `ChartRenderer` drops every prop but `schema` and `onChartClick`. Rather than give it a reader, the spec retired the key (objectstack-ai/objectstack#17751).
1095
+ - 1d9100e: Dashboard `table` widget — a numeric **measure** column now sorts **descending
1096
+ on the first click** (objectui#5845).
1097
+
1098
+ The sortable headers shipped in objectui#5827 started every column ascending,
1099
+ which left the card's own motivating complaint — the largest industry rendering
1100
+ last — two clicks away. For a measure in an analytics widget the question a
1101
+ click asks is "who is biggest", so that column now cycles
1102
+ **descending → ascending → the dataset's own order**.
1103
+
1104
+ Unchanged: a **dimension** column, and a measure whose values are not numbers
1105
+ (a `min()`/`max()` over a text field), keep **ascending → descending → the
1106
+ dataset's own order** — the idiom the console's own DataTable uses. The
1107
+ first-click direction is decided by the same measure-and-all-values-are-numbers
1108
+ classification that decides right-alignment, so it follows the column's *role*
1109
+ rather than "it looks like digits": a digit-keyed dimension (a year, a quarter)
1110
+ still starts ascending.
1111
+
1112
+ Blanks still sort **last in both directions** — that arm is now what a measure
1113
+ column's very first click runs — and `aria-sort` reports the direction actually
1114
+ applied.
1115
+ - 62c0fa1: Dashboard `table` widget (dataset-backed, flat grouped table) — four rendering
1116
+ fixes so it reads as a table rather than a raw dump (objectui#5827):
1117
+
1118
+ - **Numeric columns right-align**, header and cells. `tabular-nums` was already
1119
+ applied to every cell and could not do its job while the digits were flushed
1120
+ left. A column qualifies when it is a MEASURE and every non-null value in it
1121
+ is a number — the declared column `type` cannot decide it, because the
1122
+ analytics executor stamps `type: 'number'` on every measure regardless of the
1123
+ aggregate, so a `min()`/`max()` over a text field would have been
1124
+ right-aligned on the strength of a label. Dimensions stay left-aligned even
1125
+ when their bucket keys are digits: a grouping axis is not a quantity.
1126
+ - **Sortable headers.** Each header is a keyboard-reachable button that cycles
1127
+ ascending → descending → back to the dataset's own order, with the console's
1128
+ existing sort indicator and `aria-sort` on the `th`. Blanks sort last in
1129
+ BOTH directions. The sort is client-side over the rows already fetched — the
1130
+ widget issues one `queryDataset` call and paginates nothing, so no round trip
1131
+ is involved.
1132
+ - **The empty-dimension bucket sorts last** in the default order instead of
1133
+ floating to the top. It keeps its `—` label; only its position moves.
1134
+ - **Row hover feedback on every row**, not only on a drillable one. A drillable
1135
+ row keeps its stronger accent fill.
1136
+
1137
+ The CSV export now follows the order the table is showing, which is the same
1138
+ "the CSV is the table's data" convention its cell text already followed.
1139
+ Drill-through is unaffected: rows are reordered as `(row, incoming index)`
1140
+ pairs, so a drill still resolves through the server's parallel raw-value
1141
+ sidecar.
1142
+
1143
+ Unchanged: the cross-tab (a `pivot` with ≥2 dimensions) renderer, the KPI and
1144
+ chart paths, and every non-dataset table in the console.
1145
+ - e719ebd: `data-table` reads the declared `header`; the producers translate `label` into it.
1146
+
1147
+ `TableColumn` declares `header: string` and does not declare `label`. The
1148
+ renderer's column normalization nonetheless read `header: col.header || col.label`,
1149
+ so the same key had one spelling the type admits and one only the runtime did.
1150
+ That alias is gone (objectui#5351), and the translation it used to perform happens
1151
+ once at each producer instead: metadata vocabulary in, adapter vocabulary out.
1152
+
1153
+ **This narrows what `data-table` accepts, so read this if you author `data-table`
1154
+ nodes by hand.** A column spelled `{ label: 'Stage', accessorKey: 'stage' }` on a
1155
+ directly authored `data-table` now renders a **headerless** column over live
1156
+ cells. Spell it `header` — the key `TableColumn` has always declared. Columns
1157
+ reaching `data-table` through `object-data-table`, `object-grid` or a related
1158
+ list are unaffected: those producers resolve `header` for you from the spec's
1159
+ `ListColumnSchema.label`, so every spelling they accepted before they still
1160
+ accept.
1161
+
1162
+ `@object-ui/core` gains `columnHeader()` alongside `columnIdentity()` — the reader
1163
+ producers use to cross that boundary. It is adapter-first (`header` wins over
1164
+ `label`), so an author who addressed the table directly is never overwritten.
1165
+
1166
+ `object-data-table` also gains a fix from the same move: a column carrying a
1167
+ `label` used to render a **blank** header there even while the alias existed,
1168
+ because the widget's field-meta enrichment overwrote the authored `label` before
1169
+ the adapter ever saw it. `{ field: 'stage', label: 'Stage' }` now renders "Stage".
1170
+
1171
+ The sibling `accessorKey: col.accessorKey || col.name` alias is **unchanged** here
1172
+ and still resolves. Retiring it is objectui#5120's remaining step, which is
1173
+ gated on two published skill guides that teach that spelling.
1174
+ - 9e725e0: Dashboard flat `table` widget: render the server's grand total as a `tfoot` row (objectui#5846).
1175
+
1176
+ The flat branch now requests the `[]` marginal grouping — the same server-side
1177
+ totals machinery the cross-tab has used since framework#1753 — and renders the
1178
+ answer under the console's existing `dashboard.total` label. Totals are computed
1179
+ by the server with each measure's TRUE aggregate (a `sum` sums, an `avg`/`min`/
1180
+ `max` reports the whole-set aggregate); nothing is recombined client-side. The
1181
+ row lives in `tfoot`, so it is exempt from sorting, renders below the `—` null
1182
+ bucket, and never joins the drillable rows.
1183
+
1184
+ No footer is rendered when the executor answers no `[]` grouping, and none is
1185
+ requested for a table truncated by `options.limit`, whose visible rows a
1186
+ whole-set total could not be reconciled against.
1187
+
1188
+ ### Patch Changes
1189
+
1190
+ - 061f5e8: fix(dashboard): a dashboard filter declaring `object` now resolves its translated labels; the axis-title half reaches no surface
1191
+
1192
+ `@objectstack/spec` declares two dashboard surfaces translatable — a filter's `object` and an
1193
+ axis `title` — and the Console resolved neither, so an author could set a key the contract
1194
+ documents and nothing happened. They turned out to be **two defects, not one** — a key never
1195
+ read, and a value that bypassed a resolver already in the tree — and they were fixed
1196
+ separately. Only the first reaches an author in this release: see the note at the end.
1197
+
1198
+ **1. `GlobalFilterSchema.object` was never read.** The spec's describe text for the key is
1199
+ "Object whose `fields.<object>.<field>` translation-bundle entry resolves this filter's field
1200
+ label and option labels". `resolveDashboardFilterDefs` names the keys it copies onto a
1201
+ `DashboardFilterDef` and this one was not among them, so the authored value could not reach a
1202
+ renderer even in principle: a dashboard filter declaring `object` and no `label`, viewed on a
1203
+ translated console, painted the RAW FIELD NAME and left its option labels in the authored
1204
+ English. The definition now carries `object` through, and `DashboardFilterBar` resolves the
1205
+ field label through `useSafeFieldLabel().fieldLabel` and the option labels through the same
1206
+ object's `translateOptions` — the convention resolver every list and form already calls, which
1207
+ is the "one resolver path" the key's own spec text asks for. No second resolver was written.
1208
+ Precedence follows that resolver's signature: the translator's bundle wins, the authored label
1209
+ is the fallback. A filter that names no `object` never reaches it and renders exactly as
1210
+ before.
1211
+
1212
+ **2. `ChartAxisSchema.title` bypassed a resolver two modules downstream.** The key is
1213
+ `I18nLabel`, so an inline locale map parses, builds and validates — and `axisPresentation`
1214
+ collapsed it with `labelText`, a first-string-in-KEY-ORDER pick. Measured both ways round on
1215
+ one map: English to a `zh-CN` viewer and Chinese to an `en` viewer, decided by nothing but
1216
+ which key the author typed first. `normalizeChartSchema` already resolves an axis title
1217
+ through `pickLocalized` against the viewer's language, so the map only had to survive the
1218
+ lowering. The fix forwarded it verbatim through `forwardedI18nLabel` — the neighbour in the
1219
+ same module that already carries a chart's own `title` / `subtitle` / `description` for
1220
+ exactly this reason — and that lowering was then removed in the same release (the note at the
1221
+ end).
1222
+
1223
+ This moves one entry of the objectui#4020 first-string-wins ledger. That ledger's
1224
+ justification is "a locale-unaware pick a caller can OVERRIDE", which holds for a series
1225
+ `label` — the report renderer outranks it with objectui#4020's three-level display name — and
1226
+ never held for an axis title, which is spread onto the chart schema and drawn. `seriesPresentation` keeps the pick and
1227
+ is still pinned.
1228
+
1229
+ What an author sees change: a filter that opted in with `object` now shows its translated
1230
+ field and option labels instead of the raw field name and English options. Nothing that omits
1231
+ `object` renders differently, and point 2 changes nothing an author sees (below).
1232
+
1233
+ ⚠️ Point 2 reaches no surface in this release, which also carries objectui#11315 and
1234
+ objectui#11372. `@objectstack/spec` 17.5.0 refuses `chartConfig.xAxis` / `yAxis` / `series` on
1235
+ a dashboard widget, and the dashboard's dataset widget no longer reads them (objectui#11315), so
1236
+ a dashboard draws no authored axis title at all, in any language. That widget was the only
1237
+ caller of `axisPresentation` and `mergeAuthoredPresentation`, and objectui#11372 removes both
1238
+ from `@object-ui/core` in the same release, so the fixed lowering never ships. The react
1239
+ `ObjectChart` tier, which still authors its own axes, never went through that lowering: it hands
1240
+ `xAxis` / `yAxis` to `normalizeChartSchema`, which resolves a locale-map axis title against the
1241
+ viewer's language since objectui#8943 (released alongside this, under its own changeset).
1242
+ `ObjectChart.axisTitleLocale-10132.test.tsx` in `@object-ui/plugin-charts` pins that surface at
1243
+ two languages.
1244
+ - e2bd3e4: fix(plugin-dashboard): a field's `format` is read as a date pattern only on a `date` / `datetime` field
1245
+
1246
+ The dashboard table (`object-data-table`) and the record-detail drawer render a
1247
+ value through `renderFieldValue`, whose date branch fired on any `format` string
1248
+ containing one of the letters Y, M, D, H, m or s, whatever the field's type. A
1249
+ text field hinted `format: 'email'` — which the shared cell-renderer resolver
1250
+ promotes to a `mailto:` link — contains an `m`, so its address went to
1251
+ `formatDate` and was painted as an em dash: the value disappeared. A text field
1252
+ hinted `format: 'money'` holding a number rendered as a 1970 date, an
1253
+ `autonumber` pattern with date tokens such as `ORD-{YYYYMMDD}-{00}` and a `time`
1254
+ field's `HH:mm` rendered as an em dash, all with no error.
1255
+
1256
+ The branch now runs only when the field's type is `date` or `datetime`. Those
1257
+ fields render exactly as before. A field of any other type no longer takes the
1258
+ date branch and falls through to the cell renderer its type and `format` hint
1259
+ select; the currency and percent branches ahead of it are unchanged. A column whose field
1260
+ type is unknown — no object-schema field and no `type` on the column — is no
1261
+ longer date-formatted from its `format` alone: declare `type: 'date'` on the
1262
+ column to keep the date face.
1263
+ - d461b0d: A fixed-currency field shows its own currency in the dashboard table widget and
1264
+ in the record drawer it opens, not the tenant's (objectui#10463).
1265
+
1266
+ `@objectstack/spec` refuses a field-level `currency`, so a fixed currency is
1267
+ declared as `currencyConfig: { currencyMode: 'fixed', defaultCurrency: 'JPY' }`.
1268
+ `buildFieldMeta`, the helper both `ObjectDataTable` and `RecordDetailDrawer`
1269
+ build their field metadata with, read only the flat `currency` and
1270
+ `defaultCurrency` keys and never `currencyConfig`. In a USD tenant, a JPY-fixed
1271
+ amount of 1234 read `$1,234` in the table cell and in the drawer, while the
1272
+ grid and the metric tile read `¥1,234` for the same field.
1273
+
1274
+ `buildFieldMeta` now resolves the field's currency through
1275
+ `resolveFieldCurrency` from `@object-ui/i18n`, the resolver the grid, the
1276
+ metric tile and the currency cell already share. A column's own `currency`
1277
+ still wins over the field's. The resolver is called without the tenant
1278
+ default, so a column `format` that starts with a currency symbol still wins
1279
+ over the tenant currency, as before.
1280
+
1281
+ **Behaviour change.** In the dashboard table widget and the record drawer, a
1282
+ field whose `currencyConfig` is in `fixed` mode now shows
1283
+ `currencyConfig.defaultCurrency` where it used to show the tenant currency. A
1284
+ field in `dynamic` mode, or with a `currencyConfig` that names no
1285
+ `currencyMode`, still shows the tenant currency, because the resolver reads
1286
+ `currencyConfig.defaultCurrency` only in `fixed` mode (objectui#10422). A field
1287
+ that carries both a fixed `currencyConfig` and the legacy top-level
1288
+ `defaultCurrency` now shows the fixed code, which is the resolver's order.
1289
+ - 668dd59: Fix: the editable dashboard grid (`dashboard-grid`, `DashboardGridLayout`) now
1290
+ treats widgets whose data is `{ provider: 'object', object }` the way the
1291
+ read-only `DashboardRenderer` already does (objectui#10528). Before this change
1292
+ the grid turned a table, list or pivot widget with that config into a static
1293
+ node with `data: []`. The tile showed an empty table or cross-tab and sent no
1294
+ query.
1295
+
1296
+ **Table and list widgets now fetch their rows.** The grid builds the same node
1297
+ `DashboardRenderer` builds for the widget: an `object-data-table`, which loads
1298
+ the object's rows through the data source. The props match too:
1299
+
1300
+ - the provider's `filter` (or the widget's `filter`) reaches the query;
1301
+ - the widget's `searchable` and `pagination` are honoured, and a `list` never
1302
+ gets them;
1303
+ - a row click opens the record in a drawer by default, as on the read-only
1304
+ dashboard. `options.drillDown` still overrides this. In edit mode, dragging
1305
+ still starts only from the drag handle.
1306
+
1307
+ **Pivot widgets with that config now show the retired-widget placeholder.**
1308
+ It is the same placeholder `DashboardRenderer` already shows for them ("This
1309
+ widget uses a retired data format. Edit it to bind a dataset."). Under ADR-0021
1310
+ a pivot is bound to a dataset, and a dataset-bound pivot still renders as
1311
+ before.
1312
+
1313
+ Widgets bound to inline rows are unchanged, including static-data pivots.
1314
+
1315
+ ⚠️ **Dated note, 2026-10-02 — the `dashboard-grid` node key is retired — objectui#10859.**
1316
+ Later in this same release objectui#10859 batch 8 (phase 2b) unregistered the `dashboard-grid`
1317
+ node key. `DashboardGridLayout` is unchanged and stays exported, so this fix still holds wherever
1318
+ a host mounts it; it is no longer reachable as a schema `type`. The rest of this entry is kept as
1319
+ the reading of this change.
1320
+
1321
+ ⚠️ **Dated note, 2026-10-03 — the grid's static pivot node states its keys — objectui#11466.** At this change, "Widgets bound to inline rows are unchanged, including static-data pivots" held, and the grid's static-data pivot node spread the widget's `options` whole. Now that node states the keys `PivotTableSchema` declares and `PivotTable` draws, read from `options`, and no other option key reaches it. `.changeset/11466-dashboard-metric-node.md` states what ships. The rest of this entry is kept as the reading of this change.
1322
+ - 4a3d500: `object-metric` (`ObjectMetricWidget`) re-reads its value when the data-invalidation bus (`notifyDataChanged` from `@object-ui/react`) reports a change to the object it aggregates (objectui#10572). A write that bypasses the data source — a page action over raw HTTP, a flow, a server action — used to leave the tile stale until the whole page was remounted.
1323
+ - 9fbbb17: BREAKING (`@object-ui/components`): `RefreshIndicator`'s `ariaLabel` prop is now required and has no default. It used to default to the English literal "Refreshing", so the progress bar on every view that passed no name was announced in English to screen-reader users in every locale (objectui#10580). A `RefreshIndicator` rendered without `ariaLabel` now fails to type-check.
1324
+
1325
+ (The bump is `minor` by this repo's release model: objectui's major follows the `@objectstack` family major, and its own breaking changes ship as `minor` with the breaking semantics stated here.)
1326
+
1327
+ Migration: pass the bar's accessible name from your own translation layer, for example `ariaLabel={t('grid.refreshing')}`. The component renders the string you pass as the bar's `aria-label` and does not translate it.
1328
+
1329
+ `ObjectGrid`, `ListView`, `ObjectChart` and `ObjectDataTable` now name their refresh bar in the active locale. The grid and the list read the `grid.refreshing` and `list.refreshing` keys their pull-to-refresh text already uses. The chart reads `chart.refreshing` and the dashboard data table reads `dashboard.refreshing`, both with no English literal behind them, so a host that renders either one with no i18next instance at all gets the key itself as the bar's name.
1330
+
1331
+ `@object-ui/i18n`: new keys `chart.refreshing` and `dashboard.refreshing` in every built-in locale pack.
1332
+ - 9c78ebe: `object-data-table` sets `TableColumn.masked` on its columns, so a `password` / `secret` column no longer copies, exports, searches, sorts or sizes by its raw value (objectui#10657, which folded objectui#10658).
1333
+
1334
+ The dashboard table widget draws a `password` / `secret` cell as `••••••`, but it never set
1335
+ the flag the table reads. So the table handed the raw value out anyway: Ctrl+C / Cmd+C on
1336
+ the masked cell, its tooltip, the CSV export, the search box (which runs in the browser
1337
+ here) and the header sort all read it, and the column's width grew with it.
1338
+
1339
+ The widget now sets the flag on every column whose cell is drawn as a mask, from
1340
+ `isMaskedFieldType()` in `@object-ui/fields` over the column's authored `type` and the
1341
+ object's field type (a narrow-only union: a column authored as `text` over a `secret`
1342
+ field keeps the flag), for declared and derived columns alike.
1343
+
1344
+ It fails closed. Bound and inline rows are drawn before the object definition arrives, and
1345
+ a failed read never delivers one. In that window the widget cannot tell a masked column
1346
+ from any other, so it sets the flag on every column: no cell copies, shows a tooltip or is
1347
+ exported, the search matches nothing, no header sorts, and widths come from the headers.
1348
+ Not covered: in the same window a field the object declares as `password` / `secret` is
1349
+ drawn as text, because the mask comes from that declaration.
1350
+
1351
+ This is defence in depth. The real guard is still the server: ObjectStack's read mask
1352
+ replaces `secret` and `password` values with its mask before they reach the browser.
1353
+ - 12809a5: `object-data-table` no longer draws an untyped column as text while the object definition is loading or after its read failed: it draws the mask instead (objectui#10657, the objectui#10706 class at this producer).
1354
+
1355
+ A cell here draws from the column's authored `type`, or the object's. Bound and inline
1356
+ rows are drawn before the object definition arrives, and a failed read never delivers
1357
+ one, so in that window a column that authors no `type` drew its value as text, a
1358
+ `password` / `secret` field included. objectui#10657's first change already set
1359
+ `TableColumn.masked` on every column in that window, but the flag withholds; it does not
1360
+ draw.
1361
+
1362
+ Now, in that window, a column with no `type` of its own is drawn as the mask
1363
+ (`MaskedCellRenderer` from `@object-ui/fields`), declared and derived columns alike, and
1364
+ stays so when the read failed: fail closed, never back to text. A column that authors its
1365
+ `type` draws from it, and a column with a `cell` of its own draws what that `cell`
1366
+ returns. Once the definition lands, each column draws from its declared type as before.
1367
+
1368
+ The record drawer a row opens (record drill-down) drew the clicked record's fields from
1369
+ the same unknown types, so after a failed read it printed a `password` / `secret` field
1370
+ as text too. In that window it now draws every value as the mask.
1371
+
1372
+ The pending objectui#10657 `object-data-table` changeset says a `password` / `secret`
1373
+ field is still drawn as text in that window; that no longer holds once this change ships
1374
+ with it.
1375
+
1376
+ This is defence in depth. The real guard is still the server: ObjectStack's read mask
1377
+ replaces `secret` and `password` values with its mask before they reach the browser.
1378
+ - 7afc81d: The dashboard's `object-data-table` reads its rows once per mount, already expanded, and a dashboard filter re-reads its options when `optionsFrom.filter` changes (objectui#10664).
1379
+
1380
+ - `object-data-table` loaded the object definition in an effect of its own and listed it among the fetch effect's dependencies, so every object-bound mount issued two `find` calls, the first without `$expand`. It now reads the definition through `useSettledSchema` from `@object-ui/react` and its query waits until that read settles; a failed read, or an adapter with no `getObjectSchema`, still loads the rows, unexpanded. Its expansion also depends on `columns`, which did not re-run the read: adding a lookup column to a mounted table now re-reads with the new `$expand`, and relabelling a column does not.
1381
+ - The filter bar's option list sends `optionsFrom.filter` on both of its reads, the dataset query and the record fallback, but did not re-read when only that filter changed. It now does, and an equal filter in a new object does not re-read.
1382
+ - 5ad3b88: `object-data-table` refuses `drillDown.filter`, `.maxRows`, `.report` and `target: 'navigate'`, and `object-pivot` refuses `drillDown.mode` (objectui#10685)
1383
+
1384
+ **Breaking for authored metadata, graded `minor` by this repo's convention (AGENTS.md: a
1385
+ breaking change here is `minor`, never `major`):** `object-data-table` no longer accepts
1386
+ `drillDown.filter`, `drillDown.maxRows`, `drillDown.report` or `drillDown.target: 'navigate'`
1387
+ on either published face — the TypeScript `ObjectDataTableSchema.drillDown` and its zod
1388
+ mirror, the door `objectui validate` / `safeValidateSchema` run — so a stored JSON table
1389
+ config that parsed green before is refused by name now. Each refusal names the key and the
1390
+ blocks that do read it. None of the four was ever read by this block. Migration: delete the
1391
+ key; for the drill target write `'drawer'` or `'dialog'`. `enabled`, `mode`, `title` and
1392
+ `columns` are accepted exactly as before.
1393
+
1394
+ Its row drills to the one record it already is, so a drilled list's filter, row cap and
1395
+ report had nothing to act on, and `'navigate'` was drawn as a drawer. Before this change the
1396
+ table's zod mirror took the shared `DrillDownConfigSchema` and accepted all four.
1397
+
1398
+ `object-pivot` is refused at the TypeScript door only. A stored JSON pivot config carrying
1399
+ `mode` is still accepted and ignored at render, as it was before: neither `object-pivot` nor
1400
+ `PivotTableSchema` has a zod mirror, so no validator in this repository reads the members of
1401
+ a pivot's `drillDown`. `mode` never had a read site on a pivot. Every pivot click point is an
1402
+ aggregated bucket (a cell, a row or column header, or a total), so the block always drills
1403
+ through, and `mode`, which chooses drill-to-record for a clicked row, had nothing to choose.
1404
+
1405
+ - `@object-ui/types` adds two per-block shapes beside `ObjectMetricDrillDownConfig`, both
1406
+ published on the root entry `@object-ui/types` and on the `@object-ui/types/data-display`
1407
+ subpath. Each tombstone's docblock names the blocks that do read the key.
1408
+ - `ObjectDataTableDrillDownConfig`: `DrillDownConfig` with `filter?: never`,
1409
+ `maxRows?: never` and `report?: never`, and `target?: 'drawer' | 'dialog'`.
1410
+ `ObjectDataTableSchema.drillDown` is typed with it, and the `drillDown` member of its zod
1411
+ mirror (`@object-ui/types/zod`) refuses the same members by name.
1412
+ - `ObjectPivotDrillDownConfig`: `DrillDownConfig` with `mode?: never`.
1413
+ - The shared `DrillDownConfig` keeps every member for the blocks that read them. Its `mode`
1414
+ docblock now names only `object-data-table` instead of charts, pivot tables and metric
1415
+ cards, none of which read it.
1416
+ - `@object-ui/plugin-dashboard` types `ObjectPivotTable`'s `schema.drillDown` with
1417
+ `ObjectPivotDrillDownConfig`, and the component reads its drill config through that type
1418
+ instead of through `any`. `ObjectDataTable`'s prop is `ObjectDataTableSchema`, so it takes
1419
+ the table's shape with it.
1420
+
1421
+ TSX code now fails to compile in two places. The first is a refused member inside
1422
+ `ObjectDataTableSchema.drillDown`, or inside `ObjectPivotTable`'s `schema.drillDown`. The
1423
+ second is a value typed as the shared `DrillDownConfig` handed to either. Delete the key.
1424
+ Runtime rendering is unchanged.
1425
+
1426
+ ⚠️ **Dated note, 2026-09-28 — the plain `pivot` node has a mirror now — objectui#10859.**
1427
+ Later in this same release `PivotTableSchema` (the `pivot` node, not `object-pivot`) gains a
1428
+ zod mirror whose `drillDown` is the shared `DrillDownConfigSchema`, so a validator does read
1429
+ the members of a `pivot` node's `drillDown`, and accepts `mode` there, as `DrillDownConfig`
1430
+ declares it. `object-pivot` still has no mirror, so what the paragraph above says of a stored
1431
+ `object-pivot` config stands. The rest of this entry is kept as the reading of this change.
1432
+
1433
+ ⚠️ **Dated note, 2026-09-29 — the `pivot` node refuses `drillDown` whole now — objectui#10932.**
1434
+ Later in this same release `drillDown` was retired on the `pivot` node on both faces:
1435
+ `PivotTableSchema.drillDown` is a `?: never` tombstone, and the `pivot` arm of the zod mirror
1436
+ declares it as a `retirementTombstone()`, which refuses the key by name whatever it holds
1437
+ (`invalid_type` at `drillDown`). So "a validator does read the members of a `pivot` node's
1438
+ `drillDown`, and accepts `mode` there" in the 2026-09-28 note above no longer holds: the
1439
+ validator refuses the whole key before reading any member, so it accepts no `mode` on a `pivot`
1440
+ node. Nothing ever drilled a `pivot` node; a pivot drill is authored on `object-pivot`, whose
1441
+ `drillDown` type, `ObjectPivotDrillDownConfig`, refuses `mode` at the TypeScript door, as this
1442
+ entry says. `object-pivot` still has no zod mirror, so what this entry says of a stored
1443
+ `object-pivot` config stands. `.changeset/10932-pivot-drilldown-retired.md` (PR objectui#10972)
1444
+ states what ships; the text above is kept as the reading of this change.
1445
+
1446
+ ⚠️ **Dated note, 2026-10-02 — `object-pivot` has a zod arm now — objectui#11440.**
1447
+ Later in this same release `object-pivot` gained an arm in `@object-ui/types/zod`,
1448
+ `ObjectPivotBlockSchema`, whose `properties.drillDown` is this entry's per-block shape: the shared
1449
+ drill config with `mode` refused by name (`invalid_type` at `properties.drillDown.mode`). So "A
1450
+ stored JSON pivot config carrying `mode` is still accepted" and "neither `object-pivot` nor
1451
+ `PivotTableSchema` has a zod mirror, so no validator in this repository reads the members of a
1452
+ pivot's `drillDown`" above, and "`object-pivot` still has no zod mirror" in the 2026-09-29 note, no
1453
+ longer hold: `safeValidateSchema` and the strict face refuse that `mode`.
1454
+ `.changeset/11440-arm-passing-types.md` states what ships. The rest of this entry is kept as the
1455
+ reading of this change.
1456
+ - 8c3d7fc: fix(plugin-dashboard): `object-pivot` and `object-data-table` re-read on the data-invalidation bus
1457
+
1458
+ The fetch effects of `ObjectPivotTable` and `ObjectDataTable` now name the
1459
+ `useDataInvalidation` nonce for `schema.objectName`, so a write declared on the
1460
+ bus (`notifyDataChanged`, as a page action over raw HTTP does) re-reads the rows
1461
+ in place. Before, both refreshed after such a write only when their host
1462
+ remounted them, and `PageView` is about to stop doing that (objectui#10519).
1463
+
1464
+ The pivot's element-bound spelling (`dataSource: { object }`) resolves onto the
1465
+ same key. The table is also what a `dashboard` block draws for a table widget
1466
+ over `{ provider: 'object' }` and what a drill-down drawer lists the underlying
1467
+ records with, so those re-read too. A drill that opens a report
1468
+ (`drillDown.report`) and the single-record drawer a table row opens do not
1469
+ re-read. Bound rows and authored `data` rows do not subscribe.
1470
+
1471
+ ⚠️ **Dated note, 2026-09-27 — the report a drill opens does re-read — objectui#10853.**
1472
+ The sentence above that a drill opening a report (`drillDown.report`) does not
1473
+ re-read stopped holding with objectui#10814 (PR objectui#10836), pending in the
1474
+ same release: the drawer's `drillDown.report` arm renders `spec-report` through
1475
+ `SchemaRenderer`, and the dataset report renderer's own reader re-reads it on the
1476
+ bus. `drillDownReport.invalidationRefetch-10814.test.tsx` in `apps/console` pins
1477
+ that through the real drawer. The single-record drawer a table row opens still
1478
+ does not re-read. The rest of this entry is kept as the reading of this change.
1479
+
1480
+ ⚠️ **Dated note, 2026-10-02 — the drawer renders a `report` node — objectui#11440.**
1481
+ Later in this same release the drawer's `drillDown.report` arm renders `{ type: 'report', report }`
1482
+ through `SchemaRenderer`, not `spec-report`, which `@object-ui/plugin-report` retired. So "renders
1483
+ `spec-report` through `SchemaRenderer`" in the 2026-09-27 note no longer holds; the report it opens
1484
+ still re-reads on the bus, as that note says. `.changeset/11440-drill-drawer-report-node.md` states
1485
+ what ships. The rest of this entry is kept as the reading of this change.
1486
+ - 256b4c9: A filter refused as malformed (`INVALID_FILTER`) no longer throws out of render or out of a click
1487
+ handler at the sites that merge filters there. That holds whether this layer's filter converter
1488
+ refused it or, at the drill seam, the spec's own `parseFilterAST`. Each site now surfaces the
1489
+ refusal the way its siblings already do (objectui#10789). The filter stays **refused**: nothing
1490
+ that was refused is accepted, and no site falls back to "no filter".
1491
+
1492
+ - **`ElementDataSourceGate` / `useElementDataSource`** (`@object-ui/react`): the gate's merge of
1493
+ the component's `filter` with the composed binding, and the merge of a saved view's filter
1494
+ with the binding's own, lowered through the throwing converter inside render. A refused
1495
+ filter from either now makes the binding `missing` with a new optional `filterRefusal` on the
1496
+ result (`error` carries its message). The gate then draws, in place of the block, the
1497
+ malformed-filter notice the wrapped blocks draw: `view.malformedFilter`, naming the refused
1498
+ operator or field. `element:record_picker`, which reads the same hook, shows its
1499
+ configuration-error panel with the refusal.
1500
+ - **`PeoplePicker` / `RecordPickerDialog`** (`@object-ui/fields`): the recents merge and the
1501
+ dialog's `mergedFilter` lower through `toFilterNodeSafely`; a refusal is shown in the picker's
1502
+ existing error state and no query runs without the filter.
1503
+ - **Drill-downs** (`ObjectChart`, `ObjectPivotTable`, `DatasetWidget`, `DatasetReportRenderer`):
1504
+ a widget or report filter refused at the drill seam made the drill click throw. The converter
1505
+ refuses some (a spec `$not`, say). The spec's `parseFilterAST` refuses others, such as a scalar
1506
+ on a list operator in either dialect: `[['stage', 'in', 'won']]` or `{ stage: { $in: 'won' } }`.
1507
+ The drill is now not opened or emitted (never with the scope dropped), and the refusal is logged
1508
+ with `console.warn`, naming the operator. `composeDrillFilter` (`@object-ui/core`) now throws
1509
+ one refusal type: the spec's `INVALID_FILTER` error is re-raised as a `FilterOperatorError` with
1510
+ its message and the same `INVALID_FILTER` / 400 envelope, and any other error passes through
1511
+ unchanged.
1512
+ - **`ListView` export** (`@object-ui/plugin-list`): the server export builds its filter inside
1513
+ its `try`, so a refusal is named in the export menu instead of escaping the click.
1514
+ - **`convertFiltersToAST`** (`@object-ui/core`): an `$or` member that is the TRUE identity no
1515
+ longer stops the members after it from being read. `{ $or: [{}, { a: {} }] }` is now refused
1516
+ like `{ $or: [{ a: {} }, {}] }` already was, instead of answering TRUE (every row); a TRUE
1517
+ disjunct beside members that lower still absorbs its `$or`.
1518
+ - c5ec15c: fix: every `Calendar` picker opens on its selected date's month, not today's (objectui#10799)
1519
+
1520
+ react-day-picker opens on `month`, else `defaultMonth`, else today, and
1521
+ `selected` does not move it. Five `Calendar` mounts passed `selected` only, so
1522
+ a value outside the current month opened the picker on today's month: with
1523
+ the calendar on 4 March 2020 and today in September 2026, the header of
1524
+ `CalendarView` read `March 2020` while the popover under it opened on
1525
+ `September 2026`, with no selected day in view.
1526
+
1527
+ Each mount now passes its first selected day as `defaultMonth`:
1528
+
1529
+ - `CalendarView`'s header date popover (`@object-ui/plugin-calendar`) opens on
1530
+ the date the header names, and follows Previous / Next between opens;
1531
+ - `ui:calendar` opens on its day in `single` mode, on the first valid listed
1532
+ day in `multiple` mode (an entry that is not a date is skipped) and on `from`
1533
+ in `range` mode. It reads that month once, when it mounts: a value that
1534
+ changes afterwards does not move the month it shows;
1535
+ - the form date picker (`date-picker`) and `DatePicker` open on their value;
1536
+ - the dashboard date filter's custom-range calendar
1537
+ (`@object-ui/plugin-dashboard`) opens on the range's `from`.
1538
+
1539
+ With no value, each still opens on today. react-day-picker throws on an
1540
+ invalid `defaultMonth`, so a day that names no instant is not passed as one: an
1541
+ unparseable `ui:calendar` day, a range `from` that does not parse, and an
1542
+ invalid `Date` a host hands `CalendarView` each open on today, as before. The
1543
+ two date pickers are unchanged in this respect: an invalid value already fails
1544
+ in their trigger's label, before any calendar mounts. The `Calendar` primitive
1545
+ itself is unchanged.
1546
+ - d425b8c: fix(plugin-dashboard): a `dashboard` block on a page loads its dataset widgets, and they re-read on the data-invalidation bus
1547
+
1548
+ `DashboardRenderer` read its adapter from its `dataSource` prop only. A page
1549
+ renders its blocks through `SchemaRenderer`, which passes no adapter prop, so a
1550
+ `dashboard` block held on a page sent no `queryDataset` call and every
1551
+ dataset-bound widget showed "This data source does not support dataset
1552
+ queries." although the page had a capable adapter. The renderer now resolves
1553
+ its adapter the way `object-grid`, `object-form` and `detail-view` do
1554
+ (`useResolvedDataSource`): the prop first, the `SchemaRendererProvider` context
1555
+ second. The widgets, the filter bar's `optionsFrom` read and the drill drawers
1556
+ all get that one adapter. A host with no capable adapter anywhere still sees
1557
+ the alert.
1558
+
1559
+ `DatasetWidget`'s query now also names the `useDataInvalidation` nonce for the
1560
+ dataset's base object (the `object` its answer names), so a write declared on
1561
+ the bus (`notifyDataChanged`) re-reads the tile in place. This holds on both
1562
+ dashboard surfaces. The current rows stay on screen under a refresh indicator
1563
+ during the re-read, not the loading skeleton. A widget whose adapter cannot run
1564
+ dataset queries subscribes to nothing.
1565
+ - fab627f: The dashboard date filter's "Custom…" item now opens the range calendar,
1566
+ including over a stored custom range, and the calendar stays open
1567
+ (objectui#10843).
1568
+
1569
+ Two defects, both measured in Chromium. With a custom range stored, the
1570
+ select's value already is the "Custom…" item, and Radix Select reports a pick
1571
+ only when it changes the value, so picking it again did nothing: a stored range
1572
+ could not be reopened for editing. From a preset, picking "Custom…" from the
1573
+ list did open the calendar, but the select then returned focus to its trigger,
1574
+ and focus leaving the calendar's popover dismissed it as soon as the select
1575
+ finished closing.
1576
+
1577
+ Picked from the list (pointer, touch, Enter or Space), "Custom…" now opens the
1578
+ calendar whether the value changes or not, and only after the list has closed,
1579
+ so the calendar keeps focus. Typing its first letter on the focused, closed
1580
+ select still opens the calendar when that changes the value (from a preset or
1581
+ "All time"), as it did before this change. A stored range reopens on the month
1582
+ of its first day. Closing the calendar with Escape returns focus to the date
1583
+ select. Closing the select without picking "Custom…" (Escape, a click outside)
1584
+ still opens nothing, and picking a preset commits it as before.
1585
+ - a60539b: fix(plugin-dashboard,components,plugin-report): three more read sites show the day a stored date-only value names, in every zone (objectui#10844)
1586
+
1587
+ Each read below parsed a date-only `YYYY-MM-DD` string with the engine's own `Date`
1588
+ parse, which reads it as UTC midnight. West of UTC that is the evening before, so
1589
+ each face showed the previous day. Each now reads the value through `toDisplayDate`
1590
+ (`@object-ui/core`), which rebuilds a date-only string at local midnight of the day it
1591
+ names. For a real day, a viewer in UTC or east of it sees no change.
1592
+
1593
+ - **Dashboard date filter, custom range.** The range calendar highlighted the day
1594
+ before each stored bound and could open on the previous month: clicking 15 Sep stored
1595
+ `2026-09-15` and then highlighted the 14th. It now highlights and opens on the stored
1596
+ days. A bound naming a day its month does not have (`2026-02-30`) no longer rolls
1597
+ into March: it selects no day, and a `from` like that opens the calendar on today, as
1598
+ an unparseable `from` already did.
1599
+ - **`date-picker` renderer, an ISO string `value`.** An authored or bound
1600
+ `value: '2024-01-15'` was labelled "January 14th, 2024" and selected the 14th. It now
1601
+ labels and selects the 15th. Only a date-only string naming a real day is read this
1602
+ way: a `Date`, a date-time string and any other string reach date-fns exactly as
1603
+ before. So an unparseable string still makes the trigger's `format` throw, as it did
1604
+ before, and a date-only string naming a nonexistent day is still rolled forward
1605
+ rather than refused, because refusing it would make `format` throw where it rendered
1606
+ before.
1607
+ - **Report cell date face (`formatValue`).** The `yyyy-MM-dd` face `ReportViewer` gives
1608
+ an untyped ISO-looking value and an aggregated column read `2026-09-14` for a stored
1609
+ `2026-09-15`. It now reads `2026-09-15`. A value naming a nonexistent day now renders
1610
+ as the raw stored string, this face's existing answer for an unparseable value,
1611
+ instead of a rolled-forward day.
1612
+
1613
+ A date-time value (one with a time part) still renders in the viewer's zone at every
1614
+ site.
1615
+ - 9f0c84a: fix(plugin-dashboard): a dashboard filter's `optionsFrom` options re-read on the data-invalidation bus
1616
+
1617
+ A `select` or `lookup` entry in a dashboard's `globalFilters` that reads its
1618
+ options through `optionsFrom` now re-reads them when the bus reports a change to
1619
+ `optionsFrom.object` (`notifyDataChanged`, as a page action over raw HTTP
1620
+ does), or an unscoped `'*'`, on either read: the server-side dataset GROUP BY
1621
+ and the client-side `find` fallback. A change to another object does not
1622
+ re-read. The options on screen stay until the re-read answers, and the value the
1623
+ user selected is kept: it is the dashboard variable's, and the read never writes
1624
+ it. Before, the options refreshed after such a write only when the host
1625
+ remounted the dashboard, and `PageView` is about to stop doing that
1626
+ (objectui#10519).
1627
+
1628
+ A filter with authored `options` and no `optionsFrom` reads nothing and does not
1629
+ subscribe.
1630
+
1631
+ **Clause-②: no** — no exported symbol, prop or authored key is added, removed,
1632
+ renamed or retyped, and no accept set moves. What changes is when an existing
1633
+ read runs.
1634
+ - ac15833: fix(data-objectstack, plugin-dashboard): a dataset tile the viewer may not read shows a localized "no access" state (objectui#10899)
1635
+
1636
+ When the analytics read admission refused a dashboard dataset query — `403`
1637
+ with ADR-0112 `PERMISSION_DENIED` — `queryDataset` had no branch for that code,
1638
+ so it threw the generic `Dataset query failed: 403 Forbidden — [Analytics]
1639
+ Access denied: …` string, and `DatasetWidget` printed it verbatim in a red
1640
+ alert. The list view over the same object already says 「无权访问」.
1641
+
1642
+ - `queryDataset` now throws a typed `AnalyticsForbiddenError` for that code,
1643
+ carrying `httpStatus: 403` and `code: 'PERMISSION_DENIED'` (plus the server's
1644
+ code and message for diagnostics). A code-less 403 — no ObjectStack route
1645
+ wrote it — keeps the generic error.
1646
+ - `DatasetWidget` classifies a failed query with the shared `classifyLoadError`
1647
+ and, for `forbidden`, renders a localized no-access state
1648
+ (`dashboard.widgetForbiddenTitle` / `dashboard.widgetForbiddenMessage`, new in
1649
+ all ten packs) instead of the exception text. Every other failure keeps the
1650
+ detailed alert.
1651
+ - 6466a09: fix(plugin-dashboard): the auto-refresh interval reads its handler through a ref, not a memoised identity
1652
+
1653
+ `useDashboardAutoRefresh`, the one timer behind `DashboardGridLayout` and
1654
+ `DashboardRenderer`, armed its interval in an effect keyed on
1655
+ `[seconds, onRefresh, handleRefresh]`, and `handleRefresh` is a `useCallback`
1656
+ result. AGENTS.md #10 bans exactly that: React may throw a memo away and hand
1657
+ back a new function while nothing it depends on changed, and every such
1658
+ identity change cleared the interval and set it again, restarting its phase. A
1659
+ host that passed a new `onRefresh` identity at the same period restarted it the
1660
+ same way.
1661
+
1662
+ The interval is now keyed on the two values it reads, the period and whether a
1663
+ handler is wired. The handler is read through a ref when a run happens, so the
1664
+ run still calls the CURRENT `onRefresh`, and a new handler identity, from the
1665
+ host or from a discarded memo, leaves the phase where it was. The manual
1666
+ "Refresh All" button reads the same ref.
1667
+
1668
+ The existing timer pin keeps every case green, the equal-period phase case and
1669
+ the handler-swap case among them, and adds one case per surface that forces a
1670
+ memo discard mid-period and then swaps the host handler mid-period, asserting
1671
+ both times that the run still lands on the original schedule.
1672
+ - f61dab1: `reference` is now the only spelling ObjectUI writes or reads for a relational field's target object (objectui#11070, round 4, under the objectui#6837 ruling: 「objectui不是前端的项目吗?后端的元数据只要对,前端按协议执行就行了呀」).
1673
+
1674
+ - **Types.** `LookupFieldMetadata`, `MasterDetailFieldMetadata` and `DetailViewField` (with its zod mirror `DetailViewFieldSchema`) declare `reference` and no longer declare `reference_to`. On the two field metadata types the member is typed by reference to `@objectstack/spec`'s `FieldSchema.reference`. `@object-ui/plugin-form`'s `FieldDefaultsSchemaLike` drops its `reference_to` member the same way.
1675
+ - **Readers.** `LookupField`, `UserField`, `LookupCellRenderer`, `UserCellRenderer`, the inline editor's reference fallback, the form's `current_user` seeding and the inline-subform parent lookup read `reference` alone.
1676
+ - **Emitters.** Every in-repo producer that builds a field definition or a widget `field` prop writes `reference`: the action-param dialog (`paramToField`), the bulk-action dialog, the record detail page, drawer, footer, related list and synthesised page, the gallery card, the form's section-field override and the flow designer's reference picker. The grid's and the dashboard's relational copy sets carry `reference` and no longer copy `reference_to`.
1677
+ - **Ingestion.** `normalizeFieldReferenceKeys` (behind `ObjectStackAdapter.getObjectSchema` and `MetadataProvider`) still folds a legacy `reference_to` / `referenceTo` onto `reference` when `reference` is absent, and still warns in dev. It no longer stamps `reference_to` onto every relational definition, and it still never drops or overwrites a key.
1678
+
1679
+ `@objectstack/spec`'s `FieldSchema` refuses `reference_to` by name, and objectstack#13847 rewrites stored ones on the serve path and in `os migrate meta`, so a definition served by an ObjectStack backend is unaffected.
1680
+
1681
+ ## ⚠️ BREAKING for a host that hands `reference_to` to the widgets directly
1682
+
1683
+ The type change is a compile error for TypeScript that writes `reference_to` on `LookupFieldMetadata`, `MasterDetailFieldMetadata` or `DetailViewField`: rename it to `reference`.
1684
+
1685
+ At runtime the break reaches exactly one kind of host: one that serves object definitions spelling the target only as `reference_to` through a `DataSource` other than `ObjectStackAdapter`, or that passes such a definition straight into `LookupField`, `UserField` or a cell renderer. Those definitions never pass the ingestion fold. Measured with an object-bound lookup field on `ObjectForm` and a lookup column on `ObjectGrid`, both fed by a hand-written `DataSource` whose `getObjectSchema` returns `{ type: 'lookup', reference_to: 'account' }`:
1686
+
1687
+ - before this change, opening the picker queried `account` and the cell resolved the record's name;
1688
+ - after it, the picker has no object to query (no `find` call is made) and the cell shows the raw id beside the unresolved-reference marker.
1689
+
1690
+ The same definition served through `ObjectStackAdapter.getObjectSchema` still works, before and after: the fold adds `reference`, the picker queries `account`, the cell resolves the name, and the dev warning names the field. **Fix:** spell the target `reference` in the definition your `DataSource` serves.
1691
+
1692
+ ⚠️ **Dated note, 2026-09-30 — the break reaches more than the paragraph above states — objectui#11070.** Two corrections from the contract review of this change, appended rather than edited in; neither changes what ships.
1693
+
1694
+ - "The break reaches exactly one kind of host" above is too narrow. Host code that read `reference_to` off a definition after ingestion — from `ObjectStackAdapter.getObjectSchema()` or `useMetadata().objects` — also loses that key, because the ingestion pass no longer stamps it (the **Ingestion** bullet above states the fact, and `reference_to` was never a declared member of those definitions). **Fix:** read `reference`.
1695
+ - The measured break above is a `lookup` field's. A `user` field handed a `reference_to`-only definition directly does not end at "no query": `UserField` falls back to `sys_user`, so its picker queries `sys_user` rather than the object `reference_to` named. **Fix:** spell the target `reference` there too.
1696
+
1697
+ The text above is kept as the reading of this change.
1698
+ - dd5ff19: The text-family field types and every length reader use `@objectstack/spec`'s own `minLength` / `maxLength`, and the snake_case `min_length` / `max_length` are retired at once, with no alias (objectui#11070, the text-family round). Two switches nothing read, `auto_compute` and `auto_update`, are retired too.
1699
+
1700
+ - **Types.** `TextFieldMetadata` and `TextareaFieldMetadata` declare `minLength` and `maxLength`; `MarkdownFieldMetadata`, `HtmlFieldMetadata`, `RichtextFieldMetadata`, `EmailFieldMetadata` and `UrlFieldMetadata` declare `maxLength`. Each is `FieldSchema`'s member by reference, and each replaces the snake_case member the type declared before. `FormulaFieldMetadata.auto_compute` and `SummaryFieldMetadata.auto_update` are removed: nothing in ObjectUI read either.
1701
+ - **Readers.** The form renderer's built-in `input`, `textarea` and fallback branches, the form's validation rules (`buildValidationRules`), `TextAreaField`, `RichTextField`, `EmbeddableForm`'s default long-text cap and `ObjectForm`'s length forwarding read `maxLength` / `minLength` alone. The built-in branches no longer strip a `max_length` key off the element: nothing reads it, so it is treated like any other undeclared key.
1702
+ - **Dashboard.** `ObjectDataTable` and `RecordDetailDrawer` no longer copy a lookup's `display_field` onto their internal cell meta. The lookup cell has read the display pointer as `displayField` since objectui#7155, so nothing changes on screen.
1703
+
1704
+ `FieldSchema` refuses `min_length`, `max_length`, `auto_compute` and `auto_update` by name, so no spec-compliant producer writes them. A definition served through `ObjectStackAdapter.getObjectSchema` or `MetadataProvider` is unaffected even if it was stored with a snake_case length: the ingestion pass folds `max_length` / `min_length` onto `maxLength` / `minLength` before any reader sees it. Measured with an object-bound `textarea` field declaring `max_length: 111` on `ObjectForm`, served through `ObjectStackAdapter`: the editor has `maxlength="111"` and a `0/111` counter before and after this change.
1705
+
1706
+ ## ⚠️ BREAKING, priced as minor under the fixed group's version policy
1707
+
1708
+ The type change is a compile error for TypeScript that writes `min_length`, `max_length`, `auto_compute` or `auto_update` on one of these types (an excess-property error naming the key). Rename the first two to `minLength` / `maxLength`, and delete the other two.
1709
+
1710
+ At runtime, a length spelled only `max_length` or `min_length` now applies nowhere the ingestion fold does not run first. Measured before and after this change:
1711
+
1712
+ - **A hand-authored `form` document handed straight to the renderer.** A built-in `input` field with `max_length: 5` and a `textarea` field with `max_length: 7` rendered `maxlength="5"` and `maxlength="7"`. They now render no `maxlength`, and the element carries `max_length` as an inert attribute, as it would any key the renderer does not read. The same fields spelled `maxLength` render identically before and after.
1713
+ - **An object definition served to `ObjectForm` by a `DataSource` other than `ObjectStackAdapter`.** A `textarea` field with `max_length: 111` had `maxlength="111"` and a `0/111` counter; it now has neither. The same field spelled `maxLength` is unchanged.
1714
+ - **Submit-time validation.** `buildValidationRules` on a field carrying only `max_length: 9` / `min_length: 2` produced `maxLength` and `minLength` rules; it now produces no rules.
1715
+ - **`EmbeddableForm`.** A custom `textarea` field carrying `max_length: 40` used to opt out of the default cap and got no `maxLength`; it now gets the 5000-character long-text default.
1716
+
1717
+ **Fix:** spell the bounds `maxLength` / `minLength`.
1718
+ - cff8641: The dashboard's refresh button now speaks the session language (objectui#11097). `DashboardRenderer` and `DashboardGridLayout` hard-coded "Refresh All", "Refreshing…" and the accessible name "Refresh dashboard", so a zh-CN session read English on the button that every console dashboard shows since the console began wiring `onRefresh`. Both components now read the pack keys `dashboard.refreshAll`, `dashboard.refreshDashboard` and the existing `dashboard.refreshing`. The English wording is unchanged.
1719
+
1720
+ `@object-ui/i18n`: new keys `dashboard.refreshAll` and `dashboard.refreshDashboard` in every built-in locale pack.
1721
+ - b149617: fix(plugin-dashboard): the `object-metric` tile shows a percent or number aggregate at the field's declared width
1722
+
1723
+ A metric tile over a `percent` or `number` field picked its pattern from the
1724
+ field's type alone, so it never showed the decimals the field declares. A
1725
+ `number` field declaring `scale: 2` whose average is 3.75 read `4`, and a
1726
+ `percent` field declaring `scale: 2` read `12%` beside list cells reading
1727
+ `12.34%`.
1728
+
1729
+ The tile now reads the field's width through `resolveFieldScale` from
1730
+ `@objectstack/spec/data`, the same answer the list cell, the record header chip,
1731
+ the grid footer and the percent edit widget use:
1732
+
1733
+ - a declared `scale` shows as declared: `3.75`, `12.34%`, and `scale: 3` pads to
1734
+ `1.500`;
1735
+ - a `percent` that declares nothing keeps whole percents, the protocol's width;
1736
+ - a `min` or `max` over a `number` that declares nothing shows the value at its
1737
+ own decimals (`0.99`, not `1`), since that value is one of the records' own
1738
+ values.
1739
+
1740
+ A `sum` or `avg` over a `number` that declares no `scale` keeps its
1741
+ whole-number display: the tile does not see the values the server aggregated,
1742
+ so their widths are not known to it. An authored `format` on the widget still
1743
+ wins, and a `count` is still a plain number of rows.
1744
+
1745
+ `@object-ui/plugin-dashboard` now depends on `@objectstack/spec` at runtime,
1746
+ from `^17.5.0`, the first release exporting `resolveFieldScale`.
1747
+ - b4333ab: fix(plugin-dashboard): a dimensionless table renders a row of every measure, and a dimensionless chart one mark per measure
1748
+
1749
+ `DatasetWidget` treated every widget that declares no `dimensions` as a
1750
+ one-number tile. So `{ type: 'table', dataset: 'sales', values: ['revenue', 'cost', 'margin'] }`
1751
+ rendered `revenue` alone, and `cost` and `margin` were queried and never
1752
+ shown. Every door accepts that widget, and the spec's metric-family arity
1753
+ refusal sends authors to it: "`type: 'table'` renders a row of measures, and
1754
+ the chart families (`bar` / `line` / `area` / `combo`) render one mark per
1755
+ measure".
1756
+
1757
+ Only a metric-family type is now a tile by type. With no dimension and two or
1758
+ more measures:
1759
+
1760
+ - a `table` / `pivot` renders one row carrying every measure, with no totals
1761
+ footer (that one row is the grand total);
1762
+ - a `bar` / `line` / `area` / `combo` chart plots one mark per measure, with the
1763
+ measures' labels on the category axis, under one series labelled with the
1764
+ existing `dashboard.total` string. A comparison the widget asks for is drawn
1765
+ beside each measure.
1766
+
1767
+ Unchanged: a widget with one measure and no dimension is still a tile on every
1768
+ type, since nothing is dropped. A dimensionless widget of a type the spec's
1769
+ text does not name (`pie`, `donut`, `funnel`, `scatter`, `column`,
1770
+ `horizontal-bar`, `radar`, `treemap`, `sankey`) also keeps its tile. The query
1771
+ still selects exactly the declared measures.
1772
+ - 6e9c8d2: fix: the widget width / height editors write a whole four-number `layout` (objectui#11388)
1773
+
1774
+ The three dashboard widget width / height editors (Studio's widget inspector in
1775
+ `@object-ui/app-shell`, `DashboardWithConfig` in `@object-ui/plugin-dashboard` and
1776
+ `DashboardEditor` in `@object-ui/plugin-designer`) each spread the one edited
1777
+ dimension onto the widget's possibly-absent `layout` and cast the result past the
1778
+ declared four-number type. On a widget with no `layout`, which is how Studio adds
1779
+ one, they stored `{ w }` or `{ h }`, and `@objectstack/spec`'s widget `layout`
1780
+ refuses that box at `x`, `y` and the other dimension.
1781
+
1782
+ `@object-ui/types` gains two exports on its main entry:
1783
+
1784
+ - `defaultWidgetPlacement(index)`: the box the dashboard grid auto-places the
1785
+ widget at that position of `widgets[]` in when it has no `layout`, the fallback
1786
+ the spec's widget `layout` states (`x: (i % 4) * 3`, `y: Math.floor(i / 4) * 4`,
1787
+ `w: 3`, `h: 4`).
1788
+ - `completeWidgetLayout(layout, patch, placement)`: the whole box after an edit,
1789
+ each coordinate taken from the edit, else from the stored `layout`, else from
1790
+ the placement.
1791
+
1792
+ All three editors now write through `completeWidgetLayout`, seeding from
1793
+ `defaultWidgetPlacement` for the widget's index, and the casts are gone, so the
1794
+ compiler judges each write. A widget that has a `layout` keeps its `x` and `y`.
1795
+ The width and height inputs (and `DashboardWithConfig`'s sliders) show the same
1796
+ completed box, so a widget with no `layout` now shows the auto-placed 3 × 4 where
1797
+ it showed 1 × 1, and the dimension an edit leaves alone is the one on screen.
1798
+ `DashboardGridLayout` places a widget with no `layout` through
1799
+ `defaultWidgetPlacement`, with no change to where it lands.
1800
+
1801
+ `DashboardWithConfig`'s `onWidgetSave` payload keeps its flattened `layoutW` /
1802
+ `layoutH` shape. For a widget with no `layout` that the author did not resize,
1803
+ the two values it carries are now the auto-placed 3 and 4, not 1 and 1.
1804
+ - 175df47: fix(plugin-dashboard): a dimensioned `pie` / `donut` / `funnel` / `treemap` / `sankey` widget with several measures now says which measures it drops (objectui#11417)
1805
+
1806
+ A dataset-bound chart widget hands every measure it declares to the shared chart
1807
+ renderer as a series. On the `pie` / `donut`, `funnel`, `treemap` and `sankey` families
1808
+ the renderer draws the first series and no other, so a widget such as
1809
+ `{ type: 'pie', dataset, dimensions: ['stage'], values: ['revenue', 'cost'] }` drew
1810
+ revenue per stage and nothing of cost. The query computed both measures, every door
1811
+ accepted the document, and nothing said that cost was dropped.
1812
+
1813
+ `DatasetWidget`'s dropped-measure warning (objectui#8894) now covers that shape. It names
1814
+ the widget, the measure it renders and the measures it queried and never displayed, and
1815
+ says that the chart family draws a single series, the first declared measure. It points
1816
+ to no ADR-0087 entry for this shape: the metric-family entry the tile warning points to
1817
+ answers for a one-number tile, and a chart with a dimension is not one. A `pyramid`
1818
+ widget, which renders as a funnel, is covered too.
1819
+
1820
+ **Nothing is drawn differently.** The chart still receives every measure and draws what
1821
+ it drew before. A single measure on these charts stays silent, and so does every chart
1822
+ that draws each measure (`bar`, `line`, `area`, `combo`, `radar`, and so on). A `scatter`
1823
+ with several measures is unchanged: the chart already shows a notice naming the series
1824
+ it cannot plot.
1825
+
1826
+ The spec is to refuse two or more measures on these five types with a dimension
1827
+ (objectstack#21293); until objectui's pin carries that refusal, every door accepts the
1828
+ shape and this warning is the signal.
1829
+ - 9d9ed54: The drill-down drawer renders a `drillDown.report` as a `report` node, `{ type: 'report', report }` (objectui#11440).
1830
+
1831
+ It used to render a `spec-report` node, the alias `@object-ui/plugin-report` retires in this same release. The drilled report still carries the widget's resolved filter in its `filter` member, joined to the report's own `filter` with `$and` when it has one; a dataset-bound report does not apply that member (objectui#5137), which this release does not change (objectui#11506). What the drawer draws is unchanged, provided `@object-ui/plugin-report` is at this release too: an older `ReportRenderer` unwraps the `report` member only on `spec-report`. Nothing authored changes. The README's `object-pivot` row no longer calls the block an internal wrapper: it is a public block whose props go in `properties`.
1832
+
1833
+ ⚠️ **Dated note, 2026-10-02 — the drilled report is scoped by `runtimeFilter` — objectui#11506.**
1834
+ Later in this same release the drawer writes the widget's resolved filter into
1835
+ a dataset-bound report as `runtimeFilter`, joined to the report's own
1836
+ `runtimeFilter` with `$and` when it has one, and the dataset renderer applies
1837
+ it: a drill from a filtered widget into a dataset report queries the slice that
1838
+ was clicked. The sentence above that says the drilled report carries the filter
1839
+ in its `filter` member, which a dataset-bound report does not apply and which
1840
+ "this release does not change", no longer describes the drawer. The same change
1841
+ retires the pre-9.0 `objectName` drill report, and the drawer now recognises a
1842
+ report by its `dataset`. The rest of this entry is kept as the reading of this change.
1843
+ - 6007dd4: fix(fields): a read-only percent or currency field shows its value the way its table cell does, and a whole currency amount keeps its minor units (objectui#11444)
1844
+
1845
+ A read-only `PercentField` printed the percentage with a literal `%` and no display
1846
+ locale. A field declaring `scale: 2` and holding `0.25` read `25.00%` in a German
1847
+ read-only form while its table cell read `25,00 %`, and a four-digit percentage lost
1848
+ its grouping (`1234.5%` against the cell's `1,234.5%`). The read-only face now renders
1849
+ through the same call the cell's `formatPercent` uses, so the locale's percent
1850
+ convention, its grouping and the declared width match. The width is still the one
1851
+ `resolveFieldScale` answers, and the face still reads a value as a fraction unless the
1852
+ field declares `max > 1`.
1853
+
1854
+ A read-only `CurrencyField` and `CurrencyCellRenderer` disagreed on a whole amount: the
1855
+ form read `$3,456.00` while the cell read `$3,456`. Triage ruled that the declared
1856
+ width, a currency's ISO 4217 minor-unit count, is the protocol's convention, and retired
1857
+ the whole-amount trimming `formatCurrency` applied since objectui#4033 (objectui#11444,
1858
+ comment 5946462862). The read-only face now calls `formatCurrency` itself.
1859
+
1860
+ **Visible change in table cells.** `formatCurrency` no longer drops the fraction of a
1861
+ whole amount. Every face that formats through it moves with it: the currency table
1862
+ cell, the record detail panel, the grid column-summary footer, the dashboard metric
1863
+ tile and table widget, and the gantt tooltip. A whole USD amount reads `$3,456.00`
1864
+ instead of `$3,456`, a whole KWD amount `KWD 3,456.000` instead of `KWD 3,456`, and an
1865
+ amount with no currency resolved `3,456.00` instead of `3,456`. A currency with no
1866
+ minor unit is unchanged: a whole JPY amount still reads `¥3,456`. Fractional amounts
1867
+ render exactly as before. The dashboard metric tile restated the cell's no-currency
1868
+ width for a field with no currency resolved (no decimals for a whole amount); it now
1869
+ restates two decimals, so a whole amount reads `3,456.00` on the tile as in the cell.
1870
+
1871
+ `formatCurrency` keeps its name, its signature and its `@object-ui/fields` export.
1872
+ Nothing changes in the editable inputs.
1873
+ - 8bfc001: fix(plugin-dashboard): an `object-metric` with a structured `groupBy` and a rule-list `filter` draws its number
1874
+
1875
+ An authored node such as
1876
+
1877
+ ```ts
1878
+ {
1879
+ type: 'object-metric',
1880
+ properties: {
1881
+ objectName: 'opportunity',
1882
+ aggregate: { field: 'amount', function: 'sum', groupBy: { field: 'close_date', dateGranularity: 'month' } },
1883
+ filter: [{ field: 'stage', operator: 'equals', value: 'won' }],
1884
+ },
1885
+ }
1886
+ ```
1887
+
1888
+ passes both authoring faces (`safeValidateSchema`, which `objectui validate` runs, and
1889
+ `StrictAnyComponentSchema`), and on `ObjectStackAdapter` it drew the adapter's refusal
1890
+ ("the spec-shape branch received a `where` array that is not a filter") instead of its number
1891
+ (objectui#11526).
1892
+
1893
+ A structured `groupBy` sends the tile down the spec-shape aggregate query (objectui#8613), and
1894
+ `ObjectMetricWidget` passed the bag's `filter`, a `ViewFilterRule[]`, into that query's `where`
1895
+ as written. The adapter posts that `where` verbatim and refuses a rule list there with
1896
+ `UnloweredAggregateWhereError` (objectui#6825), so the query was never sent. The same rule list
1897
+ with a string `groupBy` drew its number, because the legacy aggregate bag carries it as
1898
+ `filter` and the adapter lowers that key itself.
1899
+
1900
+ The tile now lowers the rule list before it builds the spec-shape query: `toFilterNode` from
1901
+ `@object-ui/core` turns it into filter AST, and `parseFilterAST` from `@objectstack/spec/data`
1902
+ turns that into the `FilterCondition` that `QuerySchema.where` declares. These are the same two
1903
+ stages the adapter applies to the rule list on the legacy bag, where its own translator is
1904
+ private to `@object-ui/data-objectstack`. An array that is neither a rule list nor filter AST is
1905
+ passed on unparsed, so the adapter still refuses it rather than receiving no filter and
1906
+ aggregating every row.
1907
+
1908
+ What does not move:
1909
+
1910
+ - a string or absent `groupBy`: the legacy bag still carries the rule list as `filter`, and the
1911
+ adapter lowers it as before;
1912
+ - a flat record filter (`{ stage: 'won' }`): it is already a `FilterCondition` and still travels
1913
+ verbatim as `where`;
1914
+ - the drill-down drawer: it still receives the tile's resolved filter, unchanged;
1915
+ - `UnloweredAggregateWhereError`: the adapter still refuses an unlowered rule list from any
1916
+ caller that skips the lowering.
1917
+
1918
+ One posted body changes without changing its answer: an empty rule list on the spec-shape query
1919
+ now sends no `where`. It used to send `where: []`, which the platform engine reads as no filter.
1920
+ - baac3f4: Fix four `find()` calls that passed a query option without its `$`, and gate the shape.
1921
+
1922
+ `QueryParams` declares every query option `$`-prefixed and `convertQueryParams` copies
1923
+ exactly those keys, so an unprefixed spelling reaches no branch and is dropped — no throw,
1924
+ no warning, and it type-checks because the type carries `[key: string]: any` for
1925
+ adapter-specific params. For a dropped cap the result is an **unbounded** read rather than
1926
+ a truncated one: the platform's GET list route has no default page size, so the query
1927
+ returns the whole match set and stays invisible until the object is large.
1928
+
1929
+ - `app-shell` `ObjectView` fetched the footer's record count with `{ limit: 0 }`. This one
1930
+ **inverted** rather than widened — `$top: 0` is honoured end to end as "no records", so
1931
+ the dropped key turned "count only, fetch nothing" into "fetch every row in the object",
1932
+ on every mount and every refresh of every list view. It now sends `$top: 0` and reads
1933
+ the count off `total` only; the row-counting fallbacks are gone rather than repointed,
1934
+ because once zero rows are requested an empty `data` means "you asked for none", not
1935
+ "the object is empty", and counting it would assert a confident `0`. With no total the
1936
+ footer line is omitted instead.
1937
+ - `app-shell` `AssignedUsersSection` looked a permission set up with `{ …, limit: 1 }`,
1938
+ one line from three correct `$top` calls.
1939
+ - `plugin-dashboard` `DashboardFilterBar` passed `fields` **and** `top` in one literal, so
1940
+ a filter's option list read every row and every column of its source object while its
1941
+ own comment described it as capped at 200. The same call read `records.items`, which is
1942
+ not a `QueryResult` member, so against a real adapter the fallback produced no options
1943
+ at all.
1944
+ - `console` `sdui-workbench-preview` passed `{ top: 200 }` and read `.records` off the
1945
+ result in its page-source metadata.
1946
+
1947
+ A new `object-ui/no-unprefixed-query-params` ESLint rule rejects the shape at write time:
1948
+ a known query-option name missing its `$` in the second argument of a `find`/`findOne`
1949
+ call. It is narrow on purpose — a closed list of spellings, anchored to the call — because
1950
+ the index signature exists so adapters can take adapter-specific params, and a rule that
1951
+ flagged any unprefixed key would report the shape the type was written to allow. Its
1952
+ sibling `no-query-params-under-options` (the `{ options: { $top } }` half) is unchanged.
1953
+ - 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on
1954
+ every relative specifier, and a type error in the declaration build now fails the build
1955
+ instead of being printed and ignored (objectui#5439, objectui#5483).
1956
+
1957
+ **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that
1958
+ is the fix working.** These packages re-export mostly through NAMED re-exports —
1959
+ `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the
1960
+ extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent
1961
+ `any`. Nothing errored; consumers simply got no types. With the extension emitted, the
1962
+ symbol carries its real type, and any call site that was relying on the `any` now type
1963
+ checks for the first time. This is the mode that produced the 21 residual `TS7006` on
1964
+ `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly,
1965
+ unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced
1966
+ immediately as `TS2305: has no exported member`.
1967
+
1968
+ 410 extensionless relative specifiers across 19 packages were emitted before this change;
1969
+ the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`.
1970
+ `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and
1971
+ is wired so it stays that way.
1972
+
1973
+ The second half changes no emitted output today: 22/22 packages built green unmodified, so
1974
+ making the declaration step's exit code honest turns nothing red. It changes what a FUTURE
1975
+ regression does — print and exit 0, versus fail the build.
1976
+ - fc62bb4: `TableColumn.type` now has ONE canonical value set across all three ends that disagreed
1977
+ (maintainer ruling 2026-08-25, Option B: the 8-literal interface union is
1978
+ canonical). The interface declared `'text' | 'number' | 'date' | 'datetime' | 'currency' |
1979
+ 'percent' | 'boolean' | 'action'`; the zod mirror declared `z.string()` and accepted
1980
+ anything; the renderer branched on a third set and could only read the key through an
1981
+ `as any` cast.
1982
+
1983
+ ## ⚠️ Accept-set narrowing — these spellings stop validating
1984
+
1985
+ `TableColumnSchema.type` was `z.string().optional()`. **Any string parsed green.** It is now
1986
+ `z.enum(TABLE_COLUMN_TYPES).optional()`, so a value outside the eight is refused at parse
1987
+ time with `type` named in the error path. Spellings that validated before and are **refused
1988
+ now**, grouped by why they were being written:
1989
+
1990
+ - **Typos and invented names** — `'money'`, `'datetime2'`, `'string'`, `'int'`, `'integer'`,
1991
+ `'float'`, `'double'`, `'datetime-local'`, and every other free-form string. `'money'` is
1992
+ the card's headline case: it validated, matched no renderer branch, and the column fell
1993
+ through to plain text rendering with nothing reported. That silent fall-through is the
1994
+ lenient-validation face that lets AI-authored metadata errors through, and it is now a
1995
+ loud parse failure.
1996
+ - **Object-schema field types written into a column slot** — `'select'`, `'lookup'`,
1997
+ `'user'`, `'file'`, `'formula'`, `'textarea'`, `'email'` and the other 35 members of
1998
+ `@objectstack/spec`'s `FieldType` that are not among the eight. These belong on the FIELD,
1999
+ not on the column: a column gets its dedicated widget from the field definition behind its
2000
+ `accessorKey`, never from `type`.
2001
+
2002
+ **Authored metadata in this repo needs no migration.** Measured before tightening, across
2003
+ `examples/`, `content/`, `apps/`, `e2e/`, `docs/` and every package (591 JSON schema files
2004
+ plus the docs and playground sources): **zero** authored `TableColumn.type` values outside
2005
+ the eight, and zero occurrences of `int` / `integer` / `float` / `double` in a column
2006
+ position anywhere in the repository. If you author `type` on a table column, check it
2007
+ against the eight; if the value describes the FIELD rather than the column, remove it.
2008
+
2009
+ ## The renderer's undeclared vocabulary disappears instead of being declared
2010
+
2011
+ `int` / `integer` / `float` / `double` were members of the data-table's `NUMERIC_EDIT_TYPES`
2012
+ and `datetime-local` had its own editor branch, none of them declared. They arrived because
2013
+ column-inference producers forwarded an object schema's field type **verbatim** into
2014
+ `TableColumn.type`. Rather than publishing that dialect, producers now fold their inferred
2015
+ value onto the declared vocabulary at their emit seam via the new
2016
+ `normalizeTableColumnType()`: `int`/`integer`/`float`/`double` → `number`,
2017
+ `datetime-local` → `datetime`, and **anything else drops the `type` annotation — never the
2018
+ column**. Two producers do this, not the one the card named: `ObjectGrid` (`@object-ui/plugin-grid`)
2019
+ and `ObjectDataTable` (`@object-ui/plugin-dashboard`), whose `buildFieldMeta` spread wrote
2020
+ the raw field type into the same slot.
2021
+
2022
+ Dropping the annotation is behaviour-preserving at the only consumer that reads the key.
2023
+ `data-table`'s inline editor branches on `date`, `datetime` and the numeric set and
2024
+ otherwise falls through to a text input — which is exactly the `undefined` path. The
2025
+ dedicated widget a `select` or `lookup` column gets comes from the host's `renderCellEditor`,
2026
+ which resolves the field through `column.accessorKey` and never reads `type`.
2027
+
2028
+ ## New public API
2029
+
2030
+ `@object-ui/types` exports `TABLE_COLUMN_TYPES` (the canonical tuple — the single
2031
+ declaration the zod mirror builds its enum from, so the two cannot drift), the
2032
+ `TableColumnType` union, and `normalizeTableColumnType()` for producers. The `as any` cast
2033
+ in `data-table.tsx` is deleted and the read is typed, so re-introducing an undeclared
2034
+ spelling is a tsc error rather than a silent widening.
2035
+
2036
+ A value-level parity pin covers all three ends
2037
+ (`packages/types/src/__tests__/table-column-type-canonical.test.ts` and
2038
+ `packages/components/src/renderers/complex/__tests__/table-column-type-read-set.test.tsx`).
2039
+ objectui#5684's guard is key-set only and cannot see value drift — `type` was present on
2040
+ both sides the whole time — which is how this instance survived while its siblings were
2041
+ caught. A future inference value turning that pin red is by design; the note at the pin says
2042
+ so, and names the two correct repairs.
2043
+ - a100f77: The dashboard package now holds ONE relation predicate instead of two that
2044
+ agreed only because a sweep had just aligned them (objectui#5876).
2045
+
2046
+ `computeLookupExpand` in `ObjectDataTable.tsx` carried its own `isLookup`,
2047
+ byte-identical to the exported `isLookupType` in `recordFields.tsx` after
2048
+ objectui#5692 pointed both at `@object-ui/core`'s `EXPANDABLE_FIELD_TYPES`.
2049
+ Nothing kept them aligned: a future edit to either — a member added, the
2050
+ retirement gate moved — would have re-forked the `$expand` decision from the
2051
+ predicate whose docblock claims to drive it. `computeLookupExpand` now calls
2052
+ `isLookupType`, which gains its first production consumer, and the module no
2053
+ longer imports the shared family or the retirement gate at all.
2054
+
2055
+ **No behaviour changes**, and that is measured rather than assumed:
2056
+
2057
+ - The two bodies were identical, so every boolean answer — `tree` is expanded,
2058
+ `reference` is not, ordinary relations are — is the same before and after.
2059
+ - The retired-spelling warning is not emitted a different number of times.
2060
+ `reportRetiredFieldType` dedupes per SPELLING in one module-level set inside
2061
+ `@object-ui/core`, which both bodies already shared, so routing two callers
2062
+ through one function cannot change the count.
2063
+
2064
+ Nothing published moves: `isLookupType` is not re-exported from
2065
+ `@object-ui/plugin-dashboard`'s entry, so this is internal shape only.
2066
+
2067
+ Because a refactor with no observable delta cannot be pinned by a behavioural
2068
+ test — a byte-identical local copy satisfies every assertion you can write
2069
+ about `$expand` — the pin is identity, in
2070
+ `__tests__/expandableFamily.identity-5692.test.ts`: `computeLookupExpand` is
2071
+ observed CALLING `isLookupType`, and `ObjectDataTable.tsx` is read at source
2072
+ level to confirm no second body survives for it to call instead.
2073
+ - 99a3c2d: `BaseSchema` declares `bind`, the data-scope binding path, on both halves — the TypeScript
2074
+ interface and its Zod mirror (objectui#6357).
2075
+
2076
+ `bind` was read by ten production sites and declared by no schema shape. It resolved as `any`
2077
+ through `BaseSchema`'s index signature and rode `.passthrough()` on the validator, while three
2078
+ separate documents taught it as an authorable key of *every* node: this repo's own `AGENTS.md`
2079
+ §4 ("Every node in the UI tree follows this shape (`@object-ui/types`)"), the published
2080
+ agent-facing `skills/objectui/rules/protocol.md` ("Every UI component node MUST follow this
2081
+ shape"), and `content/docs/fields/grid.mdx`. So the agent-facing protocol told authors to write
2082
+ a key the published types did not know existed.
2083
+
2084
+ The census chose the home rather than guessing it. Nine reads go through
2085
+ `useDataScope(schema.bind)` — `list` and `tree-view` in `@object-ui/components`, and the
2086
+ `object-*` widgets in `plugin-charts`, `plugin-dashboard` (×2), `plugin-grid`, `plugin-kanban`,
2087
+ `plugin-list`, `plugin-timeline`. A tenth is `plugin-grid`'s `gridNeedsDataSource` predicate,
2088
+ where a present `bind` is one of the escape hatches that makes a missing data-source adapter
2089
+ legitimate. Two more sites destructure the key out so `SchemaRenderer`'s prop spread cannot
2090
+ write `bind="data.revenue"` onto the DOM. Per-component declaration was measured and rejected:
2091
+ it costs nine copies of one key and buys nothing extra, because neither half can refuse the key
2092
+ on a non-reader either way. `placeholder` is the standing precedent for a cross-cutting key
2093
+ declared on `BaseSchema` and honoured only by a subset.
2094
+
2095
+ **Accept-set narrowing, on the value and not the key.** `bind: 42` type-checked and parsed green
2096
+ before this change and is refused by both halves now. It only refuses what already crashed:
2097
+ `useDataScope` is `(path?: string)` and resolves via `path.split('.')`, so a non-string `bind`
2098
+ threw a `TypeError` at render time. Every `bind` authored in this repo is a string, and the
2099
+ declaration is optional, so nothing that renders today stops.
2100
+
2101
+ **What this does NOT change**, stated because the pin would otherwise be read as more than it is:
2102
+ an *undeclared* key is still accepted by both halves, so this did not buy rejection of a
2103
+ misspelling such as `bindTo` (objectui#5155 / objectui#6269 own that ceiling). And `data-table`
2104
+ still does not call `useDataScope`, so a `bind` on it is still ignored and still renders a header
2105
+ over an empty body with no error — a documented silent failure that this declaration neither
2106
+ causes nor cures, since the key was accepted on every node before it existed.
2107
+
2108
+ `ObjectPivotTable` drops its local `bind?: string`: its `PivotTableSchema & {…}` intersection
2109
+ extends `BaseSchema`, so the member was a true duplicate. Two other local declarations are left
2110
+ in place and ratcheted rather than removed — their containing types never reference `BaseSchema`,
2111
+ so deleting the member would delete the declaration rather than inherit it.
2112
+ - 129c7a9: `ObjectDataTable` no longer writes six undeclared keys into the `data-table` columns slot
2113
+ (objectui#6373). `enrich()` returned `NormalizedColumn`, whose `[key: string]: any` accepts
2114
+ anything, so nothing checked the producer's output against
2115
+ `DataTableSchema.columns: TableColumn[]`: `{ ...col, ...fieldMeta }` spread `label`,
2116
+ `options`, `referenceTo`, `format`, `currency` and `decimals` onto every emitted column, and
2117
+ `TableColumn` declares none of them.
2118
+
2119
+ The measured read set of the consumer (`data-table.tsx`, comments stripped) contains none of
2120
+ the six, so all six retire from the emit rather than being declared — declaring a key nothing
2121
+ reads is the same `declared != enforced` defect facing the other way. Rendering is unchanged
2122
+ because none of those keys was the live path for its own value: the `FieldMeta` the `cell`
2123
+ closure captures is what this widget's type-aware rendering has always read, and it is
2124
+ untouched. Authored spellings still pass through, so a column the author wrote as
2125
+ `{ format: '$0,0' }` keeps its `format` exactly as before.
2126
+
2127
+ `type` is unchanged — the fold `fc62bb490` put at this seam still applies. `name` is unchanged
2128
+ and still written: `data-table` reads `col.accessorKey || col.name` and objectui#5120 holds
2129
+ that alias while two published skill guides still teach a `{ name, label }` column. The hold
2130
+ is now declared at the seam instead of arriving anonymously inside a spread.
2131
+
2132
+ The seam's emit type carries ADR-0049 `?: never` tombstones for the retired keys rather than
2133
+ being a bare `TableColumn` annotation. Measured before the shape was chosen: a bare annotation
2134
+ raises no error at all here, because TypeScript's excess-property check exempts properties
2135
+ that arrive through a spread — it would have type-checked the boundary without enforcing it.
2136
+ - 38a123c: Land objectui#6425's per-key ruling for `ObjectDataTable`'s authored column
2137
+ override keys (maintainer, 2026-08-27):
2138
+
2139
+ - **Declare `format`, `options`, `currency`** on `TableColumn` and its
2140
+ `TableColumnSchema` zod mirror, in the same stroke. All three are honoured
2141
+ by `object-data-table`'s cell pipeline — `format` / `options` were
2142
+ documented author overrides the published types refused (a typed author got
2143
+ a compile error and the zod parse silently stripped the key); `currency`
2144
+ shipped in production but was never promised. The zod mirror now passes the
2145
+ keys through instead of stripping them; `StaticTableColumn` and its mirror
2146
+ tombstone all three under the #5474 lockstep rule (the static renderer
2147
+ reads no field-meta overrides).
2148
+ - **Retire `decimals`**, immediately: zero readers measured anywhere
2149
+ (`NumberCellRenderer` reads `scale`, `PercentCellRenderer` reads
2150
+ `precision`), so no authored `decimals` could reach a render. The authored
2151
+ read is removed and the key falls into `AuthoredColumnOverrides`' derived
2152
+ refusal band — render output is pinned unchanged.
2153
+ - **Re-arm plugin-grid's #6004 `options` retirement with an explicit
2154
+ tombstone**: that refusal rested on the key's NON-membership (excess-property
2155
+ freshness), which declaring `options` on `TableColumn` silently ended.
2156
+ `ObjectGridRetiredOptionsTombstone` (`?: never`, intersected into both
2157
+ `ObjectGridColumnDraft` and `ObjectGridColumn`) restores the refusal by
2158
+ assignability; #6004's verdict itself is unchanged.
2159
+ - **`referenceTo` is deliberately NOT declared as spelled** — it stays held,
2160
+ owned by objectui#6597 (fix the spelling chain or withdraw the README
2161
+ line). The remaining hold is that card's scope, not unfinished work here.
2162
+ - aff3d7a: A `bind` authored on a `data-table` is now diagnosed at render instead of ignored in
2163
+ silence (objectui#6575).
2164
+
2165
+ `bind` is the data-scope vocabulary: a path string resolved by `useDataScope()`.
2166
+ `list`, `tree-view` and the `object-*` plugin widgets read it. `data-table` does not
2167
+ — it takes its rows from an inline `data` array on the node and never calls the hook.
2168
+ A `bind` on a `data-table` was nevertheless accepted by every gate: the TS side via
2169
+ `BaseSchema`'s index signature, the zod side via `BaseSchema` being `.passthrough()`,
2170
+ which `DataTableSchema.extend(…)` inherits. Nothing read it at render, so the author
2171
+ got a table drawing a correct-looking header over the "No results found" empty state,
2172
+ with no error and no warning — a success receipt for a disagreement between the
2173
+ author and the renderer, and the hardest failure shape for a human or an AI author to
2174
+ self-check.
2175
+
2176
+ The platform was already paying for this in teaching rather than in diagnostics:
2177
+ `skills/objectui/rules/protocol.md` documents the pothole verbatim and a pin test
2178
+ locks the behaviour. The warning now also reaches the console, where the author who
2179
+ did not read the docs is standing:
2180
+
2181
+ > `bind: 'customers'` is ignored: data-table does not read `bind`; it reads its rows
2182
+ > from the inline `data` array on the node. This node has no inline rows, so the
2183
+ > table renders its header over an empty body.
2184
+
2185
+ It names the node's address, the path that was spelled, and the way out. The
2186
+ consequence clause is measured rather than asserted: a node carrying BOTH `data` and
2187
+ `bind` is not empty, and is told that its rows came from `data` and its `bind`
2188
+ contributed nothing.
2189
+
2190
+ **No behaviour change.** `data-table` still does not read `bind`, and per the
2191
+ 2026-08-27 ruling it must not start — making it a `useDataScope` reader is a separate
2192
+ published-surface question needing its own ruling, including a `data`-vs-`bind`
2193
+ precedence. Refusing the key at parse stays blocked on the `.passthrough()` ceiling
2194
+ (objectui#5155 / objectui#6269). The trap stops being silent; it does not stop being
2195
+ a trap. The channel is the one `plugin-grid`'s `columnSpellingDiagnostics.ts` already
2196
+ uses for this exact shape of failure — a pure describe function, a `useEffect` keyed
2197
+ on the schema slice, one `console.warn`, no NODE_ENV branch.
2198
+
2199
+ `ObjectDataTable` (`@object-ui/plugin-dashboard`) stops forwarding a `bind` it has
2200
+ already consumed. It resolves the binding itself via `useDataScope(schema.bind)` and
2201
+ then delegated with `{ ...schema, type: 'data-table', … }`, which handed the spent
2202
+ key to a component that cannot read one. Without this, a correctly authored and
2203
+ published-guide-taught `object-data-table` would have tripped the new diagnostic on
2204
+ every render, over rows that were on screen precisely because its `bind` had been
2205
+ honoured. The key is stopped where it was spent — the same shape its sibling
2206
+ `DashboardGridLayout` already uses for `data`. Nothing else about that delegation
2207
+ moved, and the bound rows still arrive.
2208
+ - 4ca30d0: Two widget prop types anchor their `schema` to exported schema types that extend
2209
+ `BaseSchema`, instead of hand-rolled inline literals with no `BaseSchema` in
2210
+ their ancestry (objectui#6576, maintainer ruling 2026-08-31 option A; folds
2211
+ objectui#6914).
2212
+
2213
+ - `@object-ui/types` exports `ObjectGallerySchema` (`type: 'object-gallery'`)
2214
+ and `ObjectDataTableSchema` (`type: 'object-data-table'`), each `extends
2215
+ BaseSchema`, beside the other `Object*Schema` declarations, with zod mirrors
2216
+ of the same names under `@object-ui/types/zod`. `ObjectDataTableSchema`
2217
+ declares the two keys the widget was reading behind casts — `drillDown`
2218
+ (`DrillDownConfig`, at this change) and `onRowClick` — which no declaration
2219
+ carried before.
2220
+
2221
+ ⚠️ **Dated note, 2026-09-25 — `drillDown` now takes the table's own shape —
2222
+ objectui#10685.** `ObjectDataTableSchema.drillDown` is
2223
+ `ObjectDataTableDrillDownConfig`, on both faces: `filter`, `maxRows` and
2224
+ `report` are refused by name, and `target` is `'drawer'` or `'dialog'`. None of
2225
+ the four was ever read by this block. The rest of this entry is kept as the
2226
+ reading of this change.
2227
+ - `@object-ui/plugin-list`: the published `ObjectGalleryProps.schema` is
2228
+ `ObjectGallerySchema`. Its accept set WIDENS — every `BaseSchema` member is
2229
+ writable (`visibleWhen`, a real base member, was a compile error on the
2230
+ literal) — and NARROWS in one place: `type` is now required and pinned to
2231
+ `'object-gallery'`. `data` stays `Record<string, unknown>[]`.
2232
+ - `@object-ui/plugin-dashboard`: `ObjectDataTableProps.schema` (not exported
2233
+ from the plugin index) is `ObjectDataTableSchema`. The literal's own
2234
+ `[key: string]: any` is gone, so a wrong-typed base member (`visible: 42`) and
2235
+ a wrong-shaped `drillDown` are refused, and `type` is pinned to
2236
+ `'object-data-table'` instead of bare `string`.
2237
+
2238
+ Unchanged on both, stated plainly: an UNKNOWN key still compiles, because
2239
+ `BaseSchema`'s index signature is inherited (objectui#5155, open). No runtime
2240
+ behaviour changes; the widgets render exactly as before.
2241
+ - 3beef6d: The spec's `dataSource` element binding is now DECLARED by the blocks that read
2242
+ it, so the html tier stops reporting the one working saved-view spelling as
2243
+ `unknown-prop` (objectui#6678).
2244
+
2245
+ `PageComponentSchema.dataSource` — `{ object, view, filter, sort, limit }` — is
2246
+ the one spelling that resolves a saved view for an object-bound block. It works,
2247
+ and it drew the identical `unknown-prop` warning as the two spellings that do
2248
+ nothing (`viewName`, `view`), because `validateTree` looks a prop up in the
2249
+ block's declared `inputs` and no registration declared this key. On the tier
2250
+ built to accept AI-authored pages, where the diagnostic IS the contract, the
2251
+ only signal pointed away from the key that works.
2252
+
2253
+ Adopting the maintainer ruling of 2026-08-29 — option B **in the injection
2254
+ form**:
2255
+
2256
+ - `ELEMENT_DATA_SOURCE_INPUT` is the single declaration, in `@object-ui/core`
2257
+ beside the binding's own semantics; `Registry.register` emits it for any
2258
+ registration whose renderer passed through the new `elementDataSourceBlock()`
2259
+ seam. One mechanism, one copy — not a hand-kept declaration per block, which is
2260
+ the shape that drifts and that a new block forgets. The seam lives in
2261
+ `@object-ui/core` and is re-exported by `@object-ui/react` beside
2262
+ `ElementDataSourceGate` for discoverability; call sites take the core import,
2263
+ because a registration runs at module scope and this repo's suites partially
2264
+ mock `@object-ui/react`.
2265
+ - Seventeen renderers, in thirteen files across twelve packages, reach the seam
2266
+ and now publish the key to the save gate, the parser whitelist, the generated
2267
+ JSX authoring types and the block list. The card named nine blocks; the tree
2268
+ also has `plugin-grid`, `plugin-timeline`, two further `plugin-form` blocks and
2269
+ `element:record_picker` — nothing was hand-listed, so the mechanism covered
2270
+ them. `element:record_picker` consumes the gate's HOOK and status panels rather
2271
+ than the wrapper tag (its object lives under `properties`), and was found by a
2272
+ render probe rather than by reading sources.
2273
+ - `dataSource` on a block that does NOT read it (`flex`, `card`) still reports
2274
+ `unknown-prop`. Adding the key to `sdui-parser`'s `BASE_PROPS` was refused for
2275
+ exactly this reason — that set mirrors `BaseSchema`, and silencing the key
2276
+ everywhere would make the diagnostic lie in the other direction.
2277
+ - New `check:element-data-source-declaration` fails any source that consumes the
2278
+ gate without reaching the seam, so a block added tomorrow cannot forget.
2279
+
2280
+ Behaviour of the binding itself is unchanged — this is a declaration, not a
2281
+ resolution change. The saved view still resolves its columns, and an
2282
+ unresolvable `view` still fails loudly rather than widening to the object's full
2283
+ scope.
2284
+
2285
+ The spec/registry parity gates (repo-wide and the `record:related_list` per-block
2286
+ pin) now derive their accepted set from the WHOLE node contract rather than from
2287
+ `ComponentPropsMap[type]` alone. `PageComponentSchema` accepts and keeps
2288
+ `dataSource` on a page-component node — it is a node-level key, a sibling of
2289
+ `type` and `className`, not a per-block prop — so the gates' previous complaint
2290
+ was measurably wrong. Derived from the spec, not exempted, and both still
2291
+ discriminate against an invented key.
2292
+ - d636743: Feed the lookup cells in `ObjectDataTable` and `RecordDetailDrawer` their reference target,
2293
+ so schema-aware display-name resolution and drill-through links engage for the first time
2294
+ (objectui#6694).
2295
+
2296
+ Both widgets build their cell meta with `buildFieldMeta` and render it through
2297
+ `renderFieldValue` → `getCellRenderer` → `LookupCellRenderer` (`@object-ui/fields`). That
2298
+ renderer resolves its target from `field.reference_to || field.reference`, and `FieldMeta`
2299
+ carried neither spelling — nor `display_field`. So the renderer resolved `undefined` and two
2300
+ things failed, independently and both silently:
2301
+
2302
+ - `useRefObjectSchema` never loaded the referenced object's schema, so the ADR-0079 /
2303
+ objectui#2357 resolution never ran and every cell fell back to `pickRecordDisplayName`'s
2304
+ generic `.name` / `.title` heuristic. Quiet, because that heuristic usually still produces
2305
+ a readable name — it diverges only when the referenced object's display field is not
2306
+ literally `name` / `title`, and then it silently shows the wrong one.
2307
+ - `ReferencedRecordLink`'s `objectName` was always `undefined`, so `navigable` was always
2308
+ `false` and no lookup cell in either widget ever rendered a real anchor — no
2309
+ drill-through, no middle-click-new-tab, no copy-link. Quiet, because the cell still
2310
+ rendered its value as plain text.
2311
+
2312
+ This RESTORES intended behaviour rather than adding surface. `ReferencedRecordLink` was
2313
+ placed in the shared cell renderer precisely so every surface would get the affordance once
2314
+ ("Both surfaces resolve through `LookupCellRenderer`, so the affordance belongs here,
2315
+ once"), and `plugin-grid`'s `ObjectGrid` has fed it all along via `applyRelationalMeta` at
2316
+ all three of its column-building call sites. These two widgets simply never adopted that
2317
+ copy. Nothing new is authorable: the keys come off the OBJECT SCHEMA field def authors
2318
+ already write, never off a column override — the distinction objectui#6597 measured when it
2319
+ retired `referenceTo`, and the reason this needs no new column hold.
2320
+
2321
+ The copy is made once in `buildFieldMeta`, the seam both widgets funnel through, so the two
2322
+ surfaces cannot drift — which is what that module exists for.
2323
+
2324
+ ⚠️ The copy set is three keys where `ObjectGrid`'s `RELATIONAL_META_KEYS` is nine, and the
2325
+ difference is measured per key, not preferred. The grid's cells are EDITABLE, so its extra
2326
+ keys drive the inline picker's query (`LookupField` / `UserField` read `id_field`,
2327
+ `description_field`, `lookup_filters`, `lookupFilters`); these two widgets are read-only and
2328
+ their render path ends at a cell renderer. `packages/fields/src/index.tsx` reads exactly
2329
+ `reference_to`, `reference` and `display_field` off a cell's `field` prop; `titleFormat` is
2330
+ never read off a field meta at all (its readers take it off the object schema, which arrives
2331
+ here through `useRefObjectSchema(reference_to)`), and `reference_to_field` has zero member
2332
+ reads anywhere in the repo. Copying the other six would mint six members written on every
2333
+ call and read by nothing — precisely what objectui#6625 (`decimals`) and objectui#6597
2334
+ (`referenceTo`) retired from this same file.
2335
+
2336
+ No published type widened: `FieldMeta` is internal to the package — it is not re-exported
2337
+ through the barrel (`dist/index.d.ts` names neither it nor `recordFields`) and the `exports`
2338
+ map publishes only `"."`, so Node refuses `@object-ui/plugin-dashboard/recordFields` with
2339
+ `ERR_PACKAGE_PATH_NOT_EXPORTED`.
2340
+
2341
+ Behaviour note for existing dashboards: a lookup cell whose referenced object declares a
2342
+ `nameField` other than `name` / `title` will now show that declared name instead of the
2343
+ heuristic's pick, and valued lookup cells become links wherever the host publishes
2344
+ `recordHref`.
2345
+
2346
+ ⚠️ **Dated note, 2026-09-30 — the cell's reads and the copy set have since moved — objectui#11070.** Later in this same release objectui#11070 (round 4) narrowed `LookupCellRenderer` to `reference` alone, so "reads exactly `reference_to`, `reference` and `display_field`" and `useRefObjectSchema(reference_to)` above no longer describe what ships: the cell reads `reference`, and `buildFieldMeta` no longer copies `reference_to` (its `FieldMeta` member is retired, and `ObjectDataTable` refuses it by a hand-written tombstone). `.changeset/11070-reference-to-round4.md` states what ships; the text above is kept as the reading of this change.
2347
+
2348
+ ⚠️ **Dated note, 2026-09-30 — `display_field` has left the copy set too — objectui#11070.** The copy set above includes `display_field`. Later in this same release objectui#11070 (its text-family round) removed it from `buildFieldMeta`'s copy and from `FieldMeta`: `LookupCellRenderer` has resolved the display pointer as `displayField`, then `reference_field`, since objectui#7155, so the copied key reached no reader. The copy set that ships is `reference` and `displayField`, and `ObjectDataTable` refuses `display_field` on an authored column by a hand-written tombstone. `.changeset/11070-text-family-round5.md` states what ships; the text above is kept as the reading of this change.
2349
+ - 9700dd9: A lookup cell in `ObjectGrid` now honours the author's `displayField`
2350
+ (objectui#6875).
2351
+
2352
+ `ObjectGrid` copies a set of relational keys off the object-schema field def
2353
+ onto each column's `fieldMeta`, and that bag is what the lookup cell renderer
2354
+ and the inline picker receive. The set was hand-kept and had become a strict
2355
+ SUBSET of what those two consumers read — `displayField`, `descriptionField`
2356
+ and `lookupColumns` were read on the grid's own path and never copied.
2357
+
2358
+ They are the spellings that matter. `@objectstack/spec` 17.2.0's `FieldSchema`
2359
+ is strict and declares `displayField` / `descriptionField` / `lookupColumns` /
2360
+ `lookupFilters` / `reference`, and none of the snake_case twins the copy set
2361
+ mostly carried — those parse to `unrecognized_keys`, so a spec-compliant
2362
+ producer cannot emit them. Nothing renames anything on the way in either: the
2363
+ adapter's `getObjectSchema` choke point rewrites only the `reference` ⇄
2364
+ `reference_to` pair. So an author who declared `displayField: 'project_code'`
2365
+ got a grid cell showing the referenced record's generic `.name` instead.
2366
+
2367
+ - The copy set is now DERIVED, in `plugin-grid/src/relationalMetaKeys.ts`, from
2368
+ a table that classifies every key the consumers read off this bag. A gate
2369
+ re-extracts that read set from the consumer sources on each run and fails on
2370
+ any unclassified spelling or orphan, so the two cannot drift apart again.
2371
+ - `reference_field` and `lookup_columns` — the other two never-copied keys —
2372
+ stay out on purpose: `FieldSchema` declares neither, so no producer can fill
2373
+ them. The gate proves that against the installed spec rather than asserting it
2374
+ in prose.
2375
+ - `plugin-dashboard`'s `CELL_RELATIONAL_META_KEYS` had the same omission in the
2376
+ same fallback chain and gains `displayField` too.
2377
+
2378
+ ⚠️ **Dated note, 2026-09-30 — the choke point's one reference rewrite is now one-way — objectui#11070.** "rewrites only the `reference` ⇄ `reference_to` pair" above held when this change landed. Later in this same release objectui#11070 (round 4) made that rewrite a fold onto `reference` that never stamps `reference_to`. Nothing else is rewritten on the way in, so the reasoning above is unaffected. `.changeset/11070-reference-to-round4.md` states what ships; the text above is kept as the reading of this change.
2379
+ - 5ad86dd: **Breaking for authored metadata:** `TextSchema.value` is RETIRED
2380
+ (maintainer ruling A1 of 2026-09-04; objectui#7016; ADR-0049 enforce-or-remove).
2381
+ A `text` node that authors `value` no longer validates: the parse fails loudly on
2382
+ the `value` path with the explanation in the message, the TS member is a
2383
+ `?: never` tombstone so the same document is refused at compile time, and the
2384
+ renderer no longer reads the key. Write `content`.
2385
+
2386
+ **What was measured, on this branch's base.** `TextSchema` declared two spellings
2387
+ for its one content slot — `content` (read first) and `value` (the fallback limb
2388
+ of `{schema.content || schema.value}` at `renderers/basic/text.tsx:162` and
2389
+ `:167`) — both declared by objectui#6150, whose docblock called the pair "a
2390
+ dialect, not a design" and deferred the choice. The ruling's premise, that
2391
+ `value` is the minority spelling, was measured before any edit over the four
2392
+ roots it named: **776 `content`-only `text` nodes, 25 `value`-only, 0 authoring
2393
+ both** across `examples/` (674 / 13), `apps/` (59 / 0), the `examples/`
2394
+ directories under `packages/` (0 / 1) and `content/docs/**` (43 / 11) — a
2395
+ thirty-to-one majority for `content`, so the retirement went ahead as ruled.
2396
+ (A further 14 `{ value, label, type: "text" }` objects in the filter-builder
2397
+ catalog entries are field descriptors whose `type` is a field type, not `text`
2398
+ nodes, and were excluded by kind.)
2399
+
2400
+ **Who is affected — a `value` authored on a `text` node:**
2401
+
2402
+ ```json
2403
+ { "type": "text",
2404
+ "value": "Hello" } // ← was tolerated (rendered as the fallback)
2405
+ ```
2406
+
2407
+ now fails validation with:
2408
+
2409
+ > RETIRED (ADR-0049) — `value` is no longer part of TextSchema; write
2410
+ > `content`. It was a second spelling of the one content slot, read only as the
2411
+ > fallback limb of `schema.content || schema.value`, and was retired under
2412
+ > ADR-0049 enforce-or-remove with no deprecation window (maintainer ruling A1,
2413
+ > 2026-09-04). The renderer reads `content` alone now, so an authored `value`
2414
+ > would render nothing. Rename the key; the string is unchanged.
2415
+
2416
+ **Two published faces, one retirement.** The TypeScript interface `TextSchema`
2417
+ (`@object-ui/types`, `layout.ts`) declares `value?: never`; the Zod mirror
2418
+ `TextSchema` (`@object-ui/types/zod`, `layout.zod.ts`) declares `value` as a
2419
+ `retirementTombstone()`, so the key stays DECLARED and is refused BY NAME —
2420
+ a plain deletion would have let an authored `value` ride `BaseSchema`'s
2421
+ `.passthrough()` into a silent blank, which is worse than the tolerated
2422
+ fallback it replaces. The `value?: string` members of `TextSpanSchema` and
2423
+ `TabsSchema` in the same file are other schemas' contracts and are unchanged.
2424
+
2425
+ **`@object-ui/components`** — the `text` renderer renders `{schema.content}` at
2426
+ both arms (the `|| schema.value` limb is gone from each), and the `context-menu`
2427
+ renderer's built-in fallback trigger node now spells `content`. Nothing else in
2428
+ the package moves. **`@object-ui/plugin-dashboard`** — its three placeholder
2429
+ `text` nodes ("chart type is not supported yet", "Custom widget — set
2430
+ `component`…", the retired-widget notice) spell `content` so they keep rendering;
2431
+ their wording is unchanged and still pinned.
2432
+
2433
+ **Who is NOT affected.** A document that already wrote `content` is untouched;
2434
+ `content`, `variant`, `align` and `className` are unchanged; `absent` stays
2435
+ valid (`{ "type": "text" }` still parses). Every in-repo document that authored
2436
+ `value` on a `text` node was rewritten to `content` in the same change: nine
2437
+ `examples/schema-catalog` entries, `packages/types/examples/zod-validation-example.ts`,
2438
+ eleven doc fences under `content/docs/`, and the `@object-ui/components`,
2439
+ `@object-ui/react` and `@object-ui/types/zod` README samples; the catalog is now
2440
+ pinned tree-wide against the retired spelling.
2441
+
2442
+ **Migration:** rename `value` to `content` on every `text` node; the string is
2443
+ unchanged. If a document authored both, `content` was already the value that
2444
+ rendered — delete `value`.
2445
+
2446
+ Graded `minor`, not `patch`: this narrows the accepted input set, which is
2447
+ breaking for any author who wrote the tolerated spelling. It is not `major` per
2448
+ this repo's fixed-group convention (objectui's own breaking changes ship as
2449
+ `minor`; the group's major tracks `@objectstack` — AGENTS.md 版本号策略,
2450
+ mechanically enforced by `scripts/check-changeset-no-major.mjs`).
2451
+ - 6411def: FLS-gate the `$expand` projection at the five remaining build sites (objectui#7230).
2452
+
2453
+ objectui#7215 / PR #7229 gated `$expand` at the two projection sites in its scope
2454
+ (`ObjectGrid`, `ListView`). The helper is reached from more places than that. This
2455
+ closes the five that were left: `ObjectCalendar`, `ObjectGantt`, `RecordDetailView`,
2456
+ `DetailView`, and `ObjectDataTable` (which builds its own whitelist in
2457
+ `computeLookupExpand` rather than calling `buildExpandFields`).
2458
+
2459
+ **Three of them pass no column list at all**, which makes them the sharp ones:
2460
+ `buildExpandFields` reads an absent column list as "no column restriction" and falls
2461
+ back to **every declared relation on the object**, denied ones included. So a standalone
2462
+ calendar, a gantt, and every record page in the console asked the server to resolve the
2463
+ object's full relation set by default rather than by configuration.
2464
+
2465
+ **`DetailView` was input-gated, and that is the defect rather than the fix.** Its column
2466
+ list is already FLS-filtered field by field, which is exactly the route PR #7229 measured
2467
+ as unsound: an emptied column list reads as "no column restriction", so a detail view
2468
+ whose authored fields are all denied had its `$expand` **widened** from the relations it
2469
+ asked for to every relation the object declares. The principal who may read least was
2470
+ asking for the most.
2471
+
2472
+ **Reproduced before it was fixed**, as a failing test per site.
2473
+
2474
+ **Grading, measured rather than assumed.** Against ObjectStack's own server this is
2475
+ defence-in-depth, exactly as objectui#6898 and #7215 are: `plugin-security`'s
2476
+ `FieldMasker.maskRecord` deletes every unreadable key from each returned row and
2477
+ objectql's expand path writes the resolved record back under that same key, so one
2478
+ statement removes the expanded object and the bare id alike; the expansion sub-read is
2479
+ itself gated (`__expandRead` takes the referenced object's full CRUD + RLS + FLS
2480
+ treatment). It is load-bearing for any backend that does not strip, and the
2481
+ client-request side is real regardless.
2482
+
2483
+ **Nothing a permitted view did stops working.** The gate judges each helper's OUTPUT,
2484
+ which contains only the object's declared reference-bearing fields, so the "`checkField`
2485
+ answers false for an undeclared key" trap cannot be reached and derived / host-joined
2486
+ columns are untouched. An unanswered permission policy filters nothing. Neither
2487
+ `buildExpandFields` nor `computeLookupExpand` is changed.
2488
+ - 47547d0: Localize the server's built-in aggregate measure titles on dataset charts
2489
+ (objectui#7258 — consumer half of the objectstack#14492 contract; maintainer
2490
+ ruling B, 2026-09-02).
2491
+
2492
+ A dataset-bound chart's aggregate axis / legend title read the analytics
2493
+ service's hard-coded English `Count` on a zh console whose category labels were
2494
+ already Chinese. The renderer was passing `fields[].label` through verbatim —
2495
+ correctly, for an author-declared measure (objectui#4106) — and had no way to
2496
+ tell the server's built-in default apart from an author's label.
2497
+
2498
+ The wire now can: `AnalyticsResult.fields[]` gains an OPTIONAL structural
2499
+ discriminator, `builtinAggregate?: 'count' | 'sum' | 'avg' | 'min' | 'max' |
2500
+ 'count_distinct'`, populated only on the server-side built-in defaults
2501
+ (objectstack#14492). This change is the consumer side of that contract:
2502
+
2503
+ - `@object-ui/core`: `buildChartSeries` now accepts `ChartMeasureField[]` —
2504
+ `ChartResultField` plus the optional `builtinAggregate` carrier
2505
+ (`BuiltinAggregateCarrier`), declared beside the renderer shape rather than
2506
+ on it because the spec this release is built against does not carry the key
2507
+ yet; new `BUILTIN_AGGREGATES` / `BuiltinAggregate` / `isBuiltinAggregate` /
2508
+ `resolveMeasureLabel`; `ChartSeriesOptions.builtinAggregateLabels` carries
2509
+ the locale strings in (core stays React-free and i18n-free — the same
2510
+ division as `nullCategoryLabel`). A field carrying a recognised
2511
+ discriminator resolves through that map; every other field keeps its wire
2512
+ `label` verbatim — never by matching the label's text or the field's name
2513
+ (the rejected option A).
2514
+ - `@object-ui/i18n`: `builtinAggregateLabels(tt)` resolves the six strings
2515
+ through the existing `report.aggregate.*` keys (zh already carried 计数 /
2516
+ 求和 / 平均 / …; all ten packs are pinned to cover the vocabulary).
2517
+ - `plugin-charts` (`ObjectChart`), `plugin-dashboard` (`DatasetWidget`),
2518
+ `plugin-report` (`DatasetReportRenderer`): pass the resolved map to
2519
+ `buildChartSeries`.
2520
+
2521
+ Before: 合作中 / 已流失 / 潜在 under an axis titled `Count`. After: the same
2522
+ chart titled `计数`; an `en` session still reads `Count`; an author-labelled
2523
+ measure (`Tasks`) and a measure literally named `count` without the
2524
+ discriminator are byte-for-byte unchanged. Until the upstream field is
2525
+ populated the wire carries no discriminator and every chart renders exactly as
2526
+ before.
2527
+ - faf5269: A dataset-bound KPI tile now renders the sub-caption its author declared in
2528
+ `options.description` (objectui#7293).
2529
+
2530
+ The sub-caption slot was wired end to end and consumed by nothing. It has its own
2531
+ translation key (`{ns}.dashboards.{dash}.widgets.{id}.subCaption`), the server's
2532
+ `translateDashboard` overlays that translation onto `options.description`, and
2533
+ `DashboardRenderer`'s `tWidgetSubCaption` resolves it — but only onto the two
2534
+ inline arms of `getComponentSchema()`. `dataset` is REQUIRED on
2535
+ `DashboardWidgetSchema` (published `@objectstack/spec@17.4.0`: the required keys
2536
+ are exactly `id` / `dataset` / `values`), so every spec-legal widget renders
2537
+ through `DatasetWidget` instead, which read the key nowhere. Every author who
2538
+ wrote a sub-caption got silence.
2539
+
2540
+ `DatasetWidget`'s metric branch now reads the key and renders it in the caption
2541
+ row, resolving it through the same `pickLocalized` seam every other authored
2542
+ label channel uses, so an inline per-locale map resolves instead of being
2543
+ dropped. A tile that declares no sub-caption renders byte-identical markup.
2544
+
2545
+ Read in `DatasetWidget` rather than passed down as a prop on purpose: both
2546
+ dashboard surfaces (`DashboardRenderer` and `DashboardGridLayout`) route a
2547
+ dataset-bound widget to that one component, so a prop from one dispatch site
2548
+ would have fixed one surface and left the other unchanged.
2549
+
2550
+ `@object-ui/sdui-parser` carries no behaviour change — its `unconsumed-widget-option`
2551
+ census recorded `description` as accepted *despite* having no read site on the
2552
+ dataset-bound path, and that note is now stale prose.
2553
+ - c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and
2554
+ `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781;
2555
+ maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove).
2556
+ They are the three keys the manifest serializer does not forward, and nothing read them on any
2557
+ publication or consumption path.
2558
+
2559
+ No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s
2560
+ serializer (`packages/sdui-parser/src/index.ts`) forwards exactly seven keys per input — `name`,
2561
+ `type`, `of`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three
2562
+ never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type
2563
+ has no slot for them; the registry's data-source seam reads `name` only; and neither the designer
2564
+ nor the app-shell inspectors consult registry `inputs` at all. A structural census over every
2565
+ `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type`
2566
+ 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on
2567
+ nearly every registration, read by nothing.
2568
+
2569
+ FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on
2570
+ the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an
2571
+ undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162
2572
+ authored values in silence. The tombstone is what makes the refusal loud and by name.
2573
+
2574
+ - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror.
2575
+ Migration: delete the key. An input is identified by its `name` on every path that reaches it;
2576
+ nothing ever rendered a label for it.
2577
+ - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the
2578
+ key. The renderer's own fallback read IS the default; tell the author about it in `description`,
2579
+ which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class,
2580
+ since nothing reads the value.)
2581
+ - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key.
2582
+ No designer surface ever hid an "advanced" input; there is nothing to write instead.
2583
+
2584
+ The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one
2585
+ is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema`
2586
+ (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with
2587
+ `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message
2588
+ (one string, both channels). Pinned in
2589
+ `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a
2590
+ tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`.
2591
+
2592
+ Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a
2593
+ `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses
2594
+ RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone
2595
+ `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule
2596
+ requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized
2597
+ `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT`
2598
+ drops its `label`. The patch entries on the other packages record exactly that: their registrations
2599
+ stop authoring inert keys, with no runtime or published-manifest change.
2600
+
2601
+ The nine test files that read `defaultValue` off a registration were re-pinned against the
2602
+ renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the
2603
+ declaration that went away; two assertions that only restated the shadow default were dropped with
2604
+ the reason on the line.
2605
+
2606
+ The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys
2607
+ is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop
2608
+ into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` /
2609
+ `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has
2610
+ ruled on that face; that it now has no reader either is recorded as objectui#7911.
2611
+ - 1422a92: `DashboardComponentSchema.header` and `.globalFilters` now state
2612
+ `@objectstack/spec`'s `DashboardSchema` member on both faces, the TypeScript
2613
+ interface and the Zod validator (objectui#7759 group A). Both keys are declared
2614
+ by the spec, and each face used to refuse something the other accepted.
2615
+
2616
+ **`header` (TypeScript interface).** The hand-written restatement is gone. The
2617
+ interface inherits the spec's `DashboardHeader` through the same
2618
+ `Omit` projection of the spec `Dashboard` type as its other spec-owned keys. Breaking for code
2619
+ that builds header actions in TypeScript:
2620
+
2621
+ - `actions[].label` is `I18nLabel`: a string or an inline per-locale map. It used
2622
+ to be `string`.
2623
+ - `actions[].actionUrl` is required. It used to be optional.
2624
+ - `actions[].actionType` is the spec's `ActionType` (`url`, `script`, `modal`,
2625
+ `flow`, `api` or `form`). It used to be any string.
2626
+
2627
+ The validator is unchanged here: it always took the spec's `header`.
2628
+
2629
+ **`globalFilters` (Zod validator).** `GlobalFilterSchema` is now the spec's
2630
+ `GlobalFilterSchema` by reference, and its local `options` / `optionsFrom`
2631
+ restatement is gone. Breaking for documents that relied on the looser validator:
2632
+
2633
+ - the bare-string option shorthand (`options: ['EMEA']`) is refused with
2634
+ `invalid_type` at `globalFilters.N.options.M`. Write
2635
+ `{ "value": "EMEA", "label": "EMEA" }`;
2636
+ - an option with no `label` is refused at `globalFilters.N.options.M.label`;
2637
+ - an `optionsFrom` with no `labelField` is refused with `invalid_type` at
2638
+ `globalFilters.N.optionsFrom.labelField`. Name the value field again if the
2639
+ options have no separate label column;
2640
+ - an `optionsFrom.filter` must be a filter-condition object. An array is
2641
+ refused;
2642
+ - an unknown key in a filter is refused with `unrecognized_keys` at
2643
+ `globalFilters.N`. It used to be removed without a message.
2644
+
2645
+ Widening: an option `label` may now be an inline per-locale map, which the spec
2646
+ has always allowed and the filter bar already resolves. The TypeScript type is
2647
+ unchanged, because it already bound the spec's `GlobalFilter`. The runtime is
2648
+ unchanged too: `@object-ui/core` still converts a STORED bare-string option into
2649
+ a pair when it reads the document, and logs a deprecation warning. That
2650
+ conversion keeps its own retirement schedule (objectstack#7917).
2651
+
2652
+ **`@object-ui/plugin-dashboard`.** Before this change, a header action whose
2653
+ `label` was an inline per-locale map crashed the renderer: React reported
2654
+ `Objects are not valid as a React child`. The mirror accepted that document.
2655
+ `DashboardRenderer` now shows the label in the active language, both as the
2656
+ button text and as the fallback for the
2657
+ `dashboards.NAME.actions.KEY.label` bundle lookup.
2658
+
2659
+ ⚠️ **Dated note, 2026-09-29 — the runtime bare-string lift has since retired —
2660
+ objectui#4356.** Later in this same release `@object-ui/core`'s
2661
+ `normalizeFilterOptions` stopped converting a STORED bare-string option into a
2662
+ pair: a `globalFilters[].options` member that is not a `{ value, label }` object
2663
+ now yields no option, a shorthand-only filter resolves with no `options`, and a
2664
+ mixed array keeps only its object members; in development a once-per-filter
2665
+ `console.warn` names the filter and the dropped members. The objectstack#7917
2666
+ retirement window is closed (maintainer, 2026-09-02, verbatim 「objectstack#7917
2667
+ 不考虑现有数据」). So "The runtime is unchanged too: `@object-ui/core` still
2668
+ converts a STORED bare-string option into a pair when it reads the document, and
2669
+ logs a deprecation warning" and "That conversion keeps its own retirement
2670
+ schedule (objectstack#7917)" above no longer hold; the validator's refusal of
2671
+ `options: ['EMEA']` described above is unchanged and now matches the read path.
2672
+ `.changeset/retire-options-shorthand-lift-4356.md` (PR objectui#10930) states
2673
+ what ships; the text above is kept as the reading of this change.
2674
+ - fc32921: Fix a dashboard chart widget with a FIELDLESS `count` aggregate plotting nothing
2675
+ (objectui#8266).
2676
+
2677
+ A widget bound to an object with `aggregate: { function: 'count', groupBy: 'status' }`
2678
+ and no `field` — the normal way to author "how many records per status" — rendered an
2679
+ empty chart. No error, no empty state: a plot frame with the category ticks drawn and
2680
+ not one mark in it, which reads exactly like "this object has no rows yet".
2681
+
2682
+ **Cause.** The two dashboard relays (`DashboardGridLayout`, `DashboardRenderer`) each
2683
+ built the series binding as `aggregate?.field || (options.yField || 'value')`, which for
2684
+ a fieldless count resolves to `'value'`. The rows an object-bound fieldless count
2685
+ returns are keyed `'count'` — the alias the engine projects `COUNT(*)` under, pinned
2686
+ since framework#3701. A `dataKey` naming a column no row carries plots nothing, and
2687
+ neither of the renderer's two guards fires on it: the rows DO carry the category key,
2688
+ and the series array is not empty.
2689
+
2690
+ **Fix.** `chartMeasureKey` is a new `@object-ui/core` export delegating to
2691
+ `chartAggregateValueKey` in `@objectstack/spec/ui` — the contract's own derivation of
2692
+ "the value column an object-bound aggregate produces". Both relays now consult it, and
2693
+ the row-projection side (`aggregateValueKey` in `@object-ui/plugin-charts`) is routed
2694
+ through the same function, so the two halves of the question cannot drift again.
2695
+
2696
+ **What moves on screen.** A chart that was blank now draws. Charts that already drew are
2697
+ unaffected: a field-bearing aggregate resolves to its raw field under both the old and
2698
+ the new reading, and a chart with no `aggregate` at all keeps the author's `yField`.
2699
+ One authored key changes meaning: a `yField` written on an object-bound chart that
2700
+ ALSO declares an aggregate no longer wins over the aggregate's own column — it named a
2701
+ record column that a grouped aggregate never returns, so it plotted nothing before.
2702
+
2703
+ **Not fixed here, and out of scope.** The same widget with no `options.xField` is
2704
+ refused by the category-axis guard naming `name`, a key the author never wrote (they
2705
+ wrote `aggregate.groupBy`). That is the category half of the same relay gap and is
2706
+ filed separately.
2707
+ - 8f9d87a: Fix a dashboard chart widget that declares its category as `aggregate.groupBy` being
2708
+ refused for lacking a `name` column (objectui#8269).
2709
+
2710
+ A widget bound to an object with `aggregate: { function: 'count', groupBy: 'status' }`
2711
+ and no `options.xField` rendered a refusal instead of a chart:
2712
+
2713
+ > This chart cannot plot its category axis: no row has a `name` field.
2714
+
2715
+ The author wrote `groupBy: 'status'`. Nothing on screen said `groupBy` was the key that
2716
+ had been ignored, and `name` appeared nowhere in their metadata — so the diagnostic sent
2717
+ them to debug the wrong layer.
2718
+
2719
+ **Cause.** The two dashboard relays (`DashboardGridLayout`, `DashboardRenderer`) each
2720
+ floored the category binding on a literal — `options.xField || 'name'` — and handed it to
2721
+ the `object-chart` node without ever consulting the aggregate that decides it. An
2722
+ object-bound aggregate returns one row per group keyed by the raw `groupBy` field, so no
2723
+ row carried `name` and the category-axis guard (framework#4033) fired correctly on a
2724
+ binding that was already wrong when it arrived.
2725
+
2726
+ **Fix.** `chartCategoryKey` is a new `@object-ui/core` export delegating to
2727
+ `chartAggregateCategoryKey` in `@objectstack/spec/ui` — the contract's own derivation of
2728
+ "the category column an object-bound aggregate produces", and the published sibling of the
2729
+ `chartAggregateValueKey` that objectui#8266 adopted for the measure axis. Both relays now
2730
+ consult it for the object-provider branch.
2731
+
2732
+ **What moves on screen.** A widget that rendered a refusal now draws. Measured through
2733
+ `ChartRenderer` at 480x320 over the rows a fieldless count returns
2734
+ (`[{status:'open',count:2},{status:'paid',count:5}]`): the composed binding went from
2735
+ `xAxisKey: 'name'` — a `missing-category-key` refusal, 0 marks — to `xAxisKey: 'status'`,
2736
+ 1 series and 2 marks with the category ticks drawn.
2737
+
2738
+ **Unaffected.** A chart with no `aggregate` at all keeps the author's `xField` (its rows
2739
+ are raw records, so that key is the right one), an UNGROUPED aggregate keeps it too (it
2740
+ returns a single row with no category column), and the authored-literal-rows branch — the
2741
+ `chart` node composed after the object-provider check fails — keeps its floor unchanged.
2742
+ One authored key changes meaning, exactly as objectui#8266's `yField` did: an `xField`
2743
+ written on an object-bound chart that ALSO declares a `groupBy` no longer wins over the
2744
+ aggregate's own column — it named a record column a grouped aggregate never returns, so it
2745
+ produced the same refusal before.
2746
+ - f4abff4: `@object-ui/layout` and `@object-ui/plugin-dashboard` no longer install a React of their
2747
+ own. `react` moves from `dependencies` to `devDependencies` at the same pin (`19.2.8`);
2748
+ `peerDependencies` is untouched, so both packages still ask their host for
2749
+ `^18.0.0 || ^19.0.0` (objectui#8303).
2750
+
2751
+ Both manifests were making two contradictory statements at once: asking the host for React
2752
+ as a peer, and pinning an exact React 19 of their own. The peer range is the one a consumer
2753
+ reads; the pin is the one their installer acts on. A consumer on React 18 therefore
2754
+ satisfied the peer range and **still** got a second, pinned React 19 pulled into their
2755
+ graph on these two packages' account. Two React copies in one tree is the classic cause of
2756
+ `Invalid hook call`, and the failure is not at install time — it is at first render, in the
2757
+ consumer's own component, with nothing pointing back at these manifests.
2758
+
2759
+ Re-measured on `origin/main` at `348725a7c` across all four `pnpm-workspace.yaml` globs
2760
+ (46 packages), rather than inherited from the card:
2761
+
2762
+ declares a react peer AND pins react in dependencies 2 layout, plugin-dashboard
2763
+ declares a react-dom peer AND pins it in dependencies 0 (objectui#8198 took that half)
2764
+ pins react in dependencies with NO react peer 5 runner, site, 3 examples
2765
+
2766
+ The last row is the control that keeps the fix narrow. Those five are applications that
2767
+ supply React rather than libraries that ask for it: one statement, not two, and correctly
2768
+ untouched. Against the 20+ sibling libraries the two packages fixed here were the only
2769
+ outliers, so this converges on the existing house shape — `app-shell`, `auth`,
2770
+ `collaboration`, `i18n`, `mobile` and `permissions` already carry `react` in
2771
+ `devDependencies` at exactly this pin.
2772
+
2773
+ `scripts/__tests__/react-peer-range-norm-3741.test.ts` gains the invariant so a regression
2774
+ goes red rather than being re-found by a consumer. That file already owns React manifest
2775
+ norms across the fixed version group, already walks the workspace globs, and already
2776
+ governs `react` + `react-dom`, so this is a second assertion on an existing guard rather
2777
+ than a new one. It asserts the CONTRADICTION, not "React in `dependencies`" — widening it
2778
+ further would go red on the five applications above for doing the right thing.
2779
+
2780
+ Two gates are green on this shape by construction and neither green was a reading of it.
2781
+ `check:unused-deps` asks whether a declared runtime dependency has a consumer, and `react`
2782
+ is imported by both packages — which is exactly why objectui#8198 could see and remove the
2783
+ `react-dom` half here and could not see this one. `check-changeset-presence.mjs` reads an
2784
+ eight-field allowlist that includes `peerDependencies` and deliberately excludes
2785
+ `dependencies`; its own docblock states the trade ("a runtime dependency bump can be just
2786
+ as user-visible as any of the eight, and this gate still does not see it"). This changeset
2787
+ is therefore owed by the house rule, not demanded by the gate.
2788
+ - 1f5a644: `DashboardGridLayout` and `DashboardRenderer` now share one auto-refresh timer
2789
+ (objectui#8820).
2790
+
2791
+ Each surface used to carry its own copy of the auto-refresh logic: the
2792
+ refreshing indicator, the handler behind the "Refresh All" button, and the
2793
+ interval that reads `refreshIntervalSeconds`. The two copies were identical in
2794
+ logic. They are now one internal hook, `useDashboardAutoRefresh`, that both
2795
+ surfaces call, so the authored period is read in one place.
2796
+
2797
+ Behaviour is unchanged. The timer runs only when the host passes `onRefresh`
2798
+ and `refreshIntervalSeconds` is greater than zero; it is cleared when the
2799
+ dashboard unmounts and re-armed when the period changes.
2800
+
2801
+ A new test mounts both surfaces and counts `onRefresh` calls under fake timers,
2802
+ so a timer that stops firing on either surface fails a test.
2803
+ - 5b6deed: A client i18n bundle sub-caption now reaches a DATASET-BOUND KPI tile
2804
+ (objectui#8889).
2805
+
2806
+ `{ns}.dashboards.{dash}.widgets.{id}.subCaption` had two consumers and only one
2807
+ of them worked on a dataset-bound widget. objectui#7293 taught `DatasetWidget`
2808
+ to render the AUTHORED value (`widget.options.description`), which also carried
2809
+ the server path — `translateDashboard` overlays the resolved translation onto
2810
+ that very key, so a platform-served dashboard arrived pre-translated. But the
2811
+ limb where a bundle entry OVERRIDES the authored value needs the dashboard name,
2812
+ and neither dispatch site handed the component anything but `widget` and
2813
+ `dataSource`.
2814
+
2815
+ The visible effect: a dashboard loaded from an app bundle, whose sub-caption was
2816
+ written only in the i18n bundle and not as `options.description`, rendered
2817
+ nothing on a dataset-bound tile — a shape the field's own documentation calls
2818
+ legitimate ("A translation with no authored counterpart is legitimate and
2819
+ matches the server"). A tile that declared both rendered the untranslated
2820
+ authored string instead of the translation.
2821
+
2822
+ The resolution now happens once, in a new `useWidgetSubCaption` hook, and BOTH
2823
+ dashboard surfaces (`DashboardRenderer` and `DashboardGridLayout` — both route a
2824
+ dataset-bound widget to the same component, objectui#4614) pass the resolved
2825
+ answer down. One decision point, because the field's invariant is that its two
2826
+ channels can never disagree.
2827
+
2828
+ **No behaviour changes for a tile that declares no bundle entry.** The authored
2829
+ limb is untouched, an inline per-locale map still collapses through the same
2830
+ `pickLocalized` seam, and a tile with neither channel still grows no caption
2831
+ node at all. `DatasetWidget` rendered outside a dashboard surface resolves the
2832
+ authored limb for itself exactly as before.
2833
+
2834
+ `@object-ui/sdui-parser` is touched in the same change but releases nothing new: the
2835
+ renderer-consumed-keys census watches `plugin-dashboard`'s source text from inside that
2836
+ package, so moving the sub-caption read moved its evidence anchor. Only the census test
2837
+ and one prose paragraph in the census module's header changed there — no runtime
2838
+ behaviour, no key set, no diagnostic.
2839
+ - db0e9d3: fix(plugin-dashboard): a dimensionless `column` / `horizontal-bar` draws every measure; the dropped-measure warning speaks whenever the widget's own branch leaves a declared measure off the screen; the widget panel stops offering a measure the widget door refuses (objectui#8894)
2840
+
2841
+ **`column` and `horizontal-bar` draw every measure of a dimensionless widget.** They are
2842
+ the bar family in another orientation, so the spec's "the chart families render one mark
2843
+ per measure" covers them, and they now take the same measure transposition `bar` took in
2844
+ objectui#11261: one mark per measure, the measures' labels along the bottom on a `column`
2845
+ and down the side on a `horizontal-bar`. Before, a `horizontal-bar` over revenue, cost and
2846
+ margin with no dimension rendered as a one-number tile showing revenue alone. A
2847
+ single-measure dimensionless widget keeps the tile, as on every type.
2848
+
2849
+ **The dropped-measure warning fires on what the widget's own branch does not render.**
2850
+ `DatasetWidget` derives the measures it renders once, from its branch decision, and both
2851
+ the tile and the console warning read that derivation; the warning speaks when a declared
2852
+ measure is not among them, never from a list of types. Two shapes take the tile with
2853
+ several measures: a metric-family tile stored before `@objectstack/spec` 17.5.0 refused
2854
+ that shape at its door, and a dimensionless widget of a type the spec gives no rendering
2855
+ of several measures (`pie`, `donut`, `funnel`, `scatter`, `radar`, `treemap`, `sankey`),
2856
+ which takes the tile until the spec refuses that shape too. The warning is a diagnostic:
2857
+ it names the widget, the measures it renders and the ones it queried and never displayed,
2858
+ and points to the `replacement` of the spec's ADR-0087 entry
2859
+ `dashboard-widget-metric-family-multi-measure-refused`. It names no widget types and
2860
+ gives no advice of its own. A table, a chart that draws every measure and a
2861
+ single-measure tile stay silent. A dimensioned `pie`, `donut`, `funnel`, `treemap` or
2862
+ `sankey` with several measures draws only the first, and the warning names the measures
2863
+ it drops there too (objectui#11417).
2864
+
2865
+ **`WidgetConfigPanel`'s measure picker follows the widget door.** It asks objectui's
2866
+ `DashboardWidgetSchema` (which re-attaches the spec's own measure check) instead of
2867
+ carrying a list of types: when one more measure would be refused, as on any
2868
+ metric-family widget that already has one, the add control is not offered; when the
2869
+ widget's measures are already refused (a stored document, or a type switched under
2870
+ them), the door's own message is shown under them at once rather than at publish. Every
2871
+ other widget type keeps offering further measures exactly as before.
2872
+ - 0c5cf8b: A dataset-bound metric tile now warns when it drops measures it was told to show
2873
+ (objectui#8894).
2874
+
2875
+ Declaring three measures on a `metric` / `kpi` / `gauge` / `solid-gauge` / `bullet`
2876
+ widget was legal until `@objectstack/spec` 17.5.0, which refuses a second measure on
2877
+ that family at its door; a document stored before then still reaches the tile. So does
2878
+ a widget with no dimensions whose type does not render several measures. The query
2879
+ runs every declared measure, and `DatasetWidget`'s tile rendered the first and stopped
2880
+ there in silence: no warning, no console message, no visual tell. A tile answering a
2881
+ narrower question than its metadata asked read as a finished product, which is the
2882
+ hard part of the defect — not the numbers that never appeared. That is the ADR-0049
2883
+ declared-but-unenforced shape, closed here the same way `warnSuppressedListNav`
2884
+ (ADR-0047, objectui#2338) closes it for a suppressed list-view control.
2885
+
2886
+ **Nothing renders differently on the tile.** Its markup is byte-for-byte what it was,
2887
+ the measures after the first are still dropped, and a tile declaring exactly one
2888
+ measure says nothing at all. Rendering `values[1..]` on a tile is not part of this
2889
+ change: ruling D on objectui#8894 judged the protocol wrong for the metric family and
2890
+ narrowed the spec instead.
2891
+
2892
+ The warning names the widget, its dataset, the measures it renders and each measure
2893
+ it queried and never displayed, and points to the spec's ADR-0087 replacement for a
2894
+ one-number tile with several measures. Its wording is deliberate: the dropped
2895
+ measures are **queried and then never displayed**, not "ignored". They are computed
2896
+ by the server, they join the widget's refetch signature, and `options.sortBy` accepts
2897
+ any of them — which on a `metric`-typed widget that also declares dimensions decides
2898
+ which row is read. Only the display drops them.
2899
+ - 0ab1de2: An authored drill `columns` whitelist no longer degrades the headers of the table it
2900
+ narrows (objectui#9000).
2901
+
2902
+ `ObjectDataTable` derives its column headers on two branches of one memo, and both call
2903
+ the same `fieldLabel` lookup. Only the fallback differed: the auto-derived branch fell
2904
+ back to the humanized field key, the explicit-whitelist branch fell back to whatever
2905
+ `header` the caller supplied. Both drill callers build `{ accessorKey: c, header: c }`
2906
+ out of a `drillDown.columns` string list, so that fallback was the raw field key — and
2907
+ with no bundle entry to resolve, which is the ordinary case in a drill, authoring
2908
+ `columns` rendered `close_date` where the same table without a whitelist rendered
2909
+ `Close Date`. The more deliberate configuration produced the less finished table.
2910
+
2911
+ The whitelist branch now falls back to the same humanized key the auto-derived branch
2912
+ uses. **Visible change:** a drill-through list configured with `drillDown.columns` (the
2913
+ pivot, metric and chart drill surfaces) renders humanized headers where it previously
2914
+ rendered raw field names. Nothing else about the whitelist changes — it still narrows
2915
+ the table to exactly the listed columns, in the listed order.
2916
+
2917
+ The fallback is **conditional**, and deliberately so: it fires only where the column
2918
+ carries no display text, or carries the raw field key itself. An author-written label —
2919
+ `{ accessorKey, header }` or the spec-canonical `{ field, label }`, both of which
2920
+ `normalizeColumns` resolves onto the same key, and both of which `DashboardRenderer`
2921
+ forwards here verbatim from an authored widget — is left exactly as authored. A
2922
+ translated bundle entry still wins over both, unchanged.
2923
+ - 526fc11: `object-metric` refuses `drillDown.filter` and `drillDown.mode` on its prop type (objectui#9002)
2924
+
2925
+ Refused at the TypeScript door only. A stored JSON metric config carrying either key is
2926
+ still accepted and ignored at render, as it was before: no runtime validator in this
2927
+ repository reads the members of an `object-metric` `drillDown`.
2928
+
2929
+ Neither key ever had a read site on a metric. A metric is one aggregated number, so it has
2930
+ no clicked point for a drill `filter` to interpolate `${event.*}` against, and no row for
2931
+ `mode` to open as a record. Its drilled list is always scoped by the block's own `filter`,
2932
+ which is the registration's promise that the number and the records behind it agree. Both
2933
+ keys still type-checked on the widget's `drillDown` prop and then did nothing, with no
2934
+ diagnostic.
2935
+
2936
+ - `@object-ui/types` adds `ObjectMetricDrillDownConfig`, published on the root entry
2937
+ `@object-ui/types` and on the `@object-ui/types/data-display` subpath: `DrillDownConfig`
2938
+ with `filter?: never` and
2939
+ `mode?: never` tombstones whose docblocks name the blocks that do read each key
2940
+ (`object-chart` and `object-pivot` for `filter`, `object-data-table` for `mode`). The
2941
+ shared `DrillDownConfig` is unchanged and keeps both keys for those blocks.
2942
+ - `@object-ui/plugin-dashboard` types `ObjectMetricWidget`'s `drillDown` prop with it, and the
2943
+ `object-metric` registration's `drillDown` description now says in one sentence that the
2944
+ two keys do not apply to a metric and where they do.
2945
+
2946
+ TSX code that passes `filter` or `mode` inside `ObjectMetricWidget`'s `drillDown` now fails
2947
+ to compile. Delete the key: the drilled list was already scoped by the metric's own
2948
+ `filter`, and nothing read `mode`. Runtime behaviour is unchanged.
2949
+ - 9b8bb84: Compose an `ObjectPivotTable` drill-down filter instead of spreading it, so the pivot's own
2950
+ filter survives into the drilled query for BOTH dialects it reads (objectui#9024).
2951
+
2952
+ **The defect.** The drill seam composed the pivot's filter with the click context by spreading
2953
+ the first into an object literal — the identical statement objectui#8944 removed from
2954
+ `ObjectChart` one block over. Spreading an array yields index keys, so a pivot scoped by
2955
+ `[['region','=','emea']]` drilled as `{ '0': ['region','=','emea'], stage: 'won', source: 'web' }`
2956
+ — the pivot's conditions replaced by a stray index key. Nothing errored; the drawer opened and
2957
+ looked right. Measured at the two sinks: the URL writer skips a bare array comparand, so the list
2958
+ landed scoped by the clicked cell ALONE (the superset), and the in-memory matcher refuses that
2959
+ comparand and returns nothing at all. Neither carries the pivot's own conditions.
2960
+
2961
+ **Direction of the failure.** The pivot's filter is what narrows. Dropping it made the drilled
2962
+ list a **superset** — it showed records the pivot itself was scoped to exclude.
2963
+
2964
+ **Why the array arm is reachable here.** `PivotTableSchema` declares no `filter` at all (the
2965
+ type that admits the value is a local props-intersection `filter?: any`), so nothing refuses the
2966
+ array form — and it is not only a hand author who can produce it. `object-pivot`'s registry entry
2967
+ advertises `{ name: 'filter', type: 'array' }` in its designer inputs, and `ElementDataSourceGate`
2968
+ writes the array arm mechanically: binding a pivot the way the spec documents
2969
+ (`dataSource: { object, view }`) sets `schema.filter` to `mergeFilterNodes(schema.filter,
2970
+ view.filter)`, an ObjectQL AST node, for every saved view that carries a filter. So the pre-fix
2971
+ drill dropped the scope of every view-bound pivot, not just of a pivot whose author happened to
2972
+ pick the array spelling.
2973
+
2974
+ **The fix.** `composeDrillFilter` (`@object-ui/core`, added by objectui#8944) already names and
2975
+ applies the rule — `widget.filter` conjoined with `drill.filter`, via the repo's single filter
2976
+ sink `mergeFilterNodes` — and lowers the result back to the object dialect both of this widget's
2977
+ drill sinks take. Routing this site through it replaces the local spread with the shared answer
2978
+ rather than deriving a second one.
2979
+
2980
+ **Compatibility.** The drilled ROW SET is unchanged wherever it was already correct, which is the
2981
+ oracle this change is measured by. The composed VALUE can change shape: a pivot cell click derives
2982
+ two conditions (row field and column field), which are themselves a conjunction, so a pivot with no
2983
+ filter of its own now drills by `{ $and: [{ stage: 'won' }, { source: 'web' }] }` where the spread
2984
+ produced one flat object. Every sink accepts both — `$filter` takes `$and`, and
2985
+ `serializeDrillFilterParams` flattens nested `$and` into the same `filter[...]` params it always
2986
+ wrote (pinned since objectui#8944). When neither source carries anything the seam now answers
2987
+ `undefined` rather than `{}`; `DrillDownDrawer.filter` is optional, so the drilled list is unscoped
2988
+ either way.
2989
+ - 5e9b502: Fix `DashboardGridLayout` composing chart series with no `label` at all
2990
+ (objectui#9172), so a grid-laid-out dashboard legended a measure like
2991
+ `close_date` under the raw field key while the read-only `DashboardRenderer`
2992
+ relay legended the same widget "Close Date". The three-arm label decision
2993
+ (synthetic aggregate name / bundle-or-humanized field label) is now
2994
+ `composeSeriesLabel` (`packages/plugin-dashboard/src/utils.ts`), a single
2995
+ authority both relays call, rather than a second copy of the logic.
2996
+ - a78cd37: Dates and numbers across the console and the plugins format in the session's
2997
+ display locale instead of the machine's (objectui#9909).
2998
+
2999
+ Every one of these faces handed `Intl` — directly, or through a formatter called
3000
+ without one — either no locale tag or an explicit `undefined`, which is not "the
3001
+ user's locale": it is the locale of the machine the browser runs on, which is
3002
+ neither of this repository's two locale channels.
3003
+ A German or Spanish session therefore read, beside a translated label, a date or
3004
+ an amount grouped and decimal-marked the machine's way — and an amount with
3005
+ inverted separators does not read as unformatted, it reads as a different
3006
+ number. They now go through `useDisplayLocale()` (tenant regional default, then
3007
+ the active UI language, then `'en'`), and a plain helper takes that tag from its
3008
+ caller. The surfaces:
3009
+
3010
+ - **`@object-ui/fields`** — `GridField`'s numeric and currency cells (list mode
3011
+ and computed columns) and both total cells. The same cell helper's date branch
3012
+ already used the display locale, so a single grid row read two conventions at
3013
+ once.
3014
+ - **`@object-ui/plugin-charts`** — ISO-date x-axis ticks, compact y-axis ticks,
3015
+ the single-value face, spec `format` strings and the tooltip value.
3016
+ - **`@object-ui/app-shell`** — the organization invitations, members and
3017
+ accept-invitation dates; marketplace version dates; the AI conversation row's
3018
+ older-than-a-week date and its tooltip; the build-debug token count; the
3019
+ record approvals timeline; and the metadata-admin audit, history, external
3020
+ datasource snapshot, schema-browser row estimate, flow-run start and job
3021
+ next-fire faces. The audit, history and flow-run panels format these in the
3022
+ DISPLAY locale, not in the `locale` prop they take for their UI strings
3023
+ (`AuditPanel` defaults that prop to `'en-US'`; `HistoryPanel` and
3024
+ `FlowRunsPanel` leave it optional).
3025
+ - **`@object-ui/console`** — the approvals inbox's timestamp tooltips, amounts
3026
+ and payload summary; the audit log's timestamp column; the flow runs table and
3027
+ its run detail.
3028
+ - **`@object-ui/components`** — the export dialog's record counts and the debug
3029
+ panel's event times.
3030
+ - **`@object-ui/plugin-form`** — the analytics submission count, the
3031
+ master-detail subtotal / tax / total stack and the edit-conflict dialog's
3032
+ "their save" time.
3033
+ - **`@object-ui/plugin-chatbot`** — the approvals inbox's past-30-days date and
3034
+ the times stamped on local-mode chat messages (the user's and the
3035
+ auto-response).
3036
+ - **`@object-ui/plugin-dashboard`** — the record-count badge, and the currency
3037
+ and date-format branches of `renderFieldValue`, which was handed the display
3038
+ locale and spent it only on its percent branch.
3039
+ - **`@object-ui/plugin-grid`** — the record-detail panel's inferred currency
3040
+ value and the mobile card's amount line, siblings of date cells already on the
3041
+ display locale.
3042
+ - **`@object-ui/plugin-designer`** — `VersionHistory`'s version times.
3043
+
3044
+ A host that mounts one of the components that newly read the display locale with
3045
+ NO `I18nProvider` now gets react-i18next's once-per-module `NO_I18NEXT_INSTANCE`
3046
+ notice in the console on the first such mount; the faces still render, in the
3047
+ channel's `'en'` last resort, and mounting an `I18nProvider` (as the console
3048
+ does) avoids the notice.
3049
+
3050
+ **Additive API** (nothing that compiled before stops compiling):
3051
+
3052
+ - `@object-ui/types`: `ValidationContext` gains an optional `locale`. At this
3053
+ change, the `@object-ui/core` validation engine prints the `date_min` /
3054
+ `date_max` bound in it; omitted, `Intl` follows the runtime default, as
3055
+ `formatDisplayNumber` already declares for a caller with no locale in hand.
3056
+
3057
+ ⚠️ **Dated note, 2026-09-25 — that engine (`ValidationEngine`) is removed from
3058
+ `@object-ui/core` by a later change; `ValidationContext.locale` stays declared in
3059
+ `@object-ui/types`, and at that change nothing in `@object-ui/core` reads it —
3060
+ objectui#7659.**
3061
+
3062
+ ⚠️ **Dated note, 2026-09-27 — `ValidationContext` itself is removed from
3063
+ `@object-ui/types` by a later change, with the rest of the Phase 3.5
3064
+ validation types it belonged to, so the `locale` member this entry adds no
3065
+ longer exists — objectui#10719.**
3066
+ - `@object-ui/plugin-report`: `exportReport`, `exportAsHTML` and `exportAsPDF`
3067
+ take a trailing optional `locale` for the exported file's "Generated:" time,
3068
+ and `LiveExportOptions` gains an optional `locale` that `exportWithLiveData`
3069
+ forwards. Omitted, the time uses the display channel's own last resort
3070
+ (`'en'`), never the machine's locale. `ReportViewer` passes the session's
3071
+ display locale. The locale is deliberately NOT a `ReportExportConfig` member:
3072
+ that type is authored report metadata, and a display locale belongs to the
3073
+ session.
3074
+
3075
+ **One census, repository-wide.** `plugin-detail`'s machine-locale census pin
3076
+ (objectui#9786) is now a single test, `machineLocaleCensus-9909.test.ts` in
3077
+ `@object-ui/i18n`, covering every workspace package whose manifest depends on
3078
+ `@object-ui/i18n`. It refuses any call site that passes nothing, `undefined`,
3079
+ the `'default'` pseudo-tag (a subtag no locale data answers, so `Intl` resolves
3080
+ it to the machine's locale) or a hard-coded tag, unless the site is declared
3081
+ with its reason — for example the `catch` fallback for a tag `Intl` itself
3082
+ rejected, or an ISO formatter feeding `<input type="date">`. Each declared site
3083
+ is counted exactly, so deleting one without its entry is refused too. The
3084
+ per-package pin it replaces is deleted. The runtime tripwire that observes the
3085
+ argument each locale-taking call (`Intl` constructors, `Date` and `Number`
3086
+ `toLocale*`) actually receives moved into the private `@object-ui/test-support`
3087
+ package, and every surface above is pinned with it: the same data under two
3088
+ declared locales must render differently, NO locale-taking call may receive the
3089
+ machine's locale (nothing, `undefined` or `'default'`), and the surface's own
3090
+ calls must receive the declared tag. A call carrying another declared tag, such
3091
+ as the `'en'` of the session's UI language, is tolerated by design: only the
3092
+ machine's locale is the defect.
3093
+ - 3d79455: `plugin-dashboard`'s two private copies of the reference-bearing field family now
3094
+ read `@object-ui/core`'s published `EXPANDABLE_FIELD_TYPES` instead of restating
3095
+ it — `LOOKUP_TYPES` in `recordFields.tsx` and the inline disjunction inside
3096
+ `computeLookupExpand` in `ObjectDataTable.tsx`. Both carry the identity pin the
3097
+ already-converged consumers carry, so a member-identical private copy fails
3098
+ rather than quietly re-forking the table.
3099
+
3100
+ Two behaviour changes on the dashboard table's `$expand`, in opposite directions:
3101
+
3102
+ - **A `tree` column is now expanded.** A self-referencing hierarchy field is
3103
+ reference-bearing and a member of the shared family, so its cell renders the
3104
+ parent record's display name instead of a bare id — the same treatment the form
3105
+ and grid roads already gave it.
3106
+ - **A `reference`-typed column is no longer expanded.** Measured before removing
3107
+ it: `reference` is absent from `@objectstack/spec`'s closed `FieldType`
3108
+ vocabulary and is refused by `FieldSchema.safeParse`, so no spec-compliant
3109
+ object schema can declare a field whose stored type is `reference`. Dropping it
3110
+ is a no-op on real data; the spelling is a legacy dialect alias on the
3111
+ action-param surface, folded to `lookup` before any field-type data is read.
3112
+
3113
+ `EXPANDABLE_FIELD_TYPES` itself is unchanged — the measurement did not license
3114
+ widening a published shared set.
3115
+ - cc5de5c: `DashboardRenderer` no longer emits an empty dashboard header wrapper. The
3116
+ wrapper used to render whenever `header` was declared, while each of its
3117
+ children — title, description, actions — was additionally suppressible. With
3118
+ the console page chrome present (`hideHeaderText`, set because the chrome
3119
+ already renders the dashboard's title and description) and no `header.actions`,
3120
+ every child evaluated falsy and the DOM still received
3121
+ `<div class="col-span-full mb-4"></div>`: zero children, yet a full grid row
3122
+ (measured 64px) plus `mb-4` of dead band above the filter bar, on every console
3123
+ dashboard page (objectui#5812, measured on HotCRM 17.1.0).
3124
+
3125
+ The three children are now computed first and the wrapper renders only if one
3126
+ of them survives. Nothing else changes: a standalone embed (no chrome) renders
3127
+ title and description exactly as before, and declared `header.actions` keep the
3128
+ wrapper alive even under the chrome, since the chrome renders text only. Authors
3129
+ needed this — dropping `header` from the metadata to reclaim the pixels would
3130
+ have cost the standalone embed its title, which is what `header` is for.
3131
+ - 84d2e98: `DashboardRenderer` drops the unreachable `DatasetWidget` fork from its self-contained
3132
+ (card-less) branch, leaving that branch to render `SchemaRenderer` unconditionally
3133
+ (objectui#4620).
3134
+
3135
+ `isSelfContained` is defined as `widget.type === 'metric' && !datasetBound`, and the
3136
+ `isSelfContained` arm of `renderedNode` then forked on `datasetBound` a second time. The
3137
+ `datasetBound` side of that inner fork could never execute: reaching it required
3138
+ `isSelfContained` to be true, which requires `!datasetBound`. Behaviour is unchanged —
3139
+ the removed arm never ran, and the reachable fork in the Card branch (the one that gives
3140
+ a dataset-bound metric its title and border chrome) is untouched.
3141
+
3142
+ The cost was to readers, not to users: the shape read as "both branches handle
3143
+ dataset-bound widgets" when only one can, and a previous PR mirroring this fork onto
3144
+ `DashboardGridLayout` had to pay for the reachability argument before it could decline to
3145
+ copy the dead limb. A comment now names the invariant in place so the arm is not re-added.
3146
+ - f24195a: `ObjectDataTable` and `ObjectPivotTable` now use a module-scope frozen empty for
3147
+ "no rows yet" instead of a fresh array literal per render (objectui#4629).
3148
+
3149
+ Both spelled the resolved row list as `Array.isArray(rawData) ? rawData : []`, so
3150
+ whenever `rawData` was a truthy non-array — a provider-config `data`, or a `bind`
3151
+ path that resolves to an object — the fallback produced a NEW array identity on
3152
+ every render. In `ObjectDataTable` that value keys the `derivedColumns` memo, so
3153
+ every column was re-derived (`buildFieldMeta`, a fresh `cell` closure, the
3154
+ `isSystemField` pass, the `fieldLabel` lookups) and then discarded by the
3155
+ `finalData.length === 0` early return. In `ObjectPivotTable` the value is handed
3156
+ straight to `PivotTable`, where it keys the cross-tabulation memo, so the pivot
3157
+ rebuilt its row/column sets, bucket map and totals on every render over no rows
3158
+ at all.
3159
+
3160
+ Nothing rendered wrong before or after; this is wasted work in the empty window,
3161
+ plus the live `react-hooks/exhaustive-deps` warning the conditional raised. It is
3162
+ the same module-scope frozen empty `data-table.tsx` adopted for its own
3163
+ `EMPTY_ROWS` (objectui#4618), applied to the `provider: 'object'` siblings.
3164
+ - 56f4e34: A dashboard table's auto-derived column headers spell a field key the same way every other path in the `table` widget family does.
3165
+
3166
+ `ObjectDataTable` derives headers on two paths — from the author's declared
3167
+ `columns`, and from the object schema when no columns were declared. The
3168
+ declared path (and the static `data-table` half of the same widget family)
3169
+ already used `humanizeFieldKey`, whose docstring names it "the single home for
3170
+ the convention, because both halves of the `table` widget family need it and
3171
+ they must agree". The auto-derived path carried a third, inline spelling that
3172
+ split camelCase but never turned `_` into a space, so it left a raw underscore
3173
+ on screen. Measured over the same object's columns:
3174
+
3175
+ ```
3176
+ path close_date needs_analysis
3177
+ object-bound, AUTO-DERIVED (before) Close_date Needs_analysis
3178
+ object-bound, AUTO-DERIVED (after) Close Date Needs Analysis
3179
+ object-bound, DECLARED columns Close Date Needs Analysis
3180
+ static `data-table`, no columns Close Date Needs Analysis
3181
+ ```
3182
+
3183
+ One dashboard can hold all three widgets over one object, so a single field key
3184
+ rendered under two spellings — the defect class this change rules out. The odd
3185
+ path adopts the shared convention rather than the convention gaining a fourth
3186
+ dialect. camelCase keys are unaffected (`unitPrice` read `Unit Price` before and
3187
+ after — the coincidence that kept the snake_case divergence unnoticed), and a
3188
+ translated header still wins: only the fallback handed to `fieldLabel` changed.
3189
+
3190
+ Dimension MEMBER labels are untouched by this. The same card reported dashboard
3191
+ members rendering a prettified enum instead of the picklist's translated label,
3192
+ measured on 17.1.0; re-measured on this branch it no longer reproduces — the
3193
+ analytics label net shipped in 17.5.0 routes every non-metric dataset dimension
3194
+ through the field's declared options and the locale bundle. That behaviour had
3195
+ no test stated in the card's terms and now has one, over the four dashboards the
3196
+ card measured, including the property that a bar axis and a pivot header cannot
3197
+ disagree about one stored value.
3198
+ - b9cefe6: One field key now renders under one spelling across a dashboard drill chain: the record drawer and the chart series label both derive their display string with `humanizeFieldKey`, the single home for the KEY convention (objectui#9055).
3199
+
3200
+ **The record drawer, on the same click.** `RecordDetailDrawer` built its field label from a fourth inline spelling that upper-cased only the FIRST word. `ObjectDataTable`'s drill list is drill-to-record by default and the record it opens is this drawer, so one click moved from a column headed `Close Date` to a field labelled `Close date` — same key, same widget, one uninterrupted interaction. Measured on the rendered DOM of one chain, before the fix: headers `Close Date · Needs Analysis · Unit Price · Amount` against drawer labels `Close date · Needs analysis · Unit Price · Amount`. camelCase agreed under both spellings, which is why it went unnoticed.
3201
+
3202
+ **The chart series label, on two of its three arms.** `resolveSeriesLabel` in `DashboardRenderer` handed back the RAW field key twice: as `fieldLabel`'s fallback on the object-bound arm, and outright on the arm every static-data chart takes. A chart grouped on `close_date` legended it `close_date` while the table widget beside it headed that column `Close Date`. The static-data arm is not named by the card; it is the same defect reached through the other call site, and it is fixed here rather than left for a follow-up. The synthetic-key arm — a count aggregation with no real field, which resolves an i18n'd aggregate name (Count / 计数) — is a different vocabulary and is deliberately untouched.
3203
+
3204
+ The i18n wrapper is unchanged everywhere: a bundle entry still wins and the derived spelling is only its fallback, and the `objectName` guards are as they were. No published export, schema key or registry entry moves. The KEY prefixer stays distinct from `humanizeLabel`, the VALUE prefixer in `@object-ui/core` — converging those two is a separate decision its own docblock already reserves for its own card.
3205
+ - fb336df: Move `lucide-react` from `^1.31.0` to `^1.43.0` in every package that declares it, and
3206
+ repair what the jump breaks, so icons resolved from a STRING keep drawing a glyph.
3207
+
3208
+ Measured once against the installed 1.43.0 artifact when this change was made; nothing in
3209
+ the repository re-derives these readings. Across the jump lucide removes no runtime export,
3210
+ no public type name and no `lucide-react/dynamic.mjs` name, and every name this repository
3211
+ imports from `lucide-react` resolves. Exactly one key leaves the runtime `icons` record:
3212
+ `Trash2`, retired in favour of `trash`.
3213
+
3214
+ What a user sees change:
3215
+
3216
+ - Icons the lazy icon seam draws (`resolveIcon`, objectui#9251) get their path data again.
3217
+ lucide 1.43.0 icon modules export their path data inside `__iconData` and no longer
3218
+ export `__iconNode`; the seam now reads both. Reading only `__iconNode` against 1.43.0
3219
+ leaves every such glyph an empty box, with nothing thrown or logged.
3220
+ - `DetailView`'s delete action and three schema-catalog examples spell their icon
3221
+ `trash`, not `trash-2`, so they keep resolving. The glyph is unchanged: 1.43.0's `trash`
3222
+ path data is byte-identical to the `trash-2` path data of 1.31.0 and 1.35.0. Anything
3223
+ that already authored `trash` now draws that same artwork, because lucide moved it under
3224
+ the `trash` name; a few other glyphs were also redrawn upstream.
3225
+ - Icons imported as COMPONENTS carry lucide 1.43.0's own classes: one class per declared
3226
+ alias (a spinner renders `class="lucide lucide-loader-circle lucide-loader-2 ..."`), and
3227
+ no longer the class lucide used to derive from the PascalCase key where that differs
3228
+ (`ArrowDown01` no longer carries `lucide-arrow-down01`). The canonical
3229
+ `lucide-<icon name>` class is still there, so a `.lucide-loader-circle` selector still
3230
+ matches.
3231
+ - Icons resolved from a STRING through the seam keep the classes they had: `lucide`, the
3232
+ canonical `lucide-<icon name>` class and the key-derived class. They do not carry
3233
+ lucide's per-alias classes. That is a maintainer ruling (objectui#8941, option C): the
3234
+ alias list lives only inside each lazily loaded icon module, and it was not moved into
3235
+ the eager name list. A selector that targets an alias class matches a component-imported
3236
+ icon and not a string-resolved one. This also dates the example in the objectui#9251
3237
+ entry: `trash-2` no longer resolves as a string, so no seam-drawn glyph carries
3238
+ `lucide-trash2 lucide-trash-2`; a digit-bearing name that still resolves, such as
3239
+ `arrow-down-0-1`, carries `lucide-arrow-down01 lucide-arrow-down-0-1`.
3240
+ - 84d70bb: `ObjectPivotTable` now depends on the `fieldLabel` / `fieldOptionLabel`
3241
+ resolvers directly instead of holding them behind refs, so a pivot re-derives
3242
+ its header and option labels when the resolver genuinely changes
3243
+ (objectui#5625).
3244
+
3245
+ The refs existed for a reason that no longer holds. `useSafeFieldLabel()`
3246
+ returned a fresh object on every render outside an i18next provider, so a direct
3247
+ dependency would have re-run the metadata-derivation effect on every render —
3248
+ and that effect ends in `setFieldLabelMaps` / `setFieldNameLabels` with freshly
3249
+ built objects, so each run scheduled the next one: an unbounded derive loop.
3250
+ `ObjectPivotTable` worked around that locally with `fieldLabelRef` /
3251
+ `fieldOptionLabelRef` plus a `useEffect` keeping them current.
3252
+ `useObjectLabel`'s memo now holds with or without an i18next instance bound
3253
+ (objectui#5564), so both resolvers have a stable identity on both paths and the
3254
+ indirection buys nothing.
3255
+
3256
+ It did cost something, and that is the user-visible half: a ref-hidden
3257
+ dependency meant the derivation did NOT re-run when the resolver changed. A
3258
+ pivot mounted before its `I18nProvider`, or rendered across a language switch,
3259
+ kept showing its top-left header label and its select-option cell labels as
3260
+ resolved by the old resolver — until some unrelated dependency (the data source,
3261
+ the object name) happened to move. It now re-derives once on that transition and
3262
+ renders in the active language.
3263
+
3264
+ This is the same removal objectui#5587 made in `ObjectChart`, one package over.
3265
+
3266
+ Pinned by `ObjectPivotTable.i18nResolverDeps.test.tsx`, which counts derivations
3267
+ across forced re-renders both outside and inside a provider, checks that the two
3268
+ derived state maps settle, and asserts the language-switch re-derivation.
3269
+ Reverting `useObjectLabel.ts` to its pre-objectui#5585 state turns the
3270
+ no-provider case red (derivations in the dozens instead of 1), so the removal is
3271
+ pinned to the fix that unlocked it rather than to a comment.
3272
+ - f1c27f0: Dashboard record fields: percent columns now render through the one percent
3273
+ scaling decision instead of a second, drifted copy of it.
3274
+
3275
+ `renderFieldValue`'s `%`-format branch normalised the value itself before
3276
+ calling `formatPercent` (`const normalized = value > 1 ? value / 100 : value`,
3277
+ then `normalized * 100`). `formatPercent` already applies `percentDisplayValue`,
3278
+ which `@object-ui/core` documents as the single source of truth for percent
3279
+ display scaling, so the branch was re-deciding what core owns — and its copy had
3280
+ drifted from it in three measured ways:
3281
+
3282
+ - `(value / 100) * 100` is not value-preserving in binary floating point,
3283
+ re-introducing one call frame upstream the round trip that was removed from
3284
+ inside `formatPercent`. On the 0.001-step grid to 200, 19,978 of 199,000
3285
+ values change bit pattern and 1,108 rendered strings move, every one a
3286
+ last-digit off-by-one: a stored `1.605` rendered `1.60%` where half-up is
3287
+ `1.61%`.
3288
+ - A stored fraction below `0.01` was scaled twice — the local `* 100` put it
3289
+ back under 1, so core's fraction arm scaled it again. `0.005` (0.5%) rendered
3290
+ `50.00%`.
3291
+ - The local test was `value > 1` rather than core's symmetric `|value| < 1`, so
3292
+ a negative already in percentage points took the fraction arm: `-5` rendered
3293
+ `-500.00%`.
3294
+
3295
+ The branch now hands the raw stored value to `formatPercent` — the identical
3296
+ call the list-view percent cell already makes for an ordinary percent column —
3297
+ so a percent reads the same as a record field, as a grid cell and as a dashboard
3298
+ measure. Output moves where it was wrong: values at or above 1 whose round trip
3299
+ lost a digit, fractions below `0.01`, negatives at or below `-1`, and exactly
3300
+ `1`, which is one percentage point by core's convention and now renders
3301
+ `1.00%` at two decimals, where the local `value > 1` test had made it
3302
+ `100.00%`.
3303
+
3304
+ ⚠️ **Dated note, 2026-10-02 — the magnitude guess is retired in this same release — objectui#11475.**
3305
+ Later in this same release `percentDisplayValue` (`@object-ui/core`) stopped
3306
+ inferring a percentage's storage from the value: it takes the storage as a
3307
+ required argument, and every percent face passes the storage the field
3308
+ declares (`percentScaleOf` in `@objectstack/spec/data`: a fraction unless the
3309
+ field declares a `max` above 1). So exactly `1` on a `percent` field that declares no `max` renders `100.00%`,
3310
+ one on a field declaring `max: 100` renders `1.00%`, and a non-percent field
3311
+ with a `%` pattern reads its value as a fraction, numeral's own reading. The rest of this entry is kept as the reading of this change.
3312
+ - 0ccbdc1: `PivotTable` no longer re-runs its cross-tabulation memo on every render when it
3313
+ has no rows (objectui#5562).
3314
+
3315
+ The component spelled the empty array twice — as the destructuring default for
3316
+ `schema.data` and as the `Array.isArray` fallback that keeps a provider-config
3317
+ object out of iteration — so a schema declaring no `data` key, or one whose
3318
+ `data` is a provider config rather than rows, produced a fresh array identity on
3319
+ every render. That value is the first entry of the memo's dependency list, so
3320
+ the memo rebuilt its two ordered key sets, its `bucket[row][col]` map, the
3321
+ aggregated matrix and the row/column/grand totals on every render, over nothing.
3322
+ Both spellings now resolve to one module-scope frozen empty, so "no rows" is a
3323
+ stable value and the memo holds.
3324
+
3325
+ Wasted work only: the churn feeds a memo rather than a `setState`, and
3326
+ `PivotTable` holds no prop-to-state sync, so nothing rendered wrong and no
3327
+ render loop was possible. The identical fix landed for `data-table` in
3328
+ objectui#4618 and for `ObjectPivotTable` in objectui#4629; this closes the
3329
+ direct-use path those two did not cover, where `DashboardRenderer` and
3330
+ `DashboardGridLayout` construct pivot schemas without `ObjectPivotTable` in the
3331
+ chain.
3332
+ - 6c6cee7: A RETIRED field-type spelling is now refused — out loud, once — by every
3333
+ field-type predicate in the renderer, not just by the widget road
3334
+ (objectui#4914, maintainer ruling B of 2026-08-18).
3335
+
3336
+ `@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs
3337
+ ahead of six predicate faces that previously granted a retired spelling
3338
+ first-class treatment: the filter builder's operator buckets and its value
3339
+ control (`@object-ui/components`), the detail page's highlight-strip picker
3340
+ (`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`),
3341
+ the dashboard's `$expand` whitelist and `isLookupType`
3342
+ (`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter
3343
+ control (`@object-ui/plugin-list`). Each one now fires the migration
3344
+ prescription on the console — once per spelling across all of them, never once
3345
+ per predicate — and then answers as it would for a spelling it does not
3346
+ recognise.
3347
+
3348
+ This closes the whole CLASS rather than one word: the gate is quantified over
3349
+ `RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the
3350
+ day it lands. It is the shape objectui#4932 and objectui#4942 already
3351
+ established for the form and inline-edit roads.
3352
+
3353
+ Measured before the change, and the reason the fix is a gate rather than a
3354
+ deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')`
3355
+ equalled the `user` bucket item for item, `computeLookupExpand` actively
3356
+ requested `$expand` for it, `isLookupType('owner')` was `true` alongside
3357
+ `reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as
3358
+ `picklist` does. Deleting the members alone would have traded a visible
3359
+ contradiction for a SILENT degradation — a filter picker collapsing to a bare id
3360
+ box, `$expand` quietly stopping so cells show raw foreign-key ids — which is
3361
+ verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent.
3362
+ The gate keeps that fallback and adds the half that was missing: the author is
3363
+ told.
3364
+
3365
+ The boundary question is answered on record: `owner` arriving through a
3366
+ backend-vocabulary normalizer is an authoring error to refuse loudly, not
3367
+ legitimate foreign input to tolerate. The open backend vocabulary those
3368
+ normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`,
3369
+ `datetime_tz` and the rest are equally absent from the spec's closed `FieldType`
3370
+ and are equally unretired, so they classify exactly as before.
3371
+
3372
+ `RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports`
3373
+ move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that
3374
+ package's published surface is unchanged apart from the newly ruled gate.
3375
+ `@object-ui/components` is a consumer of the gate and `@object-ui/fields`
3376
+ depends on it, so a single shared table could not live in `fields` — and a
3377
+ second copy would have meant a second dedupe set and two console lines for one
3378
+ spelling. No package gained a new dependency.
3379
+
3380
+ A retired spelling never loses a stored value: `retypeFilterValue` is
3381
+ deliberately not gated, and the refused filter row stays operable rather than
3382
+ drawing a blank operator trigger.
3383
+ - cfcff30: Each package's README now states, up front, that it needs a bundler: importing it from plain Node ESM fails, and that is a supported-configuration boundary rather than a defect.
3384
+
3385
+ `@object-ui/plugin-dashboard` imports `react-grid-layout/css/styles.css` at module
3386
+ scope and `@object-ui/plugin-map` imports `maplibre-gl/dist/maplibre-gl.css`;
3387
+ `@object-ui/app-shell` reaches the first of those through the static
3388
+ `@object-ui/plugin-dashboard` imports in `DashboardView` and `ReportView`. Node has
3389
+ no loader for `.css` at all, so all three resolve and then die during evaluation:
3390
+
3391
+ ```
3392
+ TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".css"
3393
+ for .../react-grid-layout/css/styles.css
3394
+ ```
3395
+
3396
+ Nothing about how these packages load has changed — every supported host bundles
3397
+ them (Vite, webpack, or Next with the package in `transpilePackages`), and that is
3398
+ still the only supported way to consume them. What changed is that the boundary is
3399
+ now written where a consumer meets it, instead of being learned from a red import.
3400
+
3401
+ objectui#5384 ruled unbundled Node consumption **unsupported** for style-carrying
3402
+ plugin packages — permanently, over the three packages as a group — rather than
3403
+ moving the stylesheet imports out of module scope. No unbundled-Node consumer
3404
+ exists, and buying permanent machinery to close a capability gap nobody is pulling
3405
+ on was the trade the ruling declined. A real consumer request reopens it as a
3406
+ design question, not as a defect: the READMEs say so and name the issue.
3407
+ - fb96ecb: `WidgetConfigPanel` reads an inline-locale-map title, and a save no longer destroys the other locales.
3408
+
3409
+ The dashboard widget config panel carried a private `resolveLabel` documented as
3410
+ resolving an `I18nLabel` while reading `defaultValue || key` — the key-reference
3411
+ form `@objectstack/spec` retired at 17.0.0-rc.6 (objectstack#5055). The inline
3412
+ per-locale map `I18nLabelSchema` actually admits has neither limb, so
3413
+ `{ en: 'Revenue', zh: '收入' }` resolved to `''`. It was the fourth private copy
3414
+ of that resolver; objectui#4032 swept the other three out of `DashboardRenderer`,
3415
+ `MetricWidget` and `MetricCard`.
3416
+
3417
+ This was not a display bug. The resolved value seeds the panel's editable draft,
3418
+ so a widget whose stored title was a map opened with an **empty** Title field and
3419
+ the next save wrote `''` over the author's map — on the ordinary path, not an
3420
+ exotic one: open the widget, change anything, save.
3421
+
3422
+ Both halves are fixed, per the maintainer's 2026-08-20 ruling on objectui#5301:
3423
+
3424
+ - **Reading** goes through `pickLocalized(value, language)`, so the panel shows
3425
+ the active locale like every sibling surface post-objectui#4032.
3426
+ - **Writing** replaces only the active locale's entry and carries every other
3427
+ locale across. A title the author never touched round-trips the stored object
3428
+ itself through an unrelated config edit; an edited one merges into the entry
3429
+ that was displayed. The live-update callback (`onFieldChange`) forwards the
3430
+ merged map for the same reason — hosts feed it back into the widget the panel
3431
+ re-opens from, so a bare string there dropped the map before a save ever ran.
3432
+
3433
+ `@object-ui/i18n` gains `setLocalized(value, language, next)`, the write-side
3434
+ inverse of `pickLocalized`, so the rule is stated once instead of re-derived per
3435
+ panel. It follows `pickLocalized`'s first three limbs — exact tag, base language,
3436
+ region-qualified sibling — and deliberately stops there: the `default` / `en` /
3437
+ first-value limbs are display fallbacks that hand back *another* locale's string,
3438
+ and writing to one would let an author editing in `fr` overwrite English. With no
3439
+ entry for the active locale the edit adds one. The pairing
3440
+ `pickLocalized(setLocalized(map, lang, s), lang) === s` is pinned, because a
3441
+ write that lands where the read does not look is how a "saved" string disappears.
3442
+
3443
+ A full multi-locale editing UI remains out of scope (objectui#4163).
3444
+ - Updated dependencies [7b10bef]
3445
+ - Updated dependencies [97abedc]
3446
+ - Updated dependencies [b46c58f]
3447
+ - Updated dependencies [a507334]
3448
+ - Updated dependencies [ad694ac]
3449
+ - Updated dependencies [6f96fca]
3450
+ - Updated dependencies [afb2284]
3451
+ - Updated dependencies [3ac2de8]
3452
+ - Updated dependencies [0aacecc]
3453
+ - Updated dependencies [63f4f92]
3454
+ - Updated dependencies [777fca2]
3455
+ - Updated dependencies [c131d9e]
3456
+ - Updated dependencies [5f00ff4]
3457
+ - Updated dependencies [c9e073a]
3458
+ - Updated dependencies [0361d6b]
3459
+ - Updated dependencies [7b395d8]
3460
+ - Updated dependencies [0879812]
3461
+ - Updated dependencies [8cedb0d]
3462
+ - Updated dependencies [162621b]
3463
+ - Updated dependencies [4c6f549]
3464
+ - Updated dependencies [80c5412]
3465
+ - Updated dependencies [6cc910b]
3466
+ - Updated dependencies [061f5e8]
3467
+ - Updated dependencies [2dd4d3f]
3468
+ - Updated dependencies [e686f4d]
3469
+ - Updated dependencies [212c451]
3470
+ - Updated dependencies [a04b06d]
3471
+ - Updated dependencies [4ab4f1b]
3472
+ - Updated dependencies [b57107d]
3473
+ - Updated dependencies [e3ea4f9]
3474
+ - Updated dependencies [65f1e8d]
3475
+ - Updated dependencies [1f8ef0a]
3476
+ - Updated dependencies [bb5d4ee]
3477
+ - Updated dependencies [dc666f7]
3478
+ - Updated dependencies [3335767]
3479
+ - Updated dependencies [8b1f066]
3480
+ - Updated dependencies [af243c1]
3481
+ - Updated dependencies [cff4b77]
3482
+ - Updated dependencies [31938f0]
3483
+ - Updated dependencies [961ceaa]
3484
+ - Updated dependencies [f3f4e4c]
3485
+ - Updated dependencies [a05c350]
3486
+ - Updated dependencies [1dbb993]
3487
+ - Updated dependencies [2b5f509]
3488
+ - Updated dependencies [808f339]
3489
+ - Updated dependencies [6cf5999]
3490
+ - Updated dependencies [274e14a]
3491
+ - Updated dependencies [8c10f4f]
3492
+ - Updated dependencies [90dac98]
3493
+ - Updated dependencies [6096f20]
3494
+ - Updated dependencies [544aca2]
3495
+ - Updated dependencies [ea02938]
3496
+ - Updated dependencies [a14fb23]
3497
+ - Updated dependencies [ae98f1d]
3498
+ - Updated dependencies [f98eddf]
3499
+ - Updated dependencies [ce6bd99]
3500
+ - Updated dependencies [a5b08c9]
3501
+ - Updated dependencies [86982ac]
3502
+ - Updated dependencies [8c929e6]
3503
+ - Updated dependencies [cdefa2a]
3504
+ - Updated dependencies [2fc2a24]
3505
+ - Updated dependencies [0961d5e]
3506
+ - Updated dependencies [9d25b9b]
3507
+ - Updated dependencies [6276478]
3508
+ - Updated dependencies [5e67837]
3509
+ - Updated dependencies [ff14e29]
3510
+ - Updated dependencies [9b28151]
3511
+ - Updated dependencies [64563a9]
3512
+ - Updated dependencies [1a5003f]
3513
+ - Updated dependencies [09ab32b]
3514
+ - Updated dependencies [8acc51b]
3515
+ - Updated dependencies [ea9d17f]
3516
+ - Updated dependencies [45362a3]
3517
+ - Updated dependencies [2a943bf]
3518
+ - Updated dependencies [111fa4c]
3519
+ - Updated dependencies [93a689d]
3520
+ - Updated dependencies [4357a27]
3521
+ - Updated dependencies [e5f4343]
3522
+ - Updated dependencies [3261e64]
3523
+ - Updated dependencies [f5178a2]
3524
+ - Updated dependencies [2ad3671]
3525
+ - Updated dependencies [39bf246]
3526
+ - Updated dependencies [8740e86]
3527
+ - Updated dependencies [d22b37b]
3528
+ - Updated dependencies [8c0e550]
3529
+ - Updated dependencies [fde4caf]
3530
+ - Updated dependencies [caf0ed0]
3531
+ - Updated dependencies [1daf477]
3532
+ - Updated dependencies [3d614ea]
3533
+ - Updated dependencies [a7df45f]
3534
+ - Updated dependencies [6c2f3c5]
3535
+ - Updated dependencies [fb13e85]
3536
+ - Updated dependencies [c2d8659]
3537
+ - Updated dependencies [6516320]
3538
+ - Updated dependencies [98b1a7c]
3539
+ - Updated dependencies [e0f8202]
3540
+ - Updated dependencies [ac2d6f1]
3541
+ - Updated dependencies [9fbbb17]
3542
+ - Updated dependencies [c3a26cc]
3543
+ - Updated dependencies [a66e58e]
3544
+ - Updated dependencies [d89492c]
3545
+ - Updated dependencies [9a5f998]
3546
+ - Updated dependencies [1c5ee33]
3547
+ - Updated dependencies [9327397]
3548
+ - Updated dependencies [17cc3a3]
3549
+ - Updated dependencies [a9c5ea0]
3550
+ - Updated dependencies [02e6d36]
3551
+ - Updated dependencies [4758b33]
3552
+ - Updated dependencies [4758b33]
3553
+ - Updated dependencies [4758b33]
3554
+ - Updated dependencies [f905090]
3555
+ - Updated dependencies [3cd6c5e]
3556
+ - Updated dependencies [5bb855d]
3557
+ - Updated dependencies [9c78ebe]
3558
+ - Updated dependencies [12809a5]
3559
+ - Updated dependencies [9c78ebe]
3560
+ - Updated dependencies [12809a5]
3561
+ - Updated dependencies [7afc81d]
3562
+ - Updated dependencies [f9c06ef]
3563
+ - Updated dependencies [5ad3b88]
3564
+ - Updated dependencies [f9d772b]
3565
+ - Updated dependencies [bf14b64]
3566
+ - Updated dependencies [4345558]
3567
+ - Updated dependencies [97b6c21]
3568
+ - Updated dependencies [26ca2ad]
3569
+ - Updated dependencies [41ae65b]
3570
+ - Updated dependencies [baac95a]
3571
+ - Updated dependencies [13220af]
3572
+ - Updated dependencies [29b45f6]
3573
+ - Updated dependencies [39b8d51]
3574
+ - Updated dependencies [17b323e]
3575
+ - Updated dependencies [b956e69]
3576
+ - Updated dependencies [b32e7de]
3577
+ - Updated dependencies [ff94a12]
3578
+ - Updated dependencies [33e58d8]
3579
+ - Updated dependencies [256b4c9]
3580
+ - Updated dependencies [97672ba]
3581
+ - Updated dependencies [c5ec15c]
3582
+ - Updated dependencies [fec3b1a]
3583
+ - Updated dependencies [b8e0941]
3584
+ - Updated dependencies [0c50f18]
3585
+ - Updated dependencies [1dae95a]
3586
+ - Updated dependencies [e32dae1]
3587
+ - Updated dependencies [d0c0c7f]
3588
+ - Updated dependencies [30b11ad]
3589
+ - Updated dependencies [4aebea0]
3590
+ - Updated dependencies [f976774]
3591
+ - Updated dependencies [3d6badf]
3592
+ - Updated dependencies [25cb364]
3593
+ - Updated dependencies [a60539b]
3594
+ - Updated dependencies [de1b879]
3595
+ - Updated dependencies [c6678b1]
3596
+ - Updated dependencies [0638322]
3597
+ - Updated dependencies [e3782d2]
3598
+ - Updated dependencies [db0beb2]
3599
+ - Updated dependencies [997ce38]
3600
+ - Updated dependencies [ae0b9d3]
3601
+ - Updated dependencies [ad1785c]
3602
+ - Updated dependencies [3b469c8]
3603
+ - Updated dependencies [990a2d6]
3604
+ - Updated dependencies [990a2d6]
3605
+ - Updated dependencies [1c84036]
3606
+ - Updated dependencies [1c84036]
3607
+ - Updated dependencies [6650259]
3608
+ - Updated dependencies [4f8b7f8]
3609
+ - Updated dependencies [9e6619f]
3610
+ - Updated dependencies [f6ae5e2]
3611
+ - Updated dependencies [eb97ce6]
3612
+ - Updated dependencies [7343376]
3613
+ - Updated dependencies [b2683a2]
3614
+ - Updated dependencies [dded788]
3615
+ - Updated dependencies [b45d463]
3616
+ - Updated dependencies [54a7830]
3617
+ - Updated dependencies [f3135a4]
3618
+ - Updated dependencies [b5696d3]
3619
+ - Updated dependencies [3f9d926]
3620
+ - Updated dependencies [e978ed5]
3621
+ - Updated dependencies [6a7f24e]
3622
+ - Updated dependencies [b3c96d6]
3623
+ - Updated dependencies [8d0ca91]
3624
+ - Updated dependencies [c30c8dd]
3625
+ - Updated dependencies [244d516]
3626
+ - Updated dependencies [deca847]
3627
+ - Updated dependencies [ac15833]
3628
+ - Updated dependencies [ac15833]
3629
+ - Updated dependencies [e2dffc9]
3630
+ - Updated dependencies [328abeb]
3631
+ - Updated dependencies [dd0d78f]
3632
+ - Updated dependencies [24d3e65]
3633
+ - Updated dependencies [95a7c8d]
3634
+ - Updated dependencies [e227156]
3635
+ - Updated dependencies [cc4e476]
3636
+ - Updated dependencies [92970c4]
3637
+ - Updated dependencies [d570eaa]
3638
+ - Updated dependencies [4b742f4]
3639
+ - Updated dependencies [42687ba]
3640
+ - Updated dependencies [6cd8f66]
3641
+ - Updated dependencies [24a0f14]
3642
+ - Updated dependencies [797a30f]
3643
+ - Updated dependencies [b4075c0]
3644
+ - Updated dependencies [9b85600]
3645
+ - Updated dependencies [e327c89]
3646
+ - Updated dependencies [2eaf5be]
3647
+ - Updated dependencies [bf7ab35]
3648
+ - Updated dependencies [99878d8]
3649
+ - Updated dependencies [3c13675]
3650
+ - Updated dependencies [63ab761]
3651
+ - Updated dependencies [0eb9f36]
3652
+ - Updated dependencies [ae582b7]
3653
+ - Updated dependencies [51c2949]
3654
+ - Updated dependencies [a4b017e]
3655
+ - Updated dependencies [8732846]
3656
+ - Updated dependencies [559a2e2]
3657
+ - Updated dependencies [db11afd]
3658
+ - Updated dependencies [154075a]
3659
+ - Updated dependencies [582edef]
3660
+ - Updated dependencies [19f484f]
3661
+ - Updated dependencies [0a78a20]
3662
+ - Updated dependencies [615346d]
3663
+ - Updated dependencies [75dcc81]
3664
+ - Updated dependencies [55a12a8]
3665
+ - Updated dependencies [edfcf5a]
3666
+ - Updated dependencies [0a3e540]
3667
+ - Updated dependencies [c021b35]
3668
+ - Updated dependencies [f61dab1]
3669
+ - Updated dependencies [b0a05dd]
3670
+ - Updated dependencies [dd5ff19]
3671
+ - Updated dependencies [54997ff]
3672
+ - Updated dependencies [81f8498]
3673
+ - Updated dependencies [a782fa7]
3674
+ - Updated dependencies [1d6a23d]
3675
+ - Updated dependencies [0645133]
3676
+ - Updated dependencies [76e9df0]
3677
+ - Updated dependencies [0ecaa7d]
3678
+ - Updated dependencies [b24f93a]
3679
+ - Updated dependencies [6158e4c]
3680
+ - Updated dependencies [cff8641]
3681
+ - Updated dependencies [c27b575]
3682
+ - Updated dependencies [84b275c]
3683
+ - Updated dependencies [0e6e76b]
3684
+ - Updated dependencies [bf43afa]
3685
+ - Updated dependencies [385ebc5]
3686
+ - Updated dependencies [3a0e7ab]
3687
+ - Updated dependencies [858eafb]
3688
+ - Updated dependencies [a8c5509]
3689
+ - Updated dependencies [c2a8d23]
3690
+ - Updated dependencies [0ffc423]
3691
+ - Updated dependencies [3cc4fe5]
3692
+ - Updated dependencies [cd5b19a]
3693
+ - Updated dependencies [cd5b19a]
3694
+ - Updated dependencies [17dc167]
3695
+ - Updated dependencies [20d23be]
3696
+ - Updated dependencies [20d23be]
3697
+ - Updated dependencies [2e3da72]
3698
+ - Updated dependencies [1263e40]
3699
+ - Updated dependencies [e6bc087]
3700
+ - Updated dependencies [9419df1]
3701
+ - Updated dependencies [8bab157]
3702
+ - Updated dependencies [a7557a7]
3703
+ - Updated dependencies [7d074ba]
3704
+ - Updated dependencies [6158e4c]
3705
+ - Updated dependencies [6158e4c]
3706
+ - Updated dependencies [52aad5c]
3707
+ - Updated dependencies [18d1a0a]
3708
+ - Updated dependencies [7fed09d]
3709
+ - Updated dependencies [c1763e5]
3710
+ - Updated dependencies [58da8ae]
3711
+ - Updated dependencies [a8b9889]
3712
+ - Updated dependencies [138ad45]
3713
+ - Updated dependencies [138ad45]
3714
+ - Updated dependencies [5262f7d]
3715
+ - Updated dependencies [6aa029b]
3716
+ - Updated dependencies [2124d04]
3717
+ - Updated dependencies [be52115]
3718
+ - Updated dependencies [770cc5b]
3719
+ - Updated dependencies [1a88ce2]
3720
+ - Updated dependencies [a1a44d6]
3721
+ - Updated dependencies [e0a9c67]
3722
+ - Updated dependencies [5638529]
3723
+ - Updated dependencies [5638529]
3724
+ - Updated dependencies [d0fba91]
3725
+ - Updated dependencies [c476be0]
3726
+ - Updated dependencies [c82ff39]
3727
+ - Updated dependencies [6c3da53]
3728
+ - Updated dependencies [31987bd]
3729
+ - Updated dependencies [3c3ce15]
3730
+ - Updated dependencies [063119f]
3731
+ - Updated dependencies [e100589]
3732
+ - Updated dependencies [304f611]
3733
+ - Updated dependencies [e46ee77]
3734
+ - Updated dependencies [f9c8c4e]
3735
+ - Updated dependencies [6e9c8d2]
3736
+ - Updated dependencies [9547063]
3737
+ - Updated dependencies [3f6efd6]
3738
+ - Updated dependencies [52c95a1]
3739
+ - Updated dependencies [55d18c6]
3740
+ - Updated dependencies [c4ab6d0]
3741
+ - Updated dependencies [0e9058b]
3742
+ - Updated dependencies [c4ab6d0]
3743
+ - Updated dependencies [5988b6b]
3744
+ - Updated dependencies [6158e4c]
3745
+ - Updated dependencies [00ccdf7]
3746
+ - Updated dependencies [9d9ed54]
3747
+ - Updated dependencies [6007dd4]
3748
+ - Updated dependencies [4a1adb7]
3749
+ - Updated dependencies [4a1adb7]
3750
+ - Updated dependencies [4a1adb7]
3751
+ - Updated dependencies [185b7a0]
3752
+ - Updated dependencies [50c73fe]
3753
+ - Updated dependencies [ca3de72]
3754
+ - Updated dependencies [83e3f83]
3755
+ - Updated dependencies [401611b]
3756
+ - Updated dependencies [2c0ddf2]
3757
+ - Updated dependencies [4abc0aa]
3758
+ - Updated dependencies [f560ded]
3759
+ - Updated dependencies [2b188fa]
3760
+ - Updated dependencies [b654d4e]
3761
+ - Updated dependencies [f68e0a0]
3762
+ - Updated dependencies [aea682a]
3763
+ - Updated dependencies [fcdc8ec]
3764
+ - Updated dependencies [2d576e4]
3765
+ - Updated dependencies [8366acc]
3766
+ - Updated dependencies [95e58a3]
3767
+ - Updated dependencies [9d7419b]
3768
+ - Updated dependencies [fc7db05]
3769
+ - Updated dependencies [9ed8d0f]
3770
+ - Updated dependencies [c73cdb5]
3771
+ - Updated dependencies [6f5719e]
3772
+ - Updated dependencies [d0097af]
3773
+ - Updated dependencies [432882b]
3774
+ - Updated dependencies [64dae8e]
3775
+ - Updated dependencies [b06e374]
3776
+ - Updated dependencies [06a8af5]
3777
+ - Updated dependencies [6a91586]
3778
+ - Updated dependencies [a04d7c6]
3779
+ - Updated dependencies [5ccc500]
3780
+ - Updated dependencies [f3c2bb0]
3781
+ - Updated dependencies [978507b]
3782
+ - Updated dependencies [778138e]
3783
+ - Updated dependencies [9801765]
3784
+ - Updated dependencies [9cebfca]
3785
+ - Updated dependencies [460575f]
3786
+ - Updated dependencies [d796c8d]
3787
+ - Updated dependencies [594704f]
3788
+ - Updated dependencies [d3995fe]
3789
+ - Updated dependencies [1b1d772]
3790
+ - Updated dependencies [d88e20f]
3791
+ - Updated dependencies [2d7304d]
3792
+ - Updated dependencies [636b236]
3793
+ - Updated dependencies [4172589]
3794
+ - Updated dependencies [d6d8fb9]
3795
+ - Updated dependencies [64d624d]
3796
+ - Updated dependencies [053fdc8]
3797
+ - Updated dependencies [41b7ce3]
3798
+ - Updated dependencies [ae476b8]
3799
+ - Updated dependencies [39f4309]
3800
+ - Updated dependencies [95bad12]
3801
+ - Updated dependencies [d2fb6ef]
3802
+ - Updated dependencies [7cd3987]
3803
+ - Updated dependencies [ee3b878]
3804
+ - Updated dependencies [e304a4e]
3805
+ - Updated dependencies [fda49e5]
3806
+ - Updated dependencies [490d9a9]
3807
+ - Updated dependencies [fc62bb4]
3808
+ - Updated dependencies [41df893]
3809
+ - Updated dependencies [0cba1b7]
3810
+ - Updated dependencies [00f3eb5]
3811
+ - Updated dependencies [1ec291c]
3812
+ - Updated dependencies [453dbaa]
3813
+ - Updated dependencies [95f8704]
3814
+ - Updated dependencies [f8cdbf2]
3815
+ - Updated dependencies [69a2163]
3816
+ - Updated dependencies [24e027e]
3817
+ - Updated dependencies [2c3cd1b]
3818
+ - Updated dependencies [e176053]
3819
+ - Updated dependencies [e30ed15]
3820
+ - Updated dependencies [90665e0]
3821
+ - Updated dependencies [8d3a529]
3822
+ - Updated dependencies [5ac2e2c]
3823
+ - Updated dependencies [194fae1]
3824
+ - Updated dependencies [7e19d03]
3825
+ - Updated dependencies [b08b7eb]
3826
+ - Updated dependencies [1e946c9]
3827
+ - Updated dependencies [546ddf7]
3828
+ - Updated dependencies [864154e]
3829
+ - Updated dependencies [b023625]
3830
+ - Updated dependencies [75bd83d]
3831
+ - Updated dependencies [44d075b]
3832
+ - Updated dependencies [40c479a]
3833
+ - Updated dependencies [971d387]
3834
+ - Updated dependencies [ee851c3]
3835
+ - Updated dependencies [6414dfd]
3836
+ - Updated dependencies [a8d5c71]
3837
+ - Updated dependencies [905b21f]
3838
+ - Updated dependencies [88e9109]
3839
+ - Updated dependencies [2c45966]
3840
+ - Updated dependencies [db3a600]
3841
+ - Updated dependencies [3a3db76]
3842
+ - Updated dependencies [0d723a3]
3843
+ - Updated dependencies [0c95d3d]
3844
+ - Updated dependencies [3e4fa2c]
3845
+ - Updated dependencies [b5b928a]
3846
+ - Updated dependencies [6fd2cf7]
3847
+ - Updated dependencies [5fa06c4]
3848
+ - Updated dependencies [52a43de]
3849
+ - Updated dependencies [195052f]
3850
+ - Updated dependencies [e4559d1]
3851
+ - Updated dependencies [2c71482]
3852
+ - Updated dependencies [129bcc5]
3853
+ - Updated dependencies [a26b9e4]
3854
+ - Updated dependencies [5ef9c4f]
3855
+ - Updated dependencies [46f0bb4]
3856
+ - Updated dependencies [06b82b8]
3857
+ - Updated dependencies [8ec11e1]
3858
+ - Updated dependencies [6f81384]
3859
+ - Updated dependencies [22ba927]
3860
+ - Updated dependencies [8631c32]
3861
+ - Updated dependencies [f8c70f4]
3862
+ - Updated dependencies [5d3a2d1]
3863
+ - Updated dependencies [8f1d995]
3864
+ - Updated dependencies [b362c1b]
3865
+ - Updated dependencies [f9c34df]
3866
+ - Updated dependencies [dddb942]
3867
+ - Updated dependencies [00c665e]
3868
+ - Updated dependencies [29754cf]
3869
+ - Updated dependencies [d7de534]
3870
+ - Updated dependencies [3c2b6f7]
3871
+ - Updated dependencies [6e88630]
3872
+ - Updated dependencies [b84dc18]
3873
+ - Updated dependencies [ac8abb0]
3874
+ - Updated dependencies [9d86e1d]
3875
+ - Updated dependencies [3a5817f]
3876
+ - Updated dependencies [99a3c2d]
3877
+ - Updated dependencies [5961030]
3878
+ - Updated dependencies [f24de8b]
3879
+ - Updated dependencies [c8ea8af]
3880
+ - Updated dependencies [9602dc8]
3881
+ - Updated dependencies [3190414]
3882
+ - Updated dependencies [4e480f5]
3883
+ - Updated dependencies [38a123c]
3884
+ - Updated dependencies [299102e]
3885
+ - Updated dependencies [30c73cd]
3886
+ - Updated dependencies [830ed58]
3887
+ - Updated dependencies [d7acad6]
3888
+ - Updated dependencies [45a9aeb]
3889
+ - Updated dependencies [713db46]
3890
+ - Updated dependencies [c71e14d]
3891
+ - Updated dependencies [bf3a03c]
3892
+ - Updated dependencies [cb55718]
3893
+ - Updated dependencies [748494b]
3894
+ - Updated dependencies [5967be0]
3895
+ - Updated dependencies [831be72]
3896
+ - Updated dependencies [29cb85b]
3897
+ - Updated dependencies [3e028c8]
3898
+ - Updated dependencies [d0889e2]
3899
+ - Updated dependencies [ce503e5]
3900
+ - Updated dependencies [f20dcf0]
3901
+ - Updated dependencies [12402a9]
3902
+ - Updated dependencies [aff3d7a]
3903
+ - Updated dependencies [4ca30d0]
3904
+ - Updated dependencies [7a5da14]
3905
+ - Updated dependencies [fff9645]
3906
+ - Updated dependencies [9c3b7ce]
3907
+ - Updated dependencies [2c1c967]
3908
+ - Updated dependencies [9486ac6]
3909
+ - Updated dependencies [9486ac6]
3910
+ - Updated dependencies [4d5f9b4]
3911
+ - Updated dependencies [d6ceb8d]
3912
+ - Updated dependencies [dc4365c]
3913
+ - Updated dependencies [e321d52]
3914
+ - Updated dependencies [969ba84]
3915
+ - Updated dependencies [98188c2]
3916
+ - Updated dependencies [4c68077]
3917
+ - Updated dependencies [7977ff9]
3918
+ - Updated dependencies [3beef6d]
3919
+ - Updated dependencies [06b8c42]
3920
+ - Updated dependencies [46b9bc9]
3921
+ - Updated dependencies [f46bd39]
3922
+ - Updated dependencies [b98352a]
3923
+ - Updated dependencies [b76ca67]
3924
+ - Updated dependencies [45ac2cb]
3925
+ - Updated dependencies [b97790a]
3926
+ - Updated dependencies [dbd5194]
3927
+ - Updated dependencies [7c9b044]
3928
+ - Updated dependencies [e552c31]
3929
+ - Updated dependencies [d47de51]
3930
+ - Updated dependencies [3fe6463]
3931
+ - Updated dependencies [b392674]
3932
+ - Updated dependencies [4f3a1e2]
3933
+ - Updated dependencies [31ab372]
3934
+ - Updated dependencies [846889b]
3935
+ - Updated dependencies [2acd8e1]
3936
+ - Updated dependencies [7b90231]
3937
+ - Updated dependencies [26896c6]
3938
+ - Updated dependencies [67fc3b0]
3939
+ - Updated dependencies [8579e34]
3940
+ - Updated dependencies [d57db5d]
3941
+ - Updated dependencies [33a3b3c]
3942
+ - Updated dependencies [b87f15b]
3943
+ - Updated dependencies [045d20b]
3944
+ - Updated dependencies [a2d2515]
3945
+ - Updated dependencies [c18d099]
3946
+ - Updated dependencies [0caacca]
3947
+ - Updated dependencies [adb2a86]
3948
+ - Updated dependencies [03380aa]
3949
+ - Updated dependencies [4562ea5]
3950
+ - Updated dependencies [3619792]
3951
+ - Updated dependencies [3561bd2]
3952
+ - Updated dependencies [bf97b98]
3953
+ - Updated dependencies [320374d]
3954
+ - Updated dependencies [b0d308d]
3955
+ - Updated dependencies [b458300]
3956
+ - Updated dependencies [40f34b4]
3957
+ - Updated dependencies [bd0376d]
3958
+ - Updated dependencies [8063bcb]
3959
+ - Updated dependencies [b74a859]
3960
+ - Updated dependencies [d4493fd]
3961
+ - Updated dependencies [240b80f]
3962
+ - Updated dependencies [77cb489]
3963
+ - Updated dependencies [bfaa158]
3964
+ - Updated dependencies [777e5c6]
3965
+ - Updated dependencies [0c386dd]
3966
+ - Updated dependencies [39d69ad]
3967
+ - Updated dependencies [9e37d9b]
3968
+ - Updated dependencies [5ad86dd]
3969
+ - Updated dependencies [16a725f]
3970
+ - Updated dependencies [4dfdcc3]
3971
+ - Updated dependencies [6a449fc]
3972
+ - Updated dependencies [446d93d]
3973
+ - Updated dependencies [ecd9cb2]
3974
+ - Updated dependencies [f08bcd9]
3975
+ - Updated dependencies [98d4108]
3976
+ - Updated dependencies [0e3b3be]
3977
+ - Updated dependencies [a29ae2d]
3978
+ - Updated dependencies [220c18d]
3979
+ - Updated dependencies [00d3f09]
3980
+ - Updated dependencies [4388f71]
3981
+ - Updated dependencies [0b1ac58]
3982
+ - Updated dependencies [c93b4d5]
3983
+ - Updated dependencies [c1fe272]
3984
+ - Updated dependencies [3cab570]
3985
+ - Updated dependencies [8ad218d]
3986
+ - Updated dependencies [3e41187]
3987
+ - Updated dependencies [5f78953]
3988
+ - Updated dependencies [639114c]
3989
+ - Updated dependencies [639114c]
3990
+ - Updated dependencies [1490691]
3991
+ - Updated dependencies [e8e4c4d]
3992
+ - Updated dependencies [1f31d3a]
3993
+ - Updated dependencies [d1842ab]
3994
+ - Updated dependencies [78ca238]
3995
+ - Updated dependencies [d8ec8d6]
3996
+ - Updated dependencies [351eb31]
3997
+ - Updated dependencies [866cd1d]
3998
+ - Updated dependencies [20c04b2]
3999
+ - Updated dependencies [01c9023]
4000
+ - Updated dependencies [48c19bd]
4001
+ - Updated dependencies [a6d8b8d]
4002
+ - Updated dependencies [4b5bb95]
4003
+ - Updated dependencies [b652514]
4004
+ - Updated dependencies [adbda1b]
4005
+ - Updated dependencies [adbda1b]
4006
+ - Updated dependencies [8952395]
4007
+ - Updated dependencies [e2b3826]
4008
+ - Updated dependencies [0348bc9]
4009
+ - Updated dependencies [e8c553b]
4010
+ - Updated dependencies [2e32ed4]
4011
+ - Updated dependencies [554e647]
4012
+ - Updated dependencies [3ed3eec]
4013
+ - Updated dependencies [7c3df8f]
4014
+ - Updated dependencies [a4514e8]
4015
+ - Updated dependencies [db3896c]
4016
+ - Updated dependencies [b9f5ff1]
4017
+ - Updated dependencies [e75f4c9]
4018
+ - Updated dependencies [19f1639]
4019
+ - Updated dependencies [4704aa4]
4020
+ - Updated dependencies [47547d0]
4021
+ - Updated dependencies [1bee5d0]
4022
+ - Updated dependencies [858cd72]
4023
+ - Updated dependencies [cfc9b6d]
4024
+ - Updated dependencies [554f2b6]
4025
+ - Updated dependencies [72f55c9]
4026
+ - Updated dependencies [26e06d7]
4027
+ - Updated dependencies [669d71b]
4028
+ - Updated dependencies [ed27d7c]
4029
+ - Updated dependencies [52c8cf7]
4030
+ - Updated dependencies [2ceb43a]
4031
+ - Updated dependencies [7cdd2b9]
4032
+ - Updated dependencies [52c8cf7]
4033
+ - Updated dependencies [3399704]
4034
+ - Updated dependencies [71a4a53]
4035
+ - Updated dependencies [7bf244b]
4036
+ - Updated dependencies [f0bb9fa]
4037
+ - Updated dependencies [81a2eb1]
4038
+ - Updated dependencies [caa0cd3]
4039
+ - Updated dependencies [caa0cd3]
4040
+ - Updated dependencies [20cb8db]
4041
+ - Updated dependencies [25c7d58]
4042
+ - Updated dependencies [00d2fa6]
4043
+ - Updated dependencies [77b2a18]
4044
+ - Updated dependencies [c6198c2]
4045
+ - Updated dependencies [721d1e0]
4046
+ - Updated dependencies [c2f0f48]
4047
+ - Updated dependencies [1237ae4]
4048
+ - Updated dependencies [2f61238]
4049
+ - Updated dependencies [51eb515]
4050
+ - Updated dependencies [c354ce5]
4051
+ - Updated dependencies [8fe8e5c]
4052
+ - Updated dependencies [feac439]
4053
+ - Updated dependencies [9ae871d]
4054
+ - Updated dependencies [efbd566]
4055
+ - Updated dependencies [2a5bf45]
4056
+ - Updated dependencies [9587fc9]
4057
+ - Updated dependencies [e62c44e]
4058
+ - Updated dependencies [daf9d57]
4059
+ - Updated dependencies [23b9958]
4060
+ - Updated dependencies [c15d7ec]
4061
+ - Updated dependencies [5d0876c]
4062
+ - Updated dependencies [f7ace0a]
4063
+ - Updated dependencies [b041b9c]
4064
+ - Updated dependencies [ce2aaef]
4065
+ - Updated dependencies [544ecba]
4066
+ - Updated dependencies [2ce2612]
4067
+ - Updated dependencies [bc640ec]
4068
+ - Updated dependencies [1e215c4]
4069
+ - Updated dependencies [da6e191]
4070
+ - Updated dependencies [3e377c9]
4071
+ - Updated dependencies [a3eb5d0]
4072
+ - Updated dependencies [4ce14f1]
4073
+ - Updated dependencies [aef97e5]
4074
+ - Updated dependencies [2af1fa7]
4075
+ - Updated dependencies [c14d3a0]
4076
+ - Updated dependencies [a137d0c]
4077
+ - Updated dependencies [caf477f]
4078
+ - Updated dependencies [f6375da]
4079
+ - Updated dependencies [967e5d8]
4080
+ - Updated dependencies [c907a9c]
4081
+ - Updated dependencies [a4611b3]
4082
+ - Updated dependencies [20316ba]
4083
+ - Updated dependencies [d3499b3]
4084
+ - Updated dependencies [91f9276]
4085
+ - Updated dependencies [309c75e]
4086
+ - Updated dependencies [c9f9bae]
4087
+ - Updated dependencies [18897a4]
4088
+ - Updated dependencies [8b7ea39]
4089
+ - Updated dependencies [a915064]
4090
+ - Updated dependencies [dcbf0b2]
4091
+ - Updated dependencies [52cac38]
4092
+ - Updated dependencies [93fea2e]
4093
+ - Updated dependencies [1422a92]
4094
+ - Updated dependencies [d05fe17]
4095
+ - Updated dependencies [a480f79]
4096
+ - Updated dependencies [f08d1a8]
4097
+ - Updated dependencies [64a252d]
4098
+ - Updated dependencies [786bc91]
4099
+ - Updated dependencies [75fca96]
4100
+ - Updated dependencies [7ca6ddd]
4101
+ - Updated dependencies [f1cd290]
4102
+ - Updated dependencies [5a41ce7]
4103
+ - Updated dependencies [8d50bc2]
4104
+ - Updated dependencies [604476d]
4105
+ - Updated dependencies [d1bebb0]
4106
+ - Updated dependencies [95bf128]
4107
+ - Updated dependencies [335abea]
4108
+ - Updated dependencies [edea22a]
4109
+ - Updated dependencies [0f5cadf]
4110
+ - Updated dependencies [4f9f1ee]
4111
+ - Updated dependencies [66e8b2a]
4112
+ - Updated dependencies [aa083cd]
4113
+ - Updated dependencies [12b5992]
4114
+ - Updated dependencies [b93e245]
4115
+ - Updated dependencies [c842594]
4116
+ - Updated dependencies [290de37]
4117
+ - Updated dependencies [e1c27e4]
4118
+ - Updated dependencies [8c8da45]
4119
+ - Updated dependencies [8cd8eb5]
4120
+ - Updated dependencies [f52a9d7]
4121
+ - Updated dependencies [cf1d29e]
4122
+ - Updated dependencies [1bd79c8]
4123
+ - Updated dependencies [af9e957]
4124
+ - Updated dependencies [b1030c7]
4125
+ - Updated dependencies [c974edf]
4126
+ - Updated dependencies [ad852b6]
4127
+ - Updated dependencies [6778809]
4128
+ - Updated dependencies [3a3dd28]
4129
+ - Updated dependencies [7fb22a1]
4130
+ - Updated dependencies [ad66d79]
4131
+ - Updated dependencies [0758bd8]
4132
+ - Updated dependencies [ee4d19f]
4133
+ - Updated dependencies [496d31d]
4134
+ - Updated dependencies [7ed9808]
4135
+ - Updated dependencies [0ea7054]
4136
+ - Updated dependencies [ac0e39a]
4137
+ - Updated dependencies [9a853f2]
4138
+ - Updated dependencies [cb847fd]
4139
+ - Updated dependencies [ee70287]
4140
+ - Updated dependencies [3e98e13]
4141
+ - Updated dependencies [a695f50]
4142
+ - Updated dependencies [fc32921]
4143
+ - Updated dependencies [4eaa835]
4144
+ - Updated dependencies [8f9d87a]
4145
+ - Updated dependencies [b1777ae]
4146
+ - Updated dependencies [24845c4]
4147
+ - Updated dependencies [6f864cf]
4148
+ - Updated dependencies [24d1edd]
4149
+ - Updated dependencies [645087c]
4150
+ - Updated dependencies [33f4a19]
4151
+ - Updated dependencies [6e9a3d4]
4152
+ - Updated dependencies [4a292d2]
4153
+ - Updated dependencies [5323168]
4154
+ - Updated dependencies [841dd2b]
4155
+ - Updated dependencies [3014fc0]
4156
+ - Updated dependencies [dacb402]
4157
+ - Updated dependencies [846cec0]
4158
+ - Updated dependencies [91facae]
4159
+ - Updated dependencies [b38014e]
4160
+ - Updated dependencies [474797d]
4161
+ - Updated dependencies [704e695]
4162
+ - Updated dependencies [a407bd6]
4163
+ - Updated dependencies [317dbce]
4164
+ - Updated dependencies [309728c]
4165
+ - Updated dependencies [c03d03b]
4166
+ - Updated dependencies [aa08d7e]
4167
+ - Updated dependencies [3a43a15]
4168
+ - Updated dependencies [868e825]
4169
+ - Updated dependencies [f76f436]
4170
+ - Updated dependencies [ce45a03]
4171
+ - Updated dependencies [421544b]
4172
+ - Updated dependencies [fb01022]
4173
+ - Updated dependencies [e9d9212]
4174
+ - Updated dependencies [ecfb693]
4175
+ - Updated dependencies [639ca9d]
4176
+ - Updated dependencies [2152962]
4177
+ - Updated dependencies [abc1b18]
4178
+ - Updated dependencies [81a51db]
4179
+ - Updated dependencies [67749c7]
4180
+ - Updated dependencies [507b61b]
4181
+ - Updated dependencies [512c84b]
4182
+ - Updated dependencies [c300267]
4183
+ - Updated dependencies [fb3a101]
4184
+ - Updated dependencies [d4733f2]
4185
+ - Updated dependencies [7c9145f]
4186
+ - Updated dependencies [1570eac]
4187
+ - Updated dependencies [f391ede]
4188
+ - Updated dependencies [f5cfbbd]
4189
+ - Updated dependencies [f5cfbbd]
4190
+ - Updated dependencies [8b532cb]
4191
+ - Updated dependencies [64c3cdd]
4192
+ - Updated dependencies [4d65991]
4193
+ - Updated dependencies [c42554e]
4194
+ - Updated dependencies [3e71b26]
4195
+ - Updated dependencies [b89583b]
4196
+ - Updated dependencies [70c4523]
4197
+ - Updated dependencies [555b4ec]
4198
+ - Updated dependencies [64f1cf1]
4199
+ - Updated dependencies [da5e4f6]
4200
+ - Updated dependencies [1ccfc23]
4201
+ - Updated dependencies [542718f]
4202
+ - Updated dependencies [7f27bc5]
4203
+ - Updated dependencies [0a174f3]
4204
+ - Updated dependencies [676f677]
4205
+ - Updated dependencies [f95b140]
4206
+ - Updated dependencies [541ce4e]
4207
+ - Updated dependencies [6479086]
4208
+ - Updated dependencies [d79f525]
4209
+ - Updated dependencies [d1865d2]
4210
+ - Updated dependencies [88a629d]
4211
+ - Updated dependencies [cdd2542]
4212
+ - Updated dependencies [ab77513]
4213
+ - Updated dependencies [55ba3ff]
4214
+ - Updated dependencies [3ecc369]
4215
+ - Updated dependencies [f1190b0]
4216
+ - Updated dependencies [561abef]
4217
+ - Updated dependencies [ef52001]
4218
+ - Updated dependencies [6a4680b]
4219
+ - Updated dependencies [c3a4273]
4220
+ - Updated dependencies [abf710d]
4221
+ - Updated dependencies [093af32]
4222
+ - Updated dependencies [1bd1be7]
4223
+ - Updated dependencies [d234fa9]
4224
+ - Updated dependencies [adf5812]
4225
+ - Updated dependencies [0eaed83]
4226
+ - Updated dependencies [e36acd4]
4227
+ - Updated dependencies [5058336]
4228
+ - Updated dependencies [2f6b2bf]
4229
+ - Updated dependencies [2028b31]
4230
+ - Updated dependencies [63601ab]
4231
+ - Updated dependencies [c372b29]
4232
+ - Updated dependencies [152f0a7]
4233
+ - Updated dependencies [8693b85]
4234
+ - Updated dependencies [e82dad1]
4235
+ - Updated dependencies [58b7b3d]
4236
+ - Updated dependencies [84defab]
4237
+ - Updated dependencies [681d3f1]
4238
+ - Updated dependencies [969d4f2]
4239
+ - Updated dependencies [f3bc481]
4240
+ - Updated dependencies [b79aac2]
4241
+ - Updated dependencies [93fc0e7]
4242
+ - Updated dependencies [a4b723f]
4243
+ - Updated dependencies [2b10ca0]
4244
+ - Updated dependencies [7db4a81]
4245
+ - Updated dependencies [19a0b0e]
4246
+ - Updated dependencies [526fc11]
4247
+ - Updated dependencies [f8e3e9a]
4248
+ - Updated dependencies [b9d47ec]
4249
+ - Updated dependencies [3b6bc69]
4250
+ - Updated dependencies [6214db6]
4251
+ - Updated dependencies [6732df4]
4252
+ - Updated dependencies [fe9e0d0]
4253
+ - Updated dependencies [63fb72c]
4254
+ - Updated dependencies [804831c]
4255
+ - Updated dependencies [279e48e]
4256
+ - Updated dependencies [8700d6d]
4257
+ - Updated dependencies [8db2a0f]
4258
+ - Updated dependencies [689953a]
4259
+ - Updated dependencies [30443fb]
4260
+ - Updated dependencies [8d3dbb2]
4261
+ - Updated dependencies [efc1c9c]
4262
+ - Updated dependencies [da45e6b]
4263
+ - Updated dependencies [7533465]
4264
+ - Updated dependencies [835f0f3]
4265
+ - Updated dependencies [6d5db7b]
4266
+ - Updated dependencies [a9d97be]
4267
+ - Updated dependencies [ed35b44]
4268
+ - Updated dependencies [9ba7e9c]
4269
+ - Updated dependencies [729e851]
4270
+ - Updated dependencies [96919a4]
4271
+ - Updated dependencies [345e24a]
4272
+ - Updated dependencies [20b507a]
4273
+ - Updated dependencies [2e471dc]
4274
+ - Updated dependencies [6748587]
4275
+ - Updated dependencies [be50942]
4276
+ - Updated dependencies [775e079]
4277
+ - Updated dependencies [7e8b3c0]
4278
+ - Updated dependencies [53374dc]
4279
+ - Updated dependencies [f6fb83f]
4280
+ - Updated dependencies [2049b03]
4281
+ - Updated dependencies [2bf34f7]
4282
+ - Updated dependencies [15b33ae]
4283
+ - Updated dependencies [7cbc724]
4284
+ - Updated dependencies [4a94c38]
4285
+ - Updated dependencies [7098eed]
4286
+ - Updated dependencies [3df7c5c]
4287
+ - Updated dependencies [fb91ac9]
4288
+ - Updated dependencies [fb91ac9]
4289
+ - Updated dependencies [641fb55]
4290
+ - Updated dependencies [8524372]
4291
+ - Updated dependencies [7cbefa5]
4292
+ - Updated dependencies [72d6587]
4293
+ - Updated dependencies [a272a4f]
4294
+ - Updated dependencies [55f39ee]
4295
+ - Updated dependencies [0ce32d5]
4296
+ - Updated dependencies [0970a0e]
4297
+ - Updated dependencies [e427e9c]
4298
+ - Updated dependencies [bbc9dc3]
4299
+ - Updated dependencies [02f1813]
4300
+ - Updated dependencies [1ef89c0]
4301
+ - Updated dependencies [ba0b61a]
4302
+ - Updated dependencies [ba0b61a]
4303
+ - Updated dependencies [ac716ff]
4304
+ - Updated dependencies [e05553c]
4305
+ - Updated dependencies [f0f3cd5]
4306
+ - Updated dependencies [ab856ed]
4307
+ - Updated dependencies [f0f2046]
4308
+ - Updated dependencies [20f3e65]
4309
+ - Updated dependencies [bbba098]
4310
+ - Updated dependencies [87af769]
4311
+ - Updated dependencies [3be720e]
4312
+ - Updated dependencies [43c0d17]
4313
+ - Updated dependencies [c3df43a]
4314
+ - Updated dependencies [d16d0e9]
4315
+ - Updated dependencies [bbe57fd]
4316
+ - Updated dependencies [272a530]
4317
+ - Updated dependencies [1779e8d]
4318
+ - Updated dependencies [9aa2a57]
4319
+ - Updated dependencies [f7fcc2c]
4320
+ - Updated dependencies [f0f4d6c]
4321
+ - Updated dependencies [4128188]
4322
+ - Updated dependencies [b253c4e]
4323
+ - Updated dependencies [78a9c67]
4324
+ - Updated dependencies [2e97c8c]
4325
+ - Updated dependencies [4a7ef0d]
4326
+ - Updated dependencies [4598f6d]
4327
+ - Updated dependencies [dea17b4]
4328
+ - Updated dependencies [89bb77a]
4329
+ - Updated dependencies [06611e4]
4330
+ - Updated dependencies [44152c4]
4331
+ - Updated dependencies [3939545]
4332
+ - Updated dependencies [66abbde]
4333
+ - Updated dependencies [dc3893d]
4334
+ - Updated dependencies [1bbaa16]
4335
+ - Updated dependencies [6ee259a]
4336
+ - Updated dependencies [7649f43]
4337
+ - Updated dependencies [e708426]
4338
+ - Updated dependencies [3b6d53b]
4339
+ - Updated dependencies [e22fa12]
4340
+ - Updated dependencies [1560d46]
4341
+ - Updated dependencies [276d174]
4342
+ - Updated dependencies [2982ed9]
4343
+ - Updated dependencies [741864f]
4344
+ - Updated dependencies [a8198de]
4345
+ - Updated dependencies [0c789a4]
4346
+ - Updated dependencies [05a49f2]
4347
+ - Updated dependencies [b234a84]
4348
+ - Updated dependencies [a78cd37]
4349
+ - Updated dependencies [5ea623e]
4350
+ - Updated dependencies [4d7d322]
4351
+ - Updated dependencies [5eabe86]
4352
+ - Updated dependencies [ca5d671]
4353
+ - Updated dependencies [32bf2d6]
4354
+ - Updated dependencies [ff0c384]
4355
+ - Updated dependencies [af4fb29]
4356
+ - Updated dependencies [c698a81]
4357
+ - Updated dependencies [ff5ef1c]
4358
+ - Updated dependencies [befd40c]
4359
+ - Updated dependencies [9a97800]
4360
+ - Updated dependencies [6bca0e4]
4361
+ - Updated dependencies [81c0bc4]
4362
+ - Updated dependencies [3c76801]
4363
+ - Updated dependencies [60500cb]
4364
+ - Updated dependencies [2fcefb9]
4365
+ - Updated dependencies [77f846a]
4366
+ - Updated dependencies [bc5870c]
4367
+ - Updated dependencies [b55a346]
4368
+ - Updated dependencies [065bba7]
4369
+ - Updated dependencies [f760064]
4370
+ - Updated dependencies [dd19463]
4371
+ - Updated dependencies [6791717]
4372
+ - Updated dependencies [8ea3bee]
4373
+ - Updated dependencies [100547e]
4374
+ - Updated dependencies [3a58149]
4375
+ - Updated dependencies [6d1c155]
4376
+ - Updated dependencies [d7573b3]
4377
+ - Updated dependencies [bf3edfe]
4378
+ - Updated dependencies [2c8474c]
4379
+ - Updated dependencies [6ce89da]
4380
+ - Updated dependencies [0e05aac]
4381
+ - Updated dependencies [ae61ad4]
4382
+ - Updated dependencies [5aed9e4]
4383
+ - Updated dependencies [83c77dc]
4384
+ - Updated dependencies [3c9fca3]
4385
+ - Updated dependencies [18a8e7d]
4386
+ - Updated dependencies [e7957ab]
4387
+ - Updated dependencies [f7e34ca]
4388
+ - Updated dependencies [e719ebd]
4389
+ - Updated dependencies [516583b]
4390
+ - Updated dependencies [f9e4f91]
4391
+ - Updated dependencies [6ef48b1]
4392
+ - Updated dependencies [58be55e]
4393
+ - Updated dependencies [fa429cf]
4394
+ - Updated dependencies [ed8df3e]
4395
+ - Updated dependencies [fe76ece]
4396
+ - Updated dependencies [8b446f5]
4397
+ - Updated dependencies [8e74b27]
4398
+ - Updated dependencies [7102b20]
4399
+ - Updated dependencies [8ebd57f]
4400
+ - Updated dependencies [968dc1e]
4401
+ - Updated dependencies [9a1fb41]
4402
+ - Updated dependencies [617707a]
4403
+ - Updated dependencies [c40f3b8]
4404
+ - Updated dependencies [58770f3]
4405
+ - Updated dependencies [aefe428]
4406
+ - Updated dependencies [485f096]
4407
+ - Updated dependencies [7357447]
4408
+ - Updated dependencies [199d31b]
4409
+ - Updated dependencies [b655a9d]
4410
+ - Updated dependencies [a865c73]
4411
+ - Updated dependencies [3e01cb5]
4412
+ - Updated dependencies [7138bc1]
4413
+ - Updated dependencies [cef27e2]
4414
+ - Updated dependencies [4e8622b]
4415
+ - Updated dependencies [dffd752]
4416
+ - Updated dependencies [06973aa]
4417
+ - Updated dependencies [50798f3]
4418
+ - Updated dependencies [6a576c9]
4419
+ - Updated dependencies [105f3c5]
4420
+ - Updated dependencies [3ccd9e8]
4421
+ - Updated dependencies [689b979]
4422
+ - Updated dependencies [c70f865]
4423
+ - Updated dependencies [e546222]
4424
+ - Updated dependencies [fd13f52]
4425
+ - Updated dependencies [d7bd274]
4426
+ - Updated dependencies [98c3a74]
4427
+ - Updated dependencies [fffa30d]
4428
+ - Updated dependencies [e4e9557]
4429
+ - Updated dependencies [7a28e1e]
4430
+ - Updated dependencies [ebce5a3]
4431
+ - Updated dependencies [fb336df]
4432
+ - Updated dependencies [4dc80d0]
4433
+ - Updated dependencies [9d9040d]
4434
+ - Updated dependencies [20e317c]
4435
+ - Updated dependencies [0fce2ef]
4436
+ - Updated dependencies [42df928]
4437
+ - Updated dependencies [0e2ddd4]
4438
+ - Updated dependencies [b7479ab]
4439
+ - Updated dependencies [9850c6e]
4440
+ - Updated dependencies [de570cc]
4441
+ - Updated dependencies [b2ea297]
4442
+ - Updated dependencies [5b5a5c3]
4443
+ - Updated dependencies [14582b8]
4444
+ - Updated dependencies [51e144e]
4445
+ - Updated dependencies [19cbf10]
4446
+ - Updated dependencies [b6e83be]
4447
+ - Updated dependencies [ab92940]
4448
+ - Updated dependencies [a691c0b]
4449
+ - Updated dependencies [0b1326d]
4450
+ - Updated dependencies [1e66879]
4451
+ - Updated dependencies [c5200f0]
4452
+ - Updated dependencies [af3861f]
4453
+ - Updated dependencies [2609812]
4454
+ - Updated dependencies [515f171]
4455
+ - Updated dependencies [1f4e029]
4456
+ - Updated dependencies [4f14ad7]
4457
+ - Updated dependencies [258d264]
4458
+ - Updated dependencies [cac64b3]
4459
+ - Updated dependencies [4bb940b]
4460
+ - Updated dependencies [8033ad1]
4461
+ - Updated dependencies [fa140b8]
4462
+ - Updated dependencies [71cba28]
4463
+ - Updated dependencies [190fbd0]
4464
+ - Updated dependencies [c00bf28]
4465
+ - Updated dependencies [93127bd]
4466
+ - Updated dependencies [f2158ec]
4467
+ - Updated dependencies [759606e]
4468
+ - Updated dependencies [fd8dace]
4469
+ - Updated dependencies [72ffc34]
4470
+ - Updated dependencies [a51fa0c]
4471
+ - Updated dependencies [51f3d8d]
4472
+ - Updated dependencies [bf28341]
4473
+ - Updated dependencies [78cbdb5]
4474
+ - Updated dependencies [b7543a9]
4475
+ - Updated dependencies [6c6cee7]
4476
+ - Updated dependencies [42887e0]
4477
+ - Updated dependencies [f1690d4]
4478
+ - Updated dependencies [83fe6e7]
4479
+ - Updated dependencies [d1ab06f]
4480
+ - Updated dependencies [38a9568]
4481
+ - Updated dependencies [f90b8fb]
4482
+ - Updated dependencies [91783c4]
4483
+ - Updated dependencies [982885d]
4484
+ - Updated dependencies [dba7d84]
4485
+ - Updated dependencies [ca39427]
4486
+ - Updated dependencies [bd09957]
4487
+ - Updated dependencies [5a07e67]
4488
+ - Updated dependencies [2d36552]
4489
+ - Updated dependencies [45d8288]
4490
+ - Updated dependencies [b2437a7]
4491
+ - Updated dependencies [f157423]
4492
+ - Updated dependencies [7a90afd]
4493
+ - Updated dependencies [eddc1dd]
4494
+ - Updated dependencies [490f482]
4495
+ - Updated dependencies [27308c5]
4496
+ - Updated dependencies [8689166]
4497
+ - Updated dependencies [c9327c9]
4498
+ - Updated dependencies [920165d]
4499
+ - Updated dependencies [9101be5]
4500
+ - Updated dependencies [f53a8d0]
4501
+ - Updated dependencies [30266cf]
4502
+ - Updated dependencies [968dc1e]
4503
+ - Updated dependencies [57f9b07]
4504
+ - Updated dependencies [3c73d99]
4505
+ - Updated dependencies [d91aed9]
4506
+ - Updated dependencies [ed71d9e]
4507
+ - Updated dependencies [7776fc2]
4508
+ - Updated dependencies [e76634c]
4509
+ - Updated dependencies [c86185e]
4510
+ - Updated dependencies [fb96ecb]
4511
+ - Updated dependencies [1170ed1]
4512
+ - Updated dependencies [92814db]
4513
+ - Updated dependencies [4d73b07]
4514
+ - @object-ui/react@17.7.0
4515
+ - @object-ui/core@17.7.0
4516
+ - @object-ui/types@17.7.0
4517
+ - @object-ui/i18n@17.7.0
4518
+ - @object-ui/components@17.7.0
4519
+ - @object-ui/fields@17.7.0
4520
+ - @object-ui/permissions@17.7.0
4521
+
3
4522
  ## 17.6.0
4
4523
 
5
4524
  ### Minor Changes