@object-ui/plugin-list 17.5.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,4555 @@
1
1
  # @object-ui/plugin-list
2
2
 
3
+ ## 17.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 57a2bc2: Retire the undeclared timeline `metaFields` reads in both packages (objectui#10222).
8
+
9
+ The spec's `TimelineConfigSchema` is a strict object that declares no `metaFields` member and
10
+ refuses one, yet `ObjectTimeline` read it (through an `as any` cast) to pick the chip fields
11
+ beside each title, and `ListView` read it three times: two field collectors put the listed
12
+ fields into `$expand` / `$select`, and the status / priority auto-projection was skipped when
13
+ the list was present. All four reads are deleted.
14
+
15
+ **Behaviour change:** a stored interface-page view carrying `options.timeline.metaFields` falls
16
+ back to the default card labels (the built-in `status` / `priority` chips, limited to the fields
17
+ the object declares), and the fields that list named are no longer fetched for it. No such view
18
+ has been found; the stored rows are not censused. A timeline without the key renders and fetches
19
+ exactly what it did before, because `ListView` now projects `status` / `priority` for every
20
+ timeline view.
21
+
22
+ `cardFields` stays the reserved spelling if authored timeline chip fields are ever asked for;
23
+ nothing is declared.
24
+ - ae98f1d: A `chart` list view bound to a semantic `dataset` takes its scope from the dataset:
25
+ a view filter on it is now refused at authoring, and its toolbar no longer offers
26
+ filter controls (objectui#10327).
27
+
28
+ The dataset shape of the list view's chart builds a node with no filter: the chart
29
+ queries the dataset through `queryDataset` with its dimensions and measures, and a
30
+ dataset's fields need not be the list object's. So a view filter written on such a
31
+ view was accepted and silently dropped, and the chart drew unfiltered totals that
32
+ read as filtered ones. The toolbar Filter builder and the filter chips reached only
33
+ the list's own fetch of the object's rows, never the chart.
34
+
35
+ **New refusal (`@object-ui/types`).** `ListViewSchema` refuses `filter` — and the
36
+ legacy `filters` alias — on a view with `viewType: 'chart'` whose chart block (`chart`,
37
+ or the legacy `options.chart` bag when no `chart` is declared) names a `dataset`. The
38
+ issue is a `custom` one at the written key, and its message ends with the remedy: a
39
+ dataset chart's scope is written in the dataset. An empty filter array, a chart bound
40
+ to the list object, and a view of another kind that only offers a switch to a dataset
41
+ chart are all still accepted.
42
+
43
+ **Toolbar (`@object-ui/plugin-list`).** While a dataset-bound chart is on screen, the
44
+ Filter builder and the `userFilters` chips are not rendered. A chart bound to the list
45
+ object keeps both, because its node carries the effective filter.
46
+
47
+ What an author sees change: a dataset chart view that declared a `filter` now fails
48
+ validation with the remedy above instead of drawing unfiltered numbers. Put the scope
49
+ in the dataset. No example, app or doc in this repository declared such a view; the
50
+ search is on the pull request.
51
+ - 974760a: fix(plugin-list): a map list view requests the fields its markers are drawn from
52
+
53
+ `ListView` builds its `$select` projection from the authored columns plus each
54
+ view kind's per-row bindings. Kanban, calendar, gallery, timeline and gantt each
55
+ had an arm there; the map had none, and the candidate keys named none of its
56
+ bindings. A map view whose columns omit the location field therefore asked for
57
+ `id` plus those columns, and on a backend that honours `$select` every row
58
+ arrived without coordinates: a map with no markers, and no error.
59
+
60
+ The request shape changes: a map view now also requests the fields `ObjectMap`
61
+ reads off each row — `locationField`, or the `latitudeField` + `longitudeField`
62
+ pair, plus `titleField` and `descriptionField`. They are read through the same
63
+ resolver the map render branch and the view switcher already use (the spec's
64
+ view-level `map` block merged per key over the `options.map` bag), so a bag value
65
+ the view-level block shadows is not requested. A coordinate or title binding that
66
+ names a lookup is also expanded, as the map's own fetch already does; the
67
+ description binding is requested but not expanded, because the marker popup
68
+ renders it as text and cannot draw an expanded record there.
69
+
70
+ Every one of these fields passes the same checks as the other view bindings: a
71
+ field the current user may not read is never requested, and a name the object
72
+ does not declare is never sent.
73
+ - d0c0c7f: The list view's live query sends "Is empty" / "Is not empty" as the spec's `isempty` / `isnotempty` instead of an equality to `null` (objectui#10813). `@objectstack/spec` 17.6.0 and later lower that pair to `$empty`; an earlier reader lowers it to `$null`, as it does the saved view's `is_empty`.
74
+
75
+ `convertFilterGroupToAST` resolved the pair to `[FIELD, '=', null]` / `[FIELD, '!=', null]`, a null-only test, before `mapOperator` was consulted. The same rule saved into the view is persisted as `is_empty`, which `@objectstack/spec` 17.6.0 lowers to `$empty` (objectstack#20446): so one filter panel answered two record sets, depending on whether the view had been saved. The pair now takes the value-less path like `is_null`, and `mapOperator` gains `isempty` / `isnotempty` arms. The node is `[FIELD, 'isempty', null]`; the spec discards the third slot.
76
+
77
+ **What moves.** Nothing is stored by this path. Against a 17.6.0 or later server, on a text-like column a row holding `''` is now "empty", and on a multi-value column a row holding `[]` is, matching the server's per-type answer for the saved view. On a `provider: 'value'` list a row with no key at all is now "empty" too: the equality to `null` did not select it there.
78
+ - 244d516: A grouped grid over a data source that declares no `queryGroupHeaders` now
79
+ **refuses grouping** instead of grouping a page of rows (objectui#10881,
80
+ maintainer ruling F). Grouping is a property of the query (ruling A on
81
+ objectui#7189): a data source that cannot answer the group header query cannot
82
+ group, so the grid names the member it is missing rather than drawing counts it
83
+ cannot know.
84
+
85
+ - **`@object-ui/plugin-grid`** — a grouped `object-grid` that fetches its own
86
+ rows over such a source renders an error panel naming `queryGroupHeaders`
87
+ and issues no row query. It used to fetch one window and group it in the
88
+ browser, so every group count was a page slice and a group whose rows all
89
+ fell past the window was missing. Rows handed in WHOLE — a
90
+ `data: { provider: 'value', items }` block, or a `data` prop from a parent
91
+ view that declares no larger `rowCount` — are still grouped in the browser,
92
+ exactly. A host that hands rows in while declaring them one page of more
93
+ (the external-pagination props: `manualPagination`, `onPageChange` and a
94
+ `rowCount` above the rows it handed) is now refused as well, with a sentence
95
+ of its own: grouping needs every record. It used to have that page grouped as
96
+ if it were whole.
97
+ - **`@object-ui/plugin-list`** — `ListView` makes the same refusal, with the
98
+ same sentence, in place of a grouped grid over such a source, and fetches no
99
+ window for it. It used to fetch one window and hand it to the grid as
100
+ `data`, which the grid grouped as if the rows were whole. Rows handed to
101
+ `ListView` in whole are still handed to the grid.
102
+ - **`@object-ui/i18n`** — two new keys, `grid.grouping.needsHeaderQuery` and
103
+ `grid.grouping.needsWholeRows`, in all ten locale packs.
104
+
105
+ The `Partial` marker described by the pending
106
+ `7189-grouped-grid-partial-disclosure` entry is retired everywhere before any
107
+ release carried it: its three `grid.grouping.partial*` strings, `GroupRow`'s
108
+ `partialLabel` / `partialTitle` props and the notice above the group list are
109
+ gone.
110
+
111
+ **Breaking semantics (declared `minor` per this repo's version policy):** a
112
+ grouped grid that fetches its own rows over a data source without
113
+ `queryGroupHeaders` — `ApiDataSource`, `ValueDataSource`, or a host adapter —
114
+ grouped the page it fetched in the last release, and now renders an error panel
115
+ instead. So does a grouped grid view in `ListView` over such a source. So does
116
+ a grouped grid handed rows by a host that declares them one page of more
117
+ (`manualPagination`, `onPageChange` and a `rowCount` above the rows handed),
118
+ whatever its data source. To keep grouping, either:
119
+
120
+ - implement `queryGroupHeaders` on the data source (the ObjectStack adapter
121
+ does), and let the grid fetch its own rows, or
122
+ - hand the rows in whole (`data: { provider: 'value', items }`, or a `data`
123
+ prop with no `rowCount` above it).
124
+
125
+ Not changed here: while a toolbar search is active, `ListView` over a data
126
+ source that answers the header query still hands a grouped grid its window,
127
+ whose group counts are the window's, because the header query carries no
128
+ search (objectstack#20358).
129
+ - e176053: Consolidate the seven lucide icon-name resolvers into one seam (objectui#5935).
130
+
131
+ Seven modules resolved authored icon names into lucide's runtime `icons` record, each
132
+ with its own copy of the logic: **three different tokenisers** (`split('-')` on five of
133
+ them, `split(/[-_\s]/)` on one, `split(/[-_\s]+/)` on one) and the `Home` -> `House`
134
+ rename on only **four** of the seven. The same authored name therefore rendered on one
135
+ surface and not another — the sidebar-vs-action-bar disagreement objectui#5633 opened
136
+ with. There is now one resolver, `resolveIcon`, exported from `@object-ui/components`,
137
+ and the other six call it.
138
+
139
+ **The tokeniser is `split(/[-_\s]+/)` with `Home` -> `House` applied universally, and it
140
+ was measured rather than chosen.** Its regression set is empty three independent ways:
141
+ against the authored population, against a maximally-pessimistic every-authored-name x
142
+ every-surface cross-product, and against a bound-free differential over 8,298 spellings
143
+ derived from all 1,767 live record keys — each with a discrimination control that fired
144
+ in the same run. `split('-')` was **not** adoptable: it regresses 4,748 name-surface
145
+ pairs in that last reading, stripping two surfaces of every snake_case and
146
+ space-separated spelling they resolve today.
147
+
148
+ **What changes for you — all of it widening, none of it removal.** No name that resolved
149
+ before stops resolving: no key of lucide's record contains `_`, whitespace or `-`
150
+ (measured: 0 of 1,767), so whenever the old narrow tokeniser produced a live key the
151
+ wider one produces the same key. Sixteen name-surface pairs start resolving where they
152
+ rendered a fallback or nothing before:
153
+
154
+ - `layout_dashboard` and `building_2` (and every other snake_case or space-separated
155
+ spelling) now resolve on the shared resolver, `ui:icon`, `ListView`'s empty state,
156
+ `TabBar` and `ViewSwitcher` — they previously resolved only on the action preview and
157
+ the related list.
158
+ - `home` / `Home` now resolves on `RelatedList`, `ListView` and `TabBar`, which carried
159
+ no rename map. `Home` is not a live record key, so this could only ever be a widening.
160
+
161
+ **What does NOT change: what each surface draws when a name does not resolve.** The seam
162
+ answers `name -> component`, returning `null`, and decides nothing else (maintainer
163
+ ruling 2026-09-03 on objectui#5935). Every call site keeps its own fallback, visibly, at
164
+ the call site: `ui:icon` keeps its `SquareDashed` placeholder and its warning
165
+ (objectui#5631, untouched), `RelatedList` and `ListView` keep their `Inbox` glyph,
166
+ `ActionPreview` keeps its three-character name chip, and the shared resolver, `TabBar`
167
+ and `ViewSwitcher` keep `null`. A two-valued `onUnresolvable` parameter was ruled on and
168
+ then dropped once the tree was measured to have four such behaviours rather than two: a
169
+ lookup function is the wrong place to publish a presentation decision.
170
+
171
+ `resolveIcon` is newly exported from `@object-ui/components`, which is the only surface
172
+ this adds. `scripts/check-lucide-icon-record-names.mjs` is simplified in the same change:
173
+ its census goes from seven sites to one, and its normalisation stops being a
174
+ widest-common approximation of three disagreeing resolvers — so the under-reporting that
175
+ gate disclosed at objectui#5932 is closed rather than merely bounded.
176
+ - 4ca30d0: Two widget prop types anchor their `schema` to exported schema types that extend
177
+ `BaseSchema`, instead of hand-rolled inline literals with no `BaseSchema` in
178
+ their ancestry (objectui#6576, maintainer ruling 2026-08-31 option A; folds
179
+ objectui#6914).
180
+
181
+ - `@object-ui/types` exports `ObjectGallerySchema` (`type: 'object-gallery'`)
182
+ and `ObjectDataTableSchema` (`type: 'object-data-table'`), each `extends
183
+ BaseSchema`, beside the other `Object*Schema` declarations, with zod mirrors
184
+ of the same names under `@object-ui/types/zod`. `ObjectDataTableSchema`
185
+ declares the two keys the widget was reading behind casts — `drillDown`
186
+ (`DrillDownConfig`, at this change) and `onRowClick` — which no declaration
187
+ carried before.
188
+
189
+ ⚠️ **Dated note, 2026-09-25 — `drillDown` now takes the table's own shape —
190
+ objectui#10685.** `ObjectDataTableSchema.drillDown` is
191
+ `ObjectDataTableDrillDownConfig`, on both faces: `filter`, `maxRows` and
192
+ `report` are refused by name, and `target` is `'drawer'` or `'dialog'`. None of
193
+ the four was ever read by this block. The rest of this entry is kept as the
194
+ reading of this change.
195
+ - `@object-ui/plugin-list`: the published `ObjectGalleryProps.schema` is
196
+ `ObjectGallerySchema`. Its accept set WIDENS — every `BaseSchema` member is
197
+ writable (`visibleWhen`, a real base member, was a compile error on the
198
+ literal) — and NARROWS in one place: `type` is now required and pinned to
199
+ `'object-gallery'`. `data` stays `Record<string, unknown>[]`.
200
+ - `@object-ui/plugin-dashboard`: `ObjectDataTableProps.schema` (not exported
201
+ from the plugin index) is `ObjectDataTableSchema`. The literal's own
202
+ `[key: string]: any` is gone, so a wrong-typed base member (`visible: 42`) and
203
+ a wrong-shaped `drillDown` are refused, and `type` is pinned to
204
+ `'object-data-table'` instead of bare `string`.
205
+
206
+ Unchanged on both, stated plainly: an UNKNOWN key still compiles, because
207
+ `BaseSchema`'s index signature is inherited (objectui#5155, open). No runtime
208
+ behaviour changes; the widgets render exactly as before.
209
+ - 045d20b: Relationship-target readers resolve a lookup's target from `reference` alone,
210
+ dropping the `reference_to` fallback arm (objectui#6837, half 2).
211
+
212
+ Maintainer ruling, 2026-08-31, 原文照录: 「objectui不是前端的项目吗?后端的元数据只要
213
+ 对,前端按协议执行就行了呀」. Protocol normalization belongs on the SERVER; the front
214
+ end just executes the protocol. objectstack#13847 landed the server half — a
215
+ `field-reference-to-alias` conversion rewrites stored `reference_to` to
216
+ `reference` on the serve path and in `os migrate meta`.
217
+
218
+ `reference` is the only target spelling `@objectstack/spec`'s `FieldSchema`
219
+ declares. Measured on the installed 17.2.0: it refuses `reference_to`,
220
+ `referenceTo` and `target` with `unrecognized_keys`, each carrying its own
221
+ "Did you mean -> `reference`?" rename, while a nonsense key gets the same
222
+ refusal with NO rename hint and `reference` parses clean.
223
+
224
+ ## ⚠️ BREAKING for a hand-written schema that spells `reference_to` — read this
225
+
226
+ **This is a behaviour change for BYO consumers, and it is being stated rather
227
+ than shipped silently.** ObjectUI is usable without an ObjectStack backend
228
+ (`examples/byo-backend-console`), and a hand-written TypeScript schema passes
229
+ through no zod door, so nothing rejects the legacy spelling at authoring time.
230
+
231
+ **The break surface is narrower than "all BYO consumers", and this is the
232
+ measurement rather than a blanket claim.** Two ingestion choke points stamp both
233
+ snake_case keys from whichever spelling arrived — `MetadataProvider`'s type
234
+ cache for metadata type `object`, and `ObjectStackAdapter.getObjectSchema`. Any
235
+ def that passed either one already carries `reference` and is **completely
236
+ unaffected**. What is affected is exactly:
237
+
238
+ - **A `DataSource` implementation other than `ObjectStackAdapter`.**
239
+ `getObjectSchema` is a required member of the published `DataSource`
240
+ interface, and the readers call it on the generic `dataSource` (through
241
+ `useSettledSchema` and directly), so a host adapter's object schema reaches
242
+ them raw. Every in-repo example of one is on this path:
243
+ `ApiDataSource`, `ValueDataSource`, `packages/types/examples/rest-data-source.ts`,
244
+ `examples/byo-backend-console/src/mockDataSource.ts`,
245
+ `packages/runner/src/lib/mockDataSource.ts`,
246
+ `apps/site/app/components/galleryDataSource.ts`,
247
+ `apps/console/src/sdui-workbench-preview.tsx`,
248
+ `packages/plugin-grid/demo/bulk-actions.tsx`.
249
+
250
+ **Measured on this tree, none of those eight emits a relationship target at all** —
251
+ `reference_to` and `reference` are both zero in each, and
252
+ `examples/byo-backend-console` carries no lookup or master_detail field
253
+ anywhere (its only `reference` hits are a vite triple-slash directive and a
254
+ tsconfig `references` array). The single in-repo producer that WAS on this
255
+ surface, `packages/plugin-gantt/demo/main.tsx`, is fixed here at the producer.
256
+
257
+ ⇒ **If you author object metadata by hand and spell a lookup's target
258
+ `reference_to`, rename that key to `reference`.** Symptom if you do not: the
259
+ target silently fails to resolve, and the affected surface degrades rather than
260
+ erroring — a related list is not derived, a gantt quick filter falls back to the
261
+ distinct values in the loaded rows instead of the referenced object's full
262
+ domain, a tree stops auto-detecting its parent pointer, a lookup cell shows a
263
+ raw id, a chart's group-by labels stay unresolved.
264
+
265
+ The ingestion choke point now emits a **dev-mode warning** when a def arrives
266
+ carrying only `reference_to` or `referenceTo` and no `reference`. It names the
267
+ object, the field and the offending key, and points at this ruling. Stamping is
268
+ deliberately unchanged, so nothing that worked stops working. It is memoised
269
+ once per **(object name, field name, spelling, target value)** — every segment
270
+ of that key is pinned, in both directions, in
271
+ `reference-keys.legacyWarning-6837.test.ts`.
272
+
273
+ ⛔ **This warning does NOT cover the break described above, and it is worth being
274
+ exact about that rather than letting it read as mitigation.** It lives in
275
+ `normalizeFieldReferenceKeys`, reachable only through
276
+ `normalizeSchemaReferenceKeys`, which has exactly two production call sites —
277
+ `MetadataProvider` (metadata type `object`) and
278
+ `ObjectStackAdapter.getObjectSchema`. Both of those also STAMP the def, so the
279
+ warning fires precisely where the def still resolves and nothing is broken. A
280
+ hand-written schema served through any OTHER `DataSource` — the break surface —
281
+ reaches a reader raw: it never passes through this code and produces **no
282
+ warning at all**. On that path the failure is exactly as silent as before.
283
+ A reader-side or shared-resolver diagnostic, which would cover it, remains open
284
+ on objectui#6837.
285
+
286
+ ⚠️ **Dated note, 2026-09-29 — the call-site count above has since moved —
287
+ objectui#7650.** "exactly two production call sites" above held when this change landed
288
+ (`045d20ba8`). Later in this same release objectui#7650 (PR objectui#8868) added a call
289
+ on `MetadataProvider`'s by-name object-schema serve path. Re-measured for objectui#10979
290
+ on `main` at `2eaf5be27`, `normalizeSchemaReferenceKeys` had three production call sites:
291
+ two in `MetadataProvider`, on its list path and on its by-name path, and one in
292
+ `ObjectStackAdapter.getObjectSchema`.
293
+ `.changeset/7650-metadata-item-reference-canonicalisation.md` (PR objectui#8868) states
294
+ what ships; the text above is kept as the reading of this change.
295
+
296
+ ## What did NOT change
297
+
298
+ **Every key these readers EMIT is byte-identical**, and that was verified
299
+ mechanically over the whole diff rather than asserted. Eleven of the sixteen
300
+ sites write a target onto a bag whose own contract spells it `reference_to` (or
301
+ camelCase `referenceTo`): the six whose read and write share a line —
302
+ `RecordDetailDrawer`, `RelatedList`, `buildDefaultPageSchema`, `ListView`,
303
+ `FilterConditionField`, `resolveActionParams` — plus five more that read on one
304
+ line and emit on another, and so are just as much emitters: `RecordDetailView`,
305
+ `RecordMetaFooter`, `ObjectGallery`, `fieldEnrichment` (all `reference_to`) and
306
+ `UserFilters` (`referenceTo`). Only the right-hand read narrowed anywhere; the
307
+ emitted key is what its target contract declares, and renaming it would be a
308
+ separate change.
309
+
310
+ **Three readers were deliberately left alone.** `LookupCellRenderer`
311
+ (`fields/src/index.tsx`), `LookupField` and `UserField` read `FieldMetadata` —
312
+ ObjectUI's OWN contract, whose `LookupFieldMetadata` declares `reference_to` and
313
+ never declares `reference`. They are fed by the emitters above and by published
314
+ example schemas (`examples/schema-catalog/src/schemas/fields-lookup/*.json`), so
315
+ narrowing them would break in-repo producers, and `plugin-grid`'s
316
+ `relationalMetaCopySet.derivation.test.ts` re-derives its read set from exactly
317
+ those three sources — where `reference_to` is recorded with verdict
318
+ `adapter-stamped`. `DetailViewFieldSchema` is likewise untouched.
319
+
320
+ ⚠️ **Dated note, 2026-09-30 — the stamp, the three readers and the emitted key have since moved — objectui#11070.** Later in this same release objectui#11070 (round 4) retired `reference_to` inside ObjectUI as well. `normalizeFieldReferenceKeys` now FOLDS a legacy `reference_to` / `referenceTo` onto `reference` and no longer stamps `reference_to`, so "Stamping is deliberately unchanged" and "stamp both snake_case keys" above no longer describe what ships. `LookupCellRenderer`, `LookupField` and `UserField` read `reference` only; `LookupFieldMetadata`, `MasterDetailFieldMetadata` and `DetailViewField` (with `DetailViewFieldSchema`) declare `reference`; and the emitters listed under "What did NOT change" write `reference`. A hand-written schema served through a `DataSource` other than `ObjectStackAdapter` that spells only `reference_to` therefore also loses the lookup picker's target and the read cell's name. `.changeset/11070-reference-to-round4.md` states what ships; the text above is kept as the reading of this change.
321
+ - bd0376d: Read `count` / `value` answers as the contract declares them at six more seams
322
+ (objectui#6917, following objectui#5945 / #6726 / #6840 / #6839).
323
+
324
+ **One precedence inversion, repaired without deleting the arm.**
325
+ `@object-ui/fields`' lookup chip resolved fetch-on-demand rows with
326
+ `result?.value || result?.data || []` — `value` AHEAD of `data`, the one rows
327
+ member `QueryResult` (`@object-ui/types`) declares. A producer emitting both was
328
+ resolved to the undeclared key. It now reads through `@object-ui/core`'s
329
+ `extractRecords`, whose accepted set is identical (bare array, `data`, `value`)
330
+ and whose order is the contract's. The `value` arm is **kept**: its own producer
331
+ census measured eight live `find()` doubles emitting `{ value: [...] }` at this
332
+ seam (3 plugin-kanban, 3 plugin-calendar, 2 plugin-grid), so deleting it would
333
+ break them. Only the RANK was wrong.
334
+
335
+ **Five dead arms deleted, each on its own measured zero.** Every module got its
336
+ own census with the control sitting on the producer→consumer join, because
337
+ objectui#6840's zero is seam-local and is not transferable — the same sweep read
338
+ 0 producers for `value` at one seam and 5 at another in a single pass.
339
+
340
+ - `count` at the `DataSource.find()` seam — `plugin-detail`'s reference rail and
341
+ `plugin-list`'s ListView. 0 of 592 `find()` producers emit `count`; controls
342
+ `data` (312) and `total` (150) lit on the same pass. Both adapters'
343
+ `normalizeQueryResult` already fold `count` into `total` below every consumer.
344
+ - `value` at the `client.meta.getItems()` seam — `app-shell`'s help menu and the
345
+ console's Public Forms and Flow Runs pages. **These three do not sit on the
346
+ `DataSource.find()` seam at all**, so they were measured on their own join: 0
347
+ of 28 `meta.getItems` producers emit `value`; control `items` (18) lit. The
348
+ canonical readers of that envelope (`MetadataProvider.extractItems`,
349
+ `MetadataService.getItems`) have never had a `value` arm either.
350
+
351
+ No producer changes behaviour, because at these five sites there is no producer;
352
+ what changes is that a non-conforming one is refused rather than silently
353
+ absorbed (AGENTS.md #0.1).
354
+
355
+ `QueryResult` is **not** widened to bless `count` or `value` — a published-type
356
+ change and the maintainer's call, the floor objectui#6726, #6840 and #6839 all
357
+ held. A producer that really speaks either belongs behind an adapter that folds
358
+ it, which is what both adapters already do.
359
+
360
+ One refusal pin per module, each keeping the live arms green beside the deleted
361
+ one, and — where an inversion actually existed — a case feeding both members with
362
+ different contents, the only input that can tell the two orders apart.
363
+
364
+ Also repaired: two `plugin-grid` test doubles answered `{ value: [],
365
+ '@odata.count': 0 }` while `ObjectGrid` reads `result.data` / `result.total`.
366
+ Inert only while the arrays were empty; the first row put in one would have been
367
+ silently dropped. Test-only.
368
+ - 2a7ac32: Calendar views no longer render on invented field names (objectui#7029; ruled on
369
+ objectstack#13748, director batch #19, option A).
370
+
371
+ A view that carried no `calendar:` block used to have a complete-looking calendar
372
+ configuration synthesized for it. `ObjectCalendar` has always decided whether it
373
+ has a usable configuration by asking whether a start-date binding is PRESENT, so
374
+ the fabrication short-circuited its own refusal screen — "Calendar configuration
375
+ required" — which existed all
376
+ along and was simply unreachable. Measured on a leave-request object whose real
377
+ fields are `start_date` / `end_date`: every record piled onto today's cell under
378
+ titles resolved through the display-name chain. A plausible, fully wrong screen,
379
+ with zero signal to the author.
380
+
381
+ Three faces were fabricating, on two independent routes to the same renderer:
382
+
383
+ - `app-shell/ObjectView` emitted `startDateField: 'due_date'` and
384
+ `titleField: 'name'` into `options.calendar` for every object view;
385
+ - `plugin-list/ListView`'s calendar branch floored the same two bindings at
386
+ `'start_date'` / `'end_date'` one layer down;
387
+ - `plugin-view/ObjectView.generateViewSchema` — the authored `object-view`
388
+ element route, which bypasses `ListView` entirely — carried its own copy.
389
+
390
+ All three now forward only what the author declared. This converges the calendar
391
+ on the shape its siblings already had: `timelineViewOptions` (objectui#3129
392
+ retired this very literal from the timeline axis), the kanban lane detector
393
+ (ADR-0085, "never invents a field the object doesn't have"), and
394
+ `defaultCalendarFromObject` (a binding, or nothing).
395
+
396
+ **Behaviour change, loud over silent.** With no binding to forward, ADR-0047's
397
+ capability gate stops offering the Calendar toggle to views that configured
398
+ none, and a view forced onto the calendar renderer reaches the refusal screen
399
+ instead of a wrong one. A view that happened to sit on an object carrying a real
400
+ `due_date` field was rendering by luck; it now refuses until its `calendar:`
401
+ block is written. Correctly configured calendars are unaffected — same fields,
402
+ same render. The same deletion also stops the fabricated name from answering for
403
+ the Timeline switcher, which accepts a calendar binding as a legitimate axis.
404
+
405
+ The spec half — cross-field validation rejecting a half-written declaration at
406
+ authoring time — is objectstack#13817. This half makes the runtime honest
407
+ independent of which spec version the host pins.
408
+ - 5f4514f: Gantt views no longer render on invented date field names.
409
+
410
+ The half PR #7062 fenced out and reported separately. A view that carried no
411
+ `gantt:` block used to have a complete-looking date axis synthesized for it:
412
+ all three faces floored `startDateField` at `'start_date'` and `endDateField`
413
+ at `'end_date'` — field names no view had written and most objects do not
414
+ carry.
415
+
416
+ `ObjectGantt.getGanttConfig` takes its flat branch as soon as BOTH date props
417
+ are present, so the fabricated pair short-circuited the renderer's own refusal
418
+ screen — "Gantt configuration required. Please specify startDateField,
419
+ endDateField, and titleField." — which existed all along and was simply
420
+ unreachable from every route. The same fabrication answered ADR-0047's
421
+ capability gate in `ListView.availableViews`, so the Gantt toggle was live on
422
+ every object view in the product.
423
+
424
+ ⚠️ The premise was MEASURED before anything was deleted, because #7029's
425
+ mechanic is only correct where a refusal path exists and that had never been
426
+ established for this renderer: on the unmodified tree, `ObjectGantt` REFUSES an
427
+ absent binding — it does not render empty, and it does not throw.
428
+
429
+ Three faces were fabricating, on two independent routes to the same renderer:
430
+
431
+ - `app-shell/src/views/ObjectView.tsx` — the console object page. The inline
432
+ branch becomes `ganttViewOptions`, the sibling of `calendarViewOptions` and
433
+ `timelineViewOptions`: the declared block spread whole, title floored at
434
+ `'name'`, no date field invented.
435
+ - `plugin-list/src/ListView.tsx` — the render branch AND the capability gate.
436
+ - `plugin-view/src/ObjectView.tsx` — `generateViewSchema`, the authored
437
+ `object-view` element route, which bypasses `ListView` entirely.
438
+
439
+ **What changes for an author.** A view that declared no gantt configuration is
440
+ no longer offered the Gantt toggle, and one forced onto the renderer reaches
441
+ the refusal screen instead of a plausible, fully wrong chart. A view that
442
+ declared a binding is unaffected — the declared block is forwarded exactly as
443
+ before, every spec key included.
444
+
445
+ Also corrected: the objectui#3129 note at the top of `app-shell/ObjectView.tsx`
446
+ certified the gantt branch below it as already using the safe two-rung shape.
447
+ It did not. The note now states each sibling branch as measured, and says
448
+ explicitly which fabrication REMAINS — the timeline `'created_at'` floor at the
449
+ two plugin faces, which is routed to a ruling rather than settled
450
+ per-face.
451
+
452
+ Deliberately out of scope, and left in place: `progressField` / `dependenciesField`
453
+ (not date axes, different absent-value semantics) and the timeline `'created_at'`
454
+ posture conflict.
455
+ - 04a67b9: Retire the `'created_at'` timeline date-axis floors at both plugin faces
456
+ (step ③ of the maintainer ruling 2026-09-01, 总监批 #28).
457
+
458
+ **Breaking, deliberately.** A timeline view that declares **no** date axis anywhere no
459
+ longer renders. `ListView`'s and `ObjectView`'s timeline branches used to hand
460
+ `ObjectTimeline` a `startDateField` of `'created_at'` for such a view; both now forward
461
+ a declared axis or no key at all, and the renderer shows its "declare a date axis"
462
+ refusal instead.
463
+
464
+ House posture, entered with the ruling: **日期轴永不虚构** — a date axis is never
465
+ fabricated. This is the third and last step of a sequence the ruling ordered and forbade
466
+ reordering: `ObjectTimeline` gained the refusal screen and lost its own internal
467
+ `|| 'date'` floor first (objectui#7459), which by its own measurement changed nothing a
468
+ user could see — precisely because these two faces still supplied a name. They are the
469
+ supply.
470
+
471
+ The floor was not a harmless default. `'created_at'` is a column nearly every object
472
+ carries, so downstream it was indistinguishable from a real binding and could never
473
+ resolve to nothing — while the `$select` projection is collected from the **declared**
474
+ `timeline` / `options.timeline` blocks and never from this prop. An undeclared view was
475
+ therefore given a timeline bound to a column the query had not requested, and every
476
+ record bucketed into "No date": a screen that looks built, is wrong, and gives the
477
+ author no signal. The ruling also explicitly replaced the written decision that stood on
478
+ the deleted `ListView` line ("`created_at` stays the last resort for a view that
479
+ declares no date axis anywhere") — it was a second, de-facto contract held at one face,
480
+ on the very literal objectui#3129 had retired at the app-shell face.
481
+
482
+ **Migration.** Declare the axis on the view: `timeline.startDateField` (spec-canonical),
483
+ `timeline.dateField` (legacy alias), or a `calendar.startDateField` — objectui#3129
484
+ established that a calendar binding is a legitimate timeline axis, and it still is. All
485
+ three keep rendering exactly as before; only the *undeclared* case changes. A view that
486
+ really did want records laid out by creation time says so in one key:
487
+ `timeline: { startDateField: 'created_at' }`. The refusal names the accepted keys on
488
+ screen, so an affected view reports its own fix.
489
+
490
+ `titleField` is unaffected and keeps its `'name'` floor at both faces — it is not a date
491
+ axis. So do gantt's `progressField` / `dependenciesField`, which the ruling scoped out
492
+ for separate evaluation.
493
+ - 78ca238: `DataErrorState` accepts the icon props `DataEmptyState` already had, and `ListView`'s
494
+ load-failure panel is now rendered by the error state instead of the empty state
495
+ (objectui#7143; maintainer ruling 2026-09-01, director decision batch #27).
496
+
497
+ `ListView` rendered its load FAILURE through `DataEmptyState` — the component named for
498
+ the *empty* case — passing it a destructive icon, error copy and a retry action, while
499
+ `DataErrorState`, in the same file and with the same layout, had no consumer anywhere in
500
+ the repo. objectui#7132 closed the accessibility half of that collision (the panel now
501
+ declares `role="alert"` over the empty state's `role="status"` default) and deliberately
502
+ left the structural half alone: `DataErrorState` hardcoded its icon, so the swap was a
503
+ props-surface question plus a visual change rather than a rename.
504
+
505
+ **`@object-ui/components` — three additive optional props on `DataErrorState`**, mirrored
506
+ from `DataEmptyState` in the same file rather than spelled a second way:
507
+
508
+ - `icon?: React.ReactNode` — rendered above the title; falls back to the `AlertCircle`
509
+ glyph the component has always drawn.
510
+ - `showIcon?: boolean` (default `true`) — `false` omits the icon container entirely.
511
+ - `iconWrapperClassName?: string` — REPLACES the wrapper's default class rather than
512
+ merging with it, so `""` renders the icon raw. `DataEmptyState` resolves it with `??`
513
+ against its own default and this does the same, against
514
+ `flex size-10 items-center justify-center rounded-lg bg-destructive/10` — the destructive
515
+ square `DataErrorState` already drew.
516
+
517
+ Same names, same types, same default semantics as the empty state's; nothing existing on
518
+ `DataErrorState` changed, and a call site that passes none of the three renders exactly
519
+ what it rendered before. `illustration` and `action` were deliberately NOT mirrored — the
520
+ ruling pins three props, and this component's retry affordance is already spelled
521
+ `onRetry` / `retryLabel` (plus `children` for a call site that needs its own control).
522
+
523
+ One non-prop addition rides along, called out rather than folded in: the icon wrapper now
524
+ carries `data-slot="data-error-state-icon"`, mirroring the empty state's
525
+ `data-empty-state-icon`. Without it the wrapper `iconWrapperClassName` governs has no
526
+ name — untestable and unstylable — and migrating a call site off `DataEmptyState` would
527
+ DROP that identifier rather than rename it.
528
+
529
+ **`@object-ui/plugin-list` — the panel changes component identity, not pixels.** The call
530
+ site passes the same custom icon through the new `icon` prop, the same
531
+ `iconWrapperClassName="mb-3"`, the same title, and the same copy through `message` (the
532
+ error state's spelling of `description`); its retry `<Button>` moves from `action` to
533
+ `children`, which renders at the identical position. `role="alert"`, the
534
+ `data-testid="list-error-state"` hook and `data-error-kind` are untouched. The whole
535
+ rendered delta is two attributes:
536
+
537
+ - the panel root's `data-slot` becomes `data-error-state` (was `data-empty-state`);
538
+ - the icon wrapper's becomes `data-error-state-icon` (was `data-empty-state-icon`).
539
+
540
+ Both are renames, not removals. Nothing in this repo styles or selects on either — no CSS
541
+ rule and no test read them — so a stylesheet in a host app targeting
542
+ `[data-slot="data-empty-state"]` to reach *this* panel is the only way to notice, and it
543
+ should be reading `data-error-state` now. Every class on every node, and the glyphs
544
+ themselves, are byte-identical: this is a visual no-op, deliberately, so the review the
545
+ ruling asks for has a small thing to look at rather than a redesign.
546
+ - e2b3826: Grid grouping is now **server-side** (objectui#7189, maintainer ruling A): the
547
+ set of groups and every number in a group header — the count and any per-group
548
+ aggregation — come from the query, and the rows inside a group are paged by the
549
+ server.
550
+
551
+ Before, a grouped grid fetched one window and bucketed it in the browser, so
552
+ both the group set and every header number were properties of the page. On a
553
+ store of 186 records over five business units sized 86 / 61 / 31 / 7 / 1 with a
554
+ 100-row page it rendered **two** headers (86, 14) when the rows were stored
555
+ contiguously and five page slices (31 / 31 / 30 / 7 / 1) when interleaved — and
556
+ the records past the window were not on a second page, they were unreachable.
557
+ The same store now renders five headers reading 86 / 61 / 31 / 7 / 1 in either
558
+ order, and every record is reachable through its group's own pager.
559
+
560
+ - **`@object-ui/types`** — `DataSource` gains an optional
561
+ `queryGroupHeaders(resource, query)`: it answers the group header query
562
+ `@objectstack/spec/ui`'s `compileListViewGroupQuery` compiles (an
563
+ `EngineAggregateOptions`) with one header row per group. Presence is the
564
+ capability; a source that cannot answer it leaves it undeclared.
565
+ - **`@object-ui/data-objectstack`** — `ObjectStackAdapter.queryGroupHeaders`
566
+ posts the compiled query verbatim to the existing `POST /data/:object/query`
567
+ door and answers its `records`. It never degrades: a refusal throws, and a
568
+ body without `records` is refused rather than read as "no groups".
569
+ - **`@object-ui/plugin-grid`** — a grouped grid that fetches its own rows from a
570
+ source declaring `queryGroupHeaders` asks for its headers (one compiled query
571
+ per grouping level) and pages each **open** group's rows with the group's
572
+ own compiled row query (`compileListViewGroupRowsQuery`: the view's filter AND
573
+ the group key, `limit` / `offset` per group); a collapsed group costs no row
574
+ query, `aggregations` are the header query's numbers, and a reference-typed
575
+ grouping key is labelled from the referenced record. Such a grid's column
576
+ sort is the server's, and it no longer shows the `Partial` marker: its counts
577
+ are the query's own. Rows handed in whole are still grouped in the browser
578
+ (exact), and a source with no header query still groups the page it fetched
579
+ and still marks it partial.
580
+ - **`@object-ui/plugin-list`** — `ListView` hands a grouped grid its own fetch,
581
+ with the view's effective filter, when the data source can answer the header
582
+ query, and draws no record-count bar over it. While a toolbar search is
583
+ active it keeps hosting the rows as before: `$search` has no counterpart on
584
+ the header query.
585
+
586
+ **Breaking semantics (declared `minor` per this repo's version policy):**
587
+
588
+ - A grouped grid over a capable source sends different requests: one header
589
+ query per grouping level plus one row query per open group, instead of one
590
+ window. A host that counted or mocked the single window sees the new shape.
591
+ - `useGroupedData` takes an optional fifth argument (the server's header rows).
592
+ - `GroupEntry` gains two REQUIRED members: `count` (the group's size — use it
593
+ instead of `rows.length`, which is empty on a server-grouped grid) and
594
+ `keyValues`. Code that only READS a `GroupEntry` is unaffected; code that
595
+ CONSTRUCTS a `GroupEntry` literal no longer type-checks until it supplies
596
+ both.
597
+ - `AggregationResult.value` is `number | null`, `null` being what the server's
598
+ aggregate answered over no values (rendered as a dash, never invented as 0).
599
+ - `@object-ui/plugin-grid` and `@object-ui/data-objectstack` now require
600
+ `@objectstack/spec` `^17.4.0`, the first release that ships the group-query
601
+ compilers.
602
+
603
+ This supersedes the last paragraph of the pending
604
+ `7189-grouped-grid-partial-disclosure` changeset, which said server-side
605
+ grouping was not built: it now is, and the `Partial` marker described there
606
+ remains only for the data sources that cannot answer the header query.
607
+
608
+ ⚠️ **Dated note, 2026-09-28 — a source with no header query now refuses
609
+ grouping — objectui#10881.** Later in this same release two sentences above
610
+ stopped describing the grid: that a source with no header query still groups
611
+ the page it fetched and still marks it partial, and that the `Partial` marker
612
+ remains for the data sources that cannot answer the header query. Maintainer
613
+ ruling F retired both. Over such a source a grouped grid that fetches its own
614
+ rows, and a `ListView` hosting a grouped grid, refuse grouping with an error
615
+ naming `queryGroupHeaders`, and the marker is gone everywhere. Rows handed in
616
+ whole are still grouped in the browser. The rest of this entry is kept as the
617
+ reading of this change; the `10881-grouping-needs-header-query` entry states
618
+ what ships.
619
+ - 4c2e1d7: Gantt views no longer hand the renderer invented `progress` / `dependencies`
620
+ field names (objectui#7499).
621
+
622
+ Flavour 3 of the card whose date-axis half landed as `5f4514f7b` — the half its 2026-09-01 ruling ordered carded
623
+ separately and judged on its own terms, ⛔ explicitly forbidding the date-axis
624
+ conclusion from being imported. Two faces floored the pair, and app-shell
625
+ carries neither key (measured: `|| 'progress'` counts 0 there; the control that
626
+ the zero is not a dead probe is that app-shell *does* floor `titleField` at
627
+ `'name'`):
628
+
629
+ - `plugin-list/src/ListView.tsx` — the `object-gantt` render branch.
630
+ - `plugin-view/src/ObjectView.tsx` — `generateViewSchema`, the authored
631
+ `object-view` element route, which bypasses `ListView` entirely.
632
+
633
+ **The remedy is OMIT, not refuse — and the asymmetry against the date axis is
634
+ exactly why the date-axis conclusion must not be imported.** A fabricated date
635
+ axis has no legitimate twin: every bar lands on a column nobody declared, a
636
+ whole-chart error, so refusal is right there. A fabricated `progress` /
637
+ `dependencies` name yields a per-row `undefined`, which is indistinguishable
638
+ from the legitimate and common case — most gantt rows have neither. Refusing
639
+ would break that common case; fabricating manufactured a binding the author
640
+ never wrote, whose failure was silent. Omission does neither.
641
+
642
+ **What changes for an author.** A view that declares neither `progressField`
643
+ nor `dependenciesField` now passes a config carrying neither key, instead of
644
+ `'progress'` / `'dependencies'`. A view that declares one passes the author's
645
+ value verbatim, exactly as before, through both the spec-canonical `gantt`
646
+ block and the legacy `options.gantt` nesting.
647
+
648
+ The only population whose behaviour actually changes is a schema carrying a
649
+ column literally named `progress` or `dependencies` while NOT declaring the
650
+ corresponding field key — those worked by accident, because the floor happened
651
+ to match the column name. That population was measured across `examples/`,
652
+ `content/docs/**`, the `packages/*/src` fixtures and all 528 JSON/YAML metadata
653
+ files before anything was deleted: it is **empty**. The census and its
654
+ blind-spot reading are in the PR.
655
+
656
+ Nothing about the refusal screen moves: `ObjectGantt.getGanttConfig` gates on
657
+ the two date fields alone, so deleting this pair cannot resurrect a config —
658
+ `plugin-gantt/src/ObjectGantt.unconfiguredRefusal-7070.test.tsx` is the
659
+ non-regression control and stays green.
660
+
661
+ ⚠️ One consequence worth recording for objectui#6470, which is NOT touched
662
+ here. `getGanttConfig`'s flat branch reads
663
+ `schema.dependenciesField || schema.dependencyField`. While the floor was in
664
+ place, `dependenciesField` was always truthy through these two faces, so the
665
+ declared legacy singular alias `dependencyField` was unreachable via them —
666
+ shadowed, not read. With the floor gone, a view that wrote `dependencyField`
667
+ now resolves through the alias limb as its `@deprecated` contract already
668
+ promises. No in-repo view declares it (measured: zero sites at either routed
669
+ face), and the alias's own deprecation and retirement remain objectui#6470's
670
+ question, deliberately left alone.
671
+ - d80447d: Stop inventing a gallery cover binding, and offer the view switcher only when it
672
+ can switch (objectui#7547).
673
+
674
+ **The object page no longer floors `gallery.imageField` at `'image'`.** It used
675
+ to supply that key for every object view, declared or not. The screen damage was
676
+ small — `ObjectGallery` collapses the cover area when no record yields a cover —
677
+ but the key fed `ListView`'s ADR-0047 capability gate, which reads
678
+ `options.gallery.imageField`, so **Gallery was offered on every object view that
679
+ whitelisted it**, with nothing behind the toggle. `galleryViewOptions` now
680
+ forwards the view's own declared block (both legacy cover spellings still
681
+ cross-fill each other, and `titleField` keeps its `'name'` display floor) and
682
+ emits no cover key when the view declared none. Same class and same route as
683
+ objectui#7029 (calendar) and `5f4514f7b` (gantt dates).
684
+
685
+ ⚠️ A view that whitelisted `gallery` without a `gallery:` block loses the Gallery
686
+ toggle. That is the ADR-0047 rule working: it was only ever offered because this
687
+ relay answered the gate on the author's behalf.
688
+
689
+ **The visualization switcher is drawn for the resolved list, not the whitelist.**
690
+ `showViewSwitcher` was computed from the LENGTH of
691
+ `appearance.allowedVisualizations` — the whitelist BEFORE `ListView` intersects
692
+ it with the capability gate — so a view whitelisting `['grid', 'timeline']` with
693
+ no timeline block drew switcher chrome around a single Grid entry. The predicate
694
+ now lives at the one site that holds both halves. Views whose whitelisted types
695
+ all resolve are unaffected.
696
+ - 6712930: The two kanban adapters stop writing the retired `groupField` onto the
697
+ `object-kanban` node they generate (objectui#7773). `groupBy` — the key the
698
+ renderer actually reads — is unchanged and is now the only lane key emitted.
699
+
700
+ **What was measured, on this branch's base (`a915064e`).** Both adapters emitted
701
+ the key twice:
702
+
703
+ ```
704
+ packages/plugin-view/src/ObjectView.tsx:1362 groupField: groupBy,
705
+ packages/plugin-list/src/ListView.tsx:2500 groupField: laneField,
706
+ ```
707
+
708
+ The `object-kanban` renderer never read it: `groupField` has ZERO hits anywhere
709
+ under `packages/plugin-kanban/`, against a control of thirteen `schema.groupBy`
710
+ read sites in `ObjectKanban.tsx` from the same query — so the zero is a reading,
711
+ not a blind grep. The write was inert; the board grouped by `groupBy` and
712
+ `groupField` rode along unread.
713
+
714
+ **Why it is removed rather than tolerated.** objectui#7322 RETIRED
715
+ `groupField` on this node on both published faces — `groupField?: never` on the
716
+ TypeScript interface and a `retirementTombstone()` in the Zod mirror, which
717
+ refuses an authored value BY NAME. So the adapters were producers emitting a
718
+ node their own published contract rejects. That was harmless only because a
719
+ generated node never reaches the mirror at runtime (`SchemaRenderer` runs the
720
+ structural `validateSchema`, not `safeValidateSchema`) — but the CLI's
721
+ `os check` / `os validate` DO run the mirror, so the identical node was already
722
+ refused when authored by hand and admitted when generated. This closes that
723
+ split.
724
+
725
+ **What changes for a consumer.** Nothing on any documented path: the renderer's
726
+ behaviour is byte-identical, because it never read the key. A host that
727
+ registers its own `object-kanban` component and reads `props.schema.groupField`
728
+ off the generated node now reads `undefined` — read `groupBy` instead, which
729
+ carries the same value and always did. Graded `minor` rather than `patch` for
730
+ exactly that narrowing, following the repo convention that objectui's own
731
+ breaking changes ship as `minor` (AGENTS.md 版本号策略, mechanically enforced by
732
+ `scripts/check-changeset-no-major.mjs`).
733
+
734
+ **Who is NOT affected — the boundary is node-local.** Every VIEW-LEVEL
735
+ `groupField` read is untouched and still live: it is a legacy alias of the
736
+ spec's `groupByField` on the kanban *view config*, mapped by
737
+ `normalize-list-view.ts`, and both adapters still resolve lanes through it
738
+ (`ObjectView.tsx`'s `kanbanCfg.groupField ||`, `ListView.tsx`'s
739
+ `groupByField || groupField`). Authoring `options.kanban.groupField` on a
740
+ `list-view` or `object-view` keeps working exactly as documented in
741
+ `packages/plugin-list/README.md`. `groupField` is dead only on the generated
742
+ `object-kanban` NODE.
743
+
744
+ The two tests that pinned the duplicate write are TURNED, not deleted — they now
745
+ assert the key is absent, so restoring the write reddens them instead of being
746
+ silently re-blessed by a missing assertion.
747
+ - edea22a: **BREAKING** — `SchemaRendererProvider`'s `dataSource` prop, and the context
748
+ type every `useSchemaContext()` consumer reads back, are the published
749
+ `DataSource` contract instead of `any`.
750
+
751
+ **FROM** `dataSource={anything}` **TO** `dataSource={adapter}` — a `DataSource`
752
+ from `@object-ui/types`, or `null` / `undefined` when the host has no adapter
753
+ bound.
754
+
755
+ ```ts
756
+ // before — compiled, and failed at runtime on the first find()
757
+ <SchemaRendererProvider dataSource={'not-an-adapter'}>
758
+ // before — compiled, and no reader can do anything with it
759
+ <SchemaRendererProvider dataSource={{}}>
760
+ // after
761
+ <SchemaRendererProvider dataSource={adapter}> // a DataSource
762
+ <SchemaRendererProvider dataSource={undefined}> // "I have no adapter"
763
+ ```
764
+
765
+ Both sites are typed `DataSource | null | undefined` — the spelling
766
+ `useSettledSchema` in this same package already used. The two absences are part
767
+ of the contract, not a weakening of it: a Studio preview, a `kind:'react'` page
768
+ rendered before the host's adapter connects, and a widget probe driving
769
+ `apiFetch` alone all render with nothing bound, and every reader in the tree
770
+ already guards for it. What the union refuses is everything that is not an
771
+ adapter: a string, an empty object, a plain data bag, a partial adapter missing
772
+ a required member.
773
+
774
+ The measured cost, both halves, because the two `any`s have different blast
775
+ radii (measured separately on `origin/main`, whole-repo type-check over the 33
776
+ packages that depend on `@object-ui/react`):
777
+
778
+ - the **context type** — the `any` that reaches every `useSchemaContext()`
779
+ reader — reds **7 diagnostics at 7 sites in 4 packages**, all of them
780
+ production code or a mocked module factory.
781
+ - the **provider prop** — the injection points — reds **52 diagnostics at 27
782
+ sites in 11 packages**, all but one of them test doubles.
783
+
784
+ That ordering is the reverse of the prediction on the card: the context `any`
785
+ was expected to be the expensive one because it infects the whole tree, and it
786
+ is the cheap one, because every reader in the tree already guarded and none of
787
+ them ever reached past `find` / `getObjectSchema`. The prop is the expensive
788
+ one, because the injection points are overwhelmingly test doubles that were
789
+ never complete adapters. The full accounting is on objectui#7912.
790
+
791
+ Two runtime behaviours change, both in the "no adapter" direction and both
792
+ strictly closer to what the surrounding code already intended:
793
+
794
+ - `@object-ui/components`' `kind:'react'` page passed an empty object as its
795
+ "no adapter yet" stand-in. An empty object is TRUTHY, so it walked past every
796
+ `if (!dataSource)` guard written to catch exactly that state and failed later,
797
+ at the call. It now passes the absent adapter itself, so the guard fires where
798
+ it was meant to. The module-constant identity that stand-in existed for is
799
+ preserved: `null` is a primitive, so the provider's memo is unaffected.
800
+ - `@object-ui/plugin-calendar`, `@object-ui/plugin-gantt` and
801
+ `@object-ui/plugin-kanban` collapse a `null` adapter from the context to
802
+ `undefined` before handing it to their widget, whose prop declares the single
803
+ spelling `dataSource?: DataSource`.
804
+
805
+ Nothing else moves at runtime: no value flowing through this key changes, and
806
+ no data path is touched. A TypeScript consumer outside this repo that handed
807
+ this prop something other than an adapter now gets a compile error naming the
808
+ key (TS2322 / TS2739 / TS2740), which is why the FROM/TO is spelled out above.
809
+
810
+ Five `as any` reads of this context in `@object-ui/fields` are gone — they were
811
+ redundant the moment the seam became honest — and `LookupField`'s local
812
+ re-declaration of the imported context as a `Context` of `any`, which laundered
813
+ its `dataSource` read while looking typed, is gone with them. Both directions of
814
+ the contract are pinned against the real compiler in
815
+ `SchemaRendererContext.dataSourceType.pin.test.ts`, and the card's planted
816
+ documentation probe (a bare string in `packages/react/README.md`'s provider
817
+ example) now fails `pnpm check:doc-snippets`, where it used to exit 0 with zero
818
+ diagnostics.
819
+
820
+ objectui#7912.
821
+ - fb82877: `ListViewProps.onSortChange` and `onFilterChange` are declared at the types they
822
+ actually fire with, instead of `any`.
823
+
824
+ Both were left `any` by the objectui#4528 sweep that named every other prop on
825
+ this interface "at the type each one actually lands on". `dataSource?: any`
826
+ carries its own written justification right there; these two carried none, which
827
+ is what made this an oversight rather than a decision. The visible cost was that
828
+ the package README's documented "With Callbacks" example compiled green on all
829
+ four callbacks while only two of them were constrained by anything: assigning
830
+ `view` or `search` to a `number` inside the block raised TS2322, and assigning
831
+ `sort` or `filters` raised nothing at all.
832
+
833
+ Each parameter was measured at the emit site rather than read off the
834
+ declaration, and the two are asymmetric:
835
+
836
+ - `onSortChange` now takes `SortItem[]`. Every emit in the component crosses one
837
+ boundary, `emitSortChange`, and both of its legs carry that element type: the
838
+ array handed in, and `filterPlatformSortableSort`'s return, which is generic in
839
+ the element and so preserves whatever it is given. Normalized-vs-raw therefore
840
+ does not move the type here, only whether platform-unsortable entries are still
841
+ present.
842
+ - `onFilterChange` now takes `FilterGroup`. Its one call site passes the toolbar
843
+ `FilterBuilder`'s own `onChange` value straight through, beside a
844
+ `setCurrentFilters` that is itself state of that type. It is deliberately NOT
845
+ the filter AST `normalizeFilters` / `buildEffectiveFilter` speak — those run
846
+ later on the query-building path and nothing they produce reaches this
847
+ callback. A host receives the builder's group verbatim, which is what lets it
848
+ round-trip back in through `initialFilters`.
849
+
850
+ Both types were already exported from `@object-ui/components` and already
851
+ imported by this file; nothing new is minted.
852
+
853
+ Narrowing a callback parameter is contravariant, so the cost falls only on
854
+ handlers that did something with the parameter that `any` alone allowed — a
855
+ handler written `(sort: any) => ...` still compiles unchanged. The cost was
856
+ measured across every in-repo consumer that passes either callback
857
+ (`ObjectView`, in two places, and `InterfaceListPage`) plus every package that
858
+ imports `ListView` at all (`@object-ui/app-shell`, `@object-ui/plugin-map`,
859
+ `@object-ui/console`): all of them still type-check, and no handler needed
860
+ touching. Out-of-repo hosts that destructure or index the parameter in ways only
861
+ `any` permitted will now see a real error, which is the point.
862
+ - cb725e7: An unbound map now REFUSES; coordinates are never guessed (objectui#8169, maintainer
863
+ ruling 2026-09-07, decision batch #67, option B).
864
+
865
+ **Behaviour change, deliberately, with no staged window.** A map with no coordinate
866
+ binding renders
867
+
868
+ > Map configuration required — declare `map.locationField` or `map.latitudeField` + `map.longitudeField`
869
+
870
+ in place of the map, instead of painting an empty one. The principle behind
871
+ the 2026-09-01 date-axis ruling (date axes are never invented) and objectui#5953 (a marker title is never
872
+ forged) now covers coordinates as well: bindings are never fabricated, and an unbound
873
+ surface refuses.
874
+
875
+ Three things moved together, because moving any one of them alone makes the tree worse:
876
+
877
+ - `@object-ui/plugin-map` — `getMapConfig` loses its default branch, the one that
878
+ returned the field names `latitude` / `longitude` / `location` / `description` when
879
+ the author declared nothing, and `ObjectMap` gains the refusal state above. The
880
+ refusal covers every branch, so a declared block that binds no coordinate field
881
+ (`map: { titleField: 'name' }`) and a half pair (`latitudeField` with no
882
+ `longitudeField`) refuse too — those used to render an empty map under the
883
+ excluded-records notice.
884
+ - `@object-ui/plugin-list` and `@object-ui/plugin-view` — the `locationField: … ||
885
+ 'location'` floor each flattener added on the way into the map is deleted. The
886
+ flattened schema now carries exactly what the view declared.
887
+
888
+ ⚠️ **Deleting the relay floors alone would have widened the guess, not closed it** —
889
+ measured on the card. The floor forced `getMapConfig`'s flat branch, which returns
890
+ `locationField` and no `latitudeField` / `longitudeField`, so dropping it on its own
891
+ would have handed undeclared views the component's three-name default branch instead of
892
+ one name, and records carrying real `latitude` / `longitude` columns would have *started*
893
+ plotting on views that declared nothing.
894
+
895
+ **Migration.** Declare the binding on any map view that relied on the old defaults:
896
+ `map: { locationField: 'your_field' }`, or `map: { latitudeField, longitudeField }`. A
897
+ record set carrying `latitude` / `longitude` (or `location`) columns no longer plots on a
898
+ view that declared no map block — it shows the refusal, which names what to write.
899
+ Interface pages are unaffected where the object actually has a location-typed field:
900
+ `defaultMapFromObject` derives `locationField` from the object's own field list, which is
901
+ a reading of declared metadata rather than a guess, and is unchanged.
902
+ - 31a0d05: Emit the spec-canonical kanban lane key from the object page, and teach the
903
+ capability gate to recognize it (objectui#8193).
904
+
905
+ `ObjectView` built the view-level kanban config it hands to `list-view` and wrote
906
+ the deprecated alias `groupField` — never `groupByField`, the key
907
+ `@objectstack/spec`'s `KanbanConfigSchema` actually declares — even though it
908
+ already READ the canonical key first. Its sibling producer in the same package,
909
+ `defaultKanbanFromObject` in `InterfaceListPage`, had migrated long before and
910
+ left the reasoning next to itself ("that read-site now prefers the spec key, so
911
+ one key is enough"); the twin was not carried along, so one producer surface
912
+ spoke two vocabularies for one concept depending on which entry point you
913
+ arrived through. `ObjectView` writes into `options.kanban`, which
914
+ `normalizeListViewSchema`'s alias fold deliberately does not reach, so nothing
915
+ corrected it downstream and the legacy spelling was what drove the lanes.
916
+
917
+ The expression is now the exported `kanbanViewOptions`, the fifth member of the
918
+ `timelineViewOptions` / `calendarViewOptions` / `ganttViewOptions` /
919
+ `galleryViewOptions` family it had been the odd one out of.
920
+
921
+ **`ListView`'s kanban capability gate changed with it, and had to.** The gate
922
+ consulted the `options.kanban` bag for the LEGACY spelling only, so a producer
923
+ writing the spec key into the bag was invisible to it while still rendering
924
+ correctly — the render branch merges the bag and resolves
925
+ `groupByField || groupField`, so the gate recognized strictly less than what
926
+ renders. Measured before and after: a bag of `{groupBy, groupField}` offered
927
+ Kanban and `{groupBy, groupByField}` did not, so shipping the producer change
928
+ alone would have removed the Kanban toggle from every object view. The gate now
929
+ reads both spellings out of the bag — the same one-question-two-sites repair
930
+ objectui#5042 made for `map` and objectui#7544 for `chart`.
931
+
932
+ **No alias READ was removed, and the alias is not retired.** Stored metadata
933
+ still authors `groupField`, and every read site still resolves it; `ListView`
934
+ already preferring the canonical key is precisely why the sibling producer could
935
+ drop the alias write. What changed is only what this one face WRITES, plus one
936
+ added rung on the gate.
937
+
938
+ **Migration.** Nothing authored has to change. If you read
939
+ `options.kanban.groupField` off the schema `ObjectView` produces, read
940
+ `groupByField` (or both) instead — that bag now carries the spec spelling.
941
+
942
+ The view-level `groupBy` in the same bag is untouched. It is not a spec key
943
+ either — measured against the strict `KanbanConfigSchema`, which refuses it by
944
+ name — but `ListView`'s projection collectors read it, so retiring it needs its
945
+ own producer census and is filed as objectui#8213.
946
+ - 474797d: **BREAKING** — the calendar date aliases `dateField` and `endField` are retired
947
+ at both faces (objectui#8355). They are now **declared refusals**: an authored
948
+ value is rejected **by name**, at its own key path, pointed at `startDateField` /
949
+ `endDateField`.
950
+
951
+ **FROM** `calendar: { dateField, endField }` — accepted in silence, read by
952
+ `ObjectCalendar`'s alias ladder — **TO** a by-name refusal at validation, with
953
+ `startDateField` / `endDateField` as the only spellings that bind the axis.
954
+
955
+ ```ts
956
+ // before — parsed green, drew a calendar
957
+ { type: 'list-view', objectName: 'task', calendar: { dateField: 'kickoff', endField: 'wrapup' } }
958
+ // after — refused at validation, naming the canonical keys
959
+ { type: 'list-view', objectName: 'task', calendar: { startDateField: 'kickoff', endDateField: 'wrapup' } }
960
+ ```
961
+
962
+ **The refusal an author now meets**, verbatim from the arms' own messages (one
963
+ string per arm feeds both the parse-time issue and the `.describe()` metadata):
964
+
965
+ ```text
966
+ Unrecognized key(s) on this calendar configuration: `dateField`. Did you mean
967
+ `dateField` → `startDateField`? `dateField` and `endField` are the pre-#2231
968
+ objectui spellings of the calendar date axis, retired at both faces by
969
+ objectui#8355. `@objectstack/spec` spells the axis `startDateField` and
970
+ `endDateField` only. ⚠️ This package accepted BOTH legacy spellings green until
971
+ that retirement — they rode a `.passthrough()` straight through
972
+ `safeValidateSchema` into `ListView`'s calendar branch, which flattened them onto
973
+ the generated `object-calendar` node where the renderer's alias ladder read them.
974
+ That ladder is gone, so the key is refused here instead of being kept and then
975
+ ignored. Write `startDateField` for the event start. Kept rather than refused, an
976
+ authored `dateField` binds nothing: the calendar falls through to "Calendar
977
+ configuration required", a screen that names the canonical keys and never the key
978
+ you wrote.
979
+ ```
980
+
981
+ ⚠️ **The consequence clause is per key, because the two spellings fail
982
+ differently.** `endField` gets the same stem and this tail instead — measured on
983
+ the renderer, not assumed:
984
+
985
+ ```text
986
+ …Write `endDateField` for the event end. Kept rather than refused, an authored
987
+ `endField` fails even more quietly than its sibling: a calendar that also carries
988
+ a start binding still DRAWS, and only the end of every event is silently dropped.
989
+ ```
990
+
991
+ On the flat node face the surface noun is `this object-calendar node`. ⚠️ **The
992
+ element's own `calendar` container carries a different tail for `dateField`**,
993
+ because it fails differently and was measured doing so: `getCalendarConfig` reads
994
+ that container WHOLE, so a retired spelling there never reaches the refusal
995
+ screen — the calendar mounts and simply places nothing, every record landing
996
+ under "Unscheduled". `endField` keeps one clause, because it reads the same on
997
+ both.
998
+
999
+ ```text
1000
+ …Write `startDateField` for the event start. Kept rather than refused, an
1001
+ authored `dateField` fails without even reaching that refusal screen: this
1002
+ container is read WHOLE, so the calendar still mounts and simply places nothing —
1003
+ every record ends up under "Unscheduled" with no date to draw it on.
1004
+ ```
1005
+
1006
+ **ADR-0087 disposition — D2 tombstone, no conversion entry and no migration
1007
+ prescription.** The keys stay DECLARED and unwritable rather than being deleted,
1008
+ which is the whole of the ruling: every calendar block here ends `.passthrough()`
1009
+ and `BaseSchema` does too, so a deleted key is not refused — it is KEPT
1010
+ unexamined and then ignored. Nothing in `@objectstack/spec` converts these two
1011
+ (the protocol never declared either: `CalendarConfigSchema` is a strict object of
1012
+ `startDateField` / `endDateField` / `titleField` / `colorField`, measured on the
1013
+ installed pin 17.4.0 with a nonsense-key control on the same call), so there is
1014
+ no ledger entry to register and no `objectstack migrate meta` step to ship. The
1015
+ channel that reaches an author is the validator, and it now names the key and the
1016
+ remedy on all five authoring surfaces this vocabulary has: a list view's
1017
+ `calendar` block and its legacy `options.calendar` twin, a named view's two
1018
+ nestings under `listViews`, the flat `object-calendar` node face, and that
1019
+ element's own `calendar` container. ⚠️ Stated as five surfaces rather than
1020
+ "every author" on purpose: a host that builds the node in TypeScript and hands it
1021
+ to `SchemaRenderer` never parses the mirror at all — `SchemaRenderer` runs the
1022
+ structural core validator, which is dev-only and names no key — so the compiler
1023
+ is that population's channel, not this one.
1024
+
1025
+ ⚠️ **Dated note, 2026-09-28 — a named view's `options.calendar` nesting is no longer
1026
+ judged inside — objectui#7928.** Later in this same release `ObjectViewSchema.listViews`
1027
+ became the protocol's strict `ObjectListViewSchema` record, by reference, and that
1028
+ record refuses a named view's `options` bag whole (`unrecognized_keys` at
1029
+ `listViews.KEY`, naming `options`). So an alias written under
1030
+ `listViews.KEY.options.calendar` no longer draws an issue naming the key or the remedy.
1031
+ The named view's `calendar` block still does: the protocol refuses the alias there as an
1032
+ unrecognized key, and this entry's check adds the pointer at the key beside it. A stored
1033
+ body that `@object-ui/app-shell`'s `ViewPreview` relays has its `options.calendar` folded
1034
+ onto the `calendar` block first (`foldStoredListOptions`). The list view's two nestings
1035
+ and both `object-calendar` surfaces are unchanged; `calendar-date-alias-refusal-8355.test.ts`
1036
+ in `@object-ui/types` re-derives each surface. `.changeset/7928-listviews-by-reference-fold.md`
1037
+ (PR objectui#10821) states what ships; the text above is kept as the reading of this change.
1038
+
1039
+ ⚠️ **Why this is `minor` and not `major`.** This repository forbids `major` in a
1040
+ changeset: the fixed release group's major tracks `@objectstack`'s, so any
1041
+ `major` here would push all of it off that cadence
1042
+ (`scripts/check-changeset-no-major.mjs` enforces it). objectui's own breaking
1043
+ changes ship as `minor` with the break spelled out, which is what this entry is.
1044
+
1045
+ **What broke silently before, and why the two halves ship together.** An earlier
1046
+ attempt removed the renderer's alias ladder on its own. The producer kept
1047
+ spreading the authored block flat onto the generated `object-calendar` node, so
1048
+ the alias still arrived, nothing read it, and the author met a generic "Calendar
1049
+ configuration required" screen naming keys they had not written. A live authoring
1050
+ path stopped rendering with every gate green (recorded on objectui#8651). The
1051
+ refusal at the declaration is what turns that silence into a loud, by-name
1052
+ failure — so the ladder removal, the producer strip and the tombstones are one
1053
+ change and must stay one.
1054
+
1055
+ **The four moving parts:**
1056
+
1057
+ - `@object-ui/types` — `aliasKeyRefusal()` arms on the view-level `calendar`
1058
+ block, on the legacy `options.calendar` nesting (a check, since an open record
1059
+ declares no member — `custom` there, `invalid_type` at the declared slot), on
1060
+ the flat `object-calendar` node face, and on that element's own `calendar`
1061
+ container; plus a `.check()` on `ObjectViewSchema` for a named view's two
1062
+ nestings. `z.input` of each arm is `undefined`, so the TypeScript face carries
1063
+ `dateField?: never` / `endField?: never` and `tsc` refuses the key at the
1064
+ authoring site as well.
1065
+ - `@object-ui/plugin-calendar` — `getCalendarConfig` reads the declared
1066
+ spellings only; the `CalendarAliasRungs` cast target and both read sites are
1067
+ gone, and the config memo's dependency list loses the two entries with them.
1068
+ - `@object-ui/plugin-list` — the `calendar` branch destructures the two
1069
+ spellings out of the merged block and spreads only the remainder onto the node,
1070
+ exactly as the kanban branch strips its own stray `groupBy` (objectui#8365).
1071
+ - `@object-ui/plugin-view` — **the SECOND route**, and the reason this entry
1072
+ moved four packages rather than three. `generateViewSchema` runs when no host
1073
+ supplied `renderListView` — the authored `object-view` element — so a named
1074
+ view never passes through `ListView`. Its calendar branch spread the authored
1075
+ block raw, so removing the renderer's ladder turned a document that DREW into
1076
+ one that shows "Calendar configuration required" and nothing else. Measured end
1077
+ to end before the fix was written. It now strips the two spellings like its
1078
+ sibling, and the `ObjectViewSchema` check is what makes an authored alias loud
1079
+ there instead of mute.
1080
+
1081
+ ⚠️ **`ObjectViewSchema.listViews` stays UNMIRRORED, and this entry does not
1082
+ change that.** That key waits on a maintainer decision about its VALUE TYPE —
1083
+ neither the spec's strict `ObjectListViewSchema` nor the local `NamedListView`
1084
+ can be the declared value without losing documented behaviour or enforcing 43
1085
+ unread members. A `.check()` decides none of it: it declares no value type, puts
1086
+ no key in the object's `shape`, requires no `columns`, and refuses neither an
1087
+ undeclared key nor the legacy `options` bag. It judges exactly the two spellings
1088
+ this entry retires, at the two nestings the producer merges — with pins on each
1089
+ of those non-effects.
1090
+
1091
+ ⚠️ **Dated note, 2026-09-28 — `listViews` has since been mirrored — objectui#7928.** Later
1092
+ in this same release the maintainer's ruling A chose the value type this paragraph waits
1093
+ on: `ObjectViewSchema.listViews` is now `@objectstack/spec`'s own
1094
+ `ViewSchema.shape.listViews`, a record of the strict `ObjectListViewSchema`, by reference
1095
+ on both faces. It is in the object's `shape`, a named view needs `columns`, and the
1096
+ record refuses an undeclared key and the legacy `options` bag whole. The named-view check
1097
+ described here and in the `@object-ui/types` bullet above now reads the protocol's own
1098
+ refusal at `listViews.KEY.calendar` and adds the pointer at the key; it no longer walks
1099
+ the `options.calendar` nesting, so "a named view's two nestings" and "at the two nestings
1100
+ the producer merges" now hold for the `calendar` block only.
1101
+ `.changeset/7928-listviews-by-reference-fold.md` (PR objectui#10821) states what ships;
1102
+ the text above is kept as the reading of this change.
1103
+
1104
+ ⛔ **Not a producer-side fold.** Normalising `dateField` to `startDateField` in
1105
+ `ListView` (option A) was put to the director seat and refused as the end state:
1106
+ it keeps a second spelling alive at the producer, which is the lenient alias
1107
+ AGENTS.md #0.1 names. The aliased view therefore produces **no** date binding,
1108
+ and the author is told why at the door instead.
1109
+
1110
+ ⚠️ **Upstream's refusal suggests the wrong canonical key for `dateField`, and it
1111
+ is a typo-distance suggester rather than a declaration.** Re-derived by running
1112
+ the installed pin (`@objectstack/spec` 17.4.0), with controls in the same pass:
1113
+ `CalendarConfigSchema`'s `strictObject` options carry `surface` and `history` and
1114
+ no `aliases` entry, so the protocol declares nothing about either spelling. The
1115
+ "Did you mean `dateField` → `endDateField`?" an author meets is a fallback
1116
+ `findClosestMatches` Levenshtein suggestion budgeted `max(2, floor(len/3))`:
1117
+ `endDateField` is 3 edits from `dateField` and inside its budget of 3, while
1118
+ `startDateField` is 5 and outside it — and `endField`, budgeted 2, reaches
1119
+ nothing, which is why it and a nonsense control key both draw no suggestion at
1120
+ all. A one-character typo of the canonical key does resolve to `startDateField`,
1121
+ so the suggester is working as designed; it simply has no opinion to offer about
1122
+ a legacy alias.
1123
+
1124
+ ⇒ **nothing upstream says `dateField` means the end of an event.** The hazard is
1125
+ author-facing rather than contractual: someone who copies that suggestion writes
1126
+ `endDateField` and binds the END of an event to the date they meant as the START,
1127
+ which every layer accepts. Every objectui read site folds this spelling onto the
1128
+ **start** — the retired ladder did, `normalizeListViewSchema`'s `timeline` fold
1129
+ does, `resolveTimelineDateBinding` documents it as "the pre-#2231 alias for
1130
+ `startDateField`", and this package has published "Deprecated alias for
1131
+ startDateField" for releases. So these arms name `startDateField` and a control
1132
+ pin holds that line. The remedy upstream needs is an explicit `aliases` (or
1133
+ `guidance`) entry for the two spellings so the suggester never answers for them;
1134
+ that is upstream's formatter, on upstream's card, and is not fixed here.
1135
+
1136
+ ⛔ **The timeline alias is NOT retired by this change.** `timeline.dateField`
1137
+ stays a live, accepted alias with live consumers (`normalizeListViewSchema` folds
1138
+ it onto `startDateField`, `ObjectView` reads it, and app-shell pins that it still
1139
+ renders). The card's own boundaries put the map / gantt / timeline / kanban
1140
+ ladders on their own cards; a pin in `@object-ui/types` asserts the timeline
1141
+ spelling still parses green, so a later sweep has to say so rather than carrying
1142
+ it in silently.
1143
+
1144
+ **Pinned** by `calendar-date-alias-refusal-8355.test.ts` (`@object-ui/types`, all
1145
+ five surfaces, the per-surface consequence split, the compile-time face, and the
1146
+ structural guard that `listViews` stays out of the object's `shape`),
1147
+ `ListView.calendarAliasRefused-8355.test.tsx` (`@object-ui/plugin-list`) and
1148
+ `ObjectView.calendarAliasRefused-8355.test.tsx` (`@object-ui/plugin-view`) — both
1149
+ reading the node their producer really emits through a spy registration, which is
1150
+ the only census that can see a key arriving through a spread — and the inverted
1151
+ ledger rows in `calendarUnionReads-8651.test.tsx` (`@object-ui/plugin-calendar`).
1152
+
1153
+ ⚠️ **Dated note, 2026-09-28 — a named view now refuses `timeline.dateField`, and the
1154
+ shape guard is inverted — objectui#7928.** Later in this same release the protocol's
1155
+ strict named-view record refuses `timeline.dateField` on a named view as an unrecognized
1156
+ key; the `list-view` route still accepts it, and that route is the one the timeline pin
1157
+ reads. The structural guard in `calendar-date-alias-refusal-8355.test.ts` now asserts
1158
+ the opposite of "`listViews` stays out of the object's shape": `listViews` is a member
1159
+ of `ObjectViewSchema.shape`. `.changeset/7928-listviews-by-reference-fold.md` (PR
1160
+ objectui#10821) states what ships; the text above is kept as the reading of this change.
1161
+ - 704e695: **A stray `groupBy` in a view's kanban config no longer overrides the lane, and on a `list-view` document is now refused by name.**
1162
+
1163
+ `ListView`'s kanban branch destructured
1164
+ `columns`/`groupByField`/`groupField`/`cardFields`/`titleField` out of the merged
1165
+ kanban config and spread the **rest** *after* its own `groupBy: laneField`. A
1166
+ `groupBy` surviving in that bag therefore **overrode the lane the branch had just
1167
+ resolved**. Measured on a distinguishing fixture, not reasoned: with
1168
+ `options.kanban = { groupBy: 'LANE_FROM_STRAY_GROUPBY' }` against
1169
+ `kanban = { groupByField: 'LANE_FROM_CANONICAL' }`, the generated `object-kanban`
1170
+ node carried `groupBy: 'LANE_FROM_STRAY_GROUPBY'`. It read as latent only because
1171
+ the one producer that fed it wrote both spellings with the same value by
1172
+ construction; that producer was retired separately, and this closes the override
1173
+ itself.
1174
+
1175
+ Two halves, per the maintainer's ruling (option B — a silent re-grouping was the
1176
+ fallback and was **not** taken):
1177
+
1178
+ 1. **The canonical lane wins.** `groupBy` joins the destructure, so the stray key
1179
+ can no longer reach the passthrough spread. A view that authored it now groups
1180
+ by whatever `groupByField` / `groupField` / the declared lifecycle field
1181
+ resolves.
1182
+ 2. **The stray key is refused loudly, at the read door of the view.** This repo's
1183
+ `.passthrough()` `KanbanConfig` mirror (`@object-ui/types`) declares `groupBy`
1184
+ as a named alias refusal pointing at `groupByField`, in the same sentence
1185
+ shape `@objectstack/spec` already answers the sibling alias with — "Unrecognized
1186
+ key(s) on this kanban configuration: `groupBy`. Did you mean `groupBy` →
1187
+ `groupByField`?". The refusal lands on the `list-view` route: wherever
1188
+ `safeValidateSchema` validates a `list-view` document (objectui's own
1189
+ `objectui validate` runs it and prints the issue), and at the authoring site,
1190
+ where `tsc` refuses the declared `kanban.groupBy` (the inferred authoring face
1191
+ now carries `groupBy?: never`). The legacy `options.kanban` nesting — where the
1192
+ retired producer wrote, and so where stored views carry the key — takes the
1193
+ identical message through a check on that untyped bag. A named view's
1194
+ `listViews.KEY.kanban.groupBy` on an `object-view` document is not reached by
1195
+ this arm; objectui#10321 gives that route its own door, which refuses the key
1196
+ in either nesting with the same message.
1197
+
1198
+ ⚠️ **Dated note, 2026-09-28 — a named view's `options.kanban` bag is refused whole —
1199
+ objectui#7928.** Later in this same release a named view became the protocol's strict
1200
+ `ObjectListViewSchema` record, by reference, which refuses its `options` bag by name
1201
+ (`unrecognized_keys` at `listViews.KEY`, naming `options`). So the objectui#10321 door no
1202
+ longer refuses the key "in either nesting with the same message": it still adds that
1203
+ message at `listViews.KEY.kanban.groupBy`, beside the protocol's own refusal of the key,
1204
+ and nothing reports `listViews.KEY.options.kanban.groupBy`. A stored body that
1205
+ `@object-ui/app-shell`'s `ViewPreview` relays has its `options.kanban` folded onto the
1206
+ `kanban` block first (`foldStoredListOptions`). The `list-view` route described above is
1207
+ unchanged, its `options.kanban` check included.
1208
+ `.changeset/7928-listviews-by-reference-fold.md` (PR objectui#10821) states what ships;
1209
+ the text above is kept as the reading of this change.
1210
+
1211
+ **Breaking, in the sense worth stating explicitly** (shipped `minor`: this repo
1212
+ never declares `major`, and `.changeset/config.json` puts every package in one
1213
+ `fixed` group, so levels cannot be split). Two behaviours change for **stored
1214
+ data**, which is why this is not a patch:
1215
+
1216
+ - a stored view authoring `kanban.groupBy` (either nesting) **re-points its lane**
1217
+ — it used to group by the stray key and now groups by the canonical binding, so
1218
+ its board may show different columns;
1219
+ - the same document, on the `list-view` route, now **fails validation** where it
1220
+ used to pass: any pipeline running `safeValidateSchema` over it (objectui's own
1221
+ `objectui validate` does) reports one issue naming the key and the replacement.
1222
+
1223
+ Honouring `groupBy` as a declared alias was never an option here:
1224
+ `@objectstack/spec`'s `KanbanConfigSchema` is a strict object of
1225
+ `columns` / `groupByField` / `summarizeField` and refuses it by name — re-measured
1226
+ on the pinned 17.4.0 with both controls firing — so legalising it would be a spec
1227
+ change, not a renderer widening (AGENTS.md #0.1).
1228
+
1229
+ `groupBy` on the generated `object-kanban` **node** is untouched: that is the live,
1230
+ canonical lane key `ObjectKanban` reads. Only the **view-level** kanban config
1231
+ spelling is refused. The `.passthrough()` itself is kept — an undeclared sibling
1232
+ key still rides through, which is pinned as a control.
1233
+ - 3a9ab02: `list-view`: retire the legacy `title` alias from the export-filename read, and pin the
1234
+ `rowActionDefs` exemption at both of `ListView`'s read sites (objectui#8653, the
1235
+ objectui#8327 family's `plugin-list` card).
1236
+
1237
+ **Two keys, two opposite exits — and the contract chose each one.**
1238
+
1239
+ `title` — **retired.** `ListView` resolved its export filename through
1240
+ `schema.label || (schema as any).title`. `@objectstack/spec/ui`'s `ListViewSchema`
1241
+ refuses `title` **by name** (`unrecognized_keys: ['title']`) while
1242
+ `ObjectGridPropsSchema` **accepts** it; `packages/types` mirrors the platform contract
1243
+ rather than ruling over it, so declaring `title` on `ListViewSchema` would have made
1244
+ this repo accept what the platform save gate rejects. That asymmetry is also why
1245
+ objectui#6639 could take the *declare* branch for `ObjectGridSchema.title` one package
1246
+ over and this site could not. A parse-based census of `apps/ examples/ content/` and
1247
+ `packages/` found **zero** `list-view` nodes authoring `title`, so the retirement costs
1248
+ no author a filename. Over that same corpus the instrument reports **three**
1249
+ `object-grid` nodes carrying the key: **two authored** ones, both in
1250
+ `content/docs/api/schema-reference.md`, plus one that is not authored at all —
1251
+ `packages/plugin-view/src/ObjectView.tsx` composes `title: schema.table?.title` onto a
1252
+ grid node it builds, so it is a producer writing the key rather than an author
1253
+ declaring it. `ObjectGrid`'s own `title` reads are untouched — they remain declared,
1254
+ ruled and read.
1255
+
1256
+ **Behaviour change, deliberate.** A list view authoring only `title` (no `label`) no
1257
+ longer contributes a view segment to the export filename; it exports as
1258
+ `<objectLabel|objectName>-<timestamp>.<ext>`. Migration: write `label`, which is the
1259
+ declared slot on both this repo's `ListViewSchema` and the platform's.
1260
+
1261
+ **Bug fixed in the same expression.** The `as any` was laundering a second defect:
1262
+ `X || any` collapses the whole expression to `any`, so an inline locale-map `label`
1263
+ (`{ en: 'Quarterly Review', zh: '季度复盘' }` — the shape `BaseSchema.label` has
1264
+ published since objectui#4580's revised Q1) reached `sanitizeFileNameBase` **unresolved**
1265
+ and exported as `account-[object Object]-….csv`. The read now goes through the spec's
1266
+ own `resolveI18nLabel` against the display locale, the same resolver and argument order
1267
+ `ObjectGrid` already used at its twin site.
1268
+
1269
+ `rowActionDefs` — **exempt, still read, and now pinned.** objectui#5091 ruled this
1270
+ producer-derived key NON-AUTHOR SURFACE for `object-grid` on 2026-08-19. The exemption
1271
+ extends to `ListView`: the platform refuses the key by name on **both** surfaces,
1272
+ `@object-ui/types` declares it on neither mirror, and the producer is the same one
1273
+ (`app-shell`'s `ObjectView` derives it from `objectDef.actions` filtered by
1274
+ `locations.includes('list_item')`; `plugin-view`'s composes the same key onto a
1275
+ `list-view` node). Both read sites now carry the ruling in a docblock, and a new pin
1276
+ asserts — at runtime, never by source grep — that the defs still reach the child
1277
+ `object-grid` node **and** that a field named only by a row action's `visible` CEL still
1278
+ reaches `$select`. Without that second half, deleting the read would have returned rows
1279
+ whose predicate operand was never selected: objectui#3501's fail-closed CEL fault
1280
+ arriving with a success receipt.
1281
+
1282
+ No published type changed: neither key is declared on any face by this change.
1283
+ - 502eb58: The row/card click props on the view components now declare the modifier
1284
+ payload they have always been invoked with (objectui#9357).
1285
+
1286
+ `useNavigationOverlay`'s `handleClick` invokes the handler it is given with two
1287
+ arguments — the record, and an optional modifier payload (`metaKey` / `ctrlKey`
1288
+ / `button`) a host needs to implement Cmd/Ctrl/middle-click. objectui#9360 made
1289
+ the hook's own option say so. These components pass a host-supplied prop
1290
+ straight into that option, so the value a host writes against them is invoked
1291
+ with two arguments too — and every one of these props declared only the record.
1292
+ The payload was therefore invisible on the one line a host reads, exactly as it
1293
+ had been on the hook.
1294
+
1295
+ Thirteen declarations across nine packages now name both parameters:
1296
+
1297
+ - `ObjectGridComponentProps.onRowClick`
1298
+ - `ObjectKanbanComponentProps.onRowClick` (its sibling `onCardClick`, the other
1299
+ arm of the same fallback, already declared both)
1300
+ - `KanbanRendererProps.schema.onCardClick`
1301
+ - `ObjectCalendar`, `ObjectGantt`, `ObjectMap`, `ObjectTree`: `onRowClick`
1302
+ - `ObjectTimeline`: `onRowClick` and `onItemClick`, the two arms of one fallback
1303
+ - `ListView.onRowClick`
1304
+ - `ObjectGallery`: `onRowClick` and `onCardClick`, likewise two arms of one
1305
+ - `RelatedList.onRowClick`
1306
+
1307
+ **Source-compatible, and with no refused class.** A one-parameter handler is
1308
+ still assignable to the widened signature, and a handler written against the
1309
+ widened signature was already assignable to the narrow one — its minimum
1310
+ argument count is still one. The second parameter is spelled `any` rather than
1311
+ `HandleClickModifiers`, which is the difference that matters for callers: the
1312
+ hook's own option names that interface and therefore refuses a handler whose
1313
+ second parameter is annotated narrower (objectui#9360 documents that class and
1314
+ the one-line remedy). These faces refuse nothing. A host that discovered the
1315
+ payload from the implementation and annotated it `React.MouseEvent` — which is
1316
+ what actually arrives — keeps compiling. `any` is also the spelling
1317
+ `ObjectKanbanSchema.onCardClick` already carries for this same payload
1318
+ (objectui#9341) and the one `BaseSchema`'s own `onClick` / `onChange` /
1319
+ `onSubmit` use, and it is forced on the published twins in `@object-ui/types`,
1320
+ which may not name a type that lives in a package depending on them.
1321
+
1322
+ **Runtime behaviour is unchanged.** Nothing is newly called and nothing newly
1323
+ passes an argument; only the declarations move.
1324
+
1325
+ Three declarations that share the name and the shape are deliberately NOT
1326
+ widened, because the value flowing through them is not invoked with the
1327
+ payload: `VirtualGrid.onRowClick`, whose second parameter is a row index;
1328
+ `ManageViewsDialog.onRowClick`, which receives a view id; and the `data-table`
1329
+ family (`DataTableSchema.onRowClick`, `ObjectDataTableSchema.onRowClick`),
1330
+ whose renderer invokes with one argument. Widening those would have declared a
1331
+ payload that never arrives.
1332
+ - de5d400: A page size with nothing declared is now the one `@objectstack/spec` declares for
1333
+ `pagination.pageSize`, on every surface that has a pager; a fetch that has no
1334
+ pager keeps its own fetch batch, which does not follow it (objectui#9853).
1335
+
1336
+ "Page size" now means one page, and only where there is a pager. The grid,
1337
+ the list and the board read the display default from the spec (`PaginationConfigSchema`)
1338
+ instead of keeping numbers of their own, so it is declared once, in the protocol.
1339
+ `@objectstack/spec` 17.5.0 declares **50**. The views that fetch one window and
1340
+ page nothing keep a fetch batch of their own, with the value they had, so no
1341
+ view loses records it could reach before.
1342
+
1343
+ ⚠️ **Visible change where no page size is declared.** Measured on the renderer
1344
+ with `@objectstack/spec` 17.5.0. Declare `pagination: { pageSize: N }` (or, on a
1345
+ board, `limit`) to keep an old count:
1346
+
1347
+ | surface (nothing declared) | before | after |
1348
+ |:--|--:|--:|
1349
+ | `object-grid` that fetches its own rows: rows per page, and the `$top` of each page fetch | 50 | 50 (unchanged) |
1350
+ | `object-grid` over inline `data`: rows per page | 10 | **50** |
1351
+ | grouped `object-grid`: groups per page | 10 | **50** |
1352
+ | grouped `object-grid` over a data source that groups on the server: rows per group page (`$top`) | 50 | 50 (unchanged) |
1353
+ | `list-view`, grid view (paged on the server): rows per page, and `$top` | 100 | **50** |
1354
+ | `list-view`, every view it does not page (kanban, calendar, gallery, timeline and the rest, and a grouped grid): the one fetch's `$top` | 100 | 100 (unchanged) |
1355
+ | `object-kanban` with no `limit`: the board's `$top` | 100 | 100 (unchanged) |
1356
+
1357
+ What changed underneath:
1358
+
1359
+ - **`@object-ui/plugin-grid`.** `ObjectGrid` falls back to the spec's display
1360
+ default on every page it shows, with no local number behind it: if a future
1361
+ spec stops declaring the default, the module throws at load instead of
1362
+ inventing one. The old "server window" constant is renamed
1363
+ `DEFAULT_FETCH_BATCH_SIZE` and keeps its value. Since grouping moved to the
1364
+ server, it is read only when the grid groups a window of rows in the browser
1365
+ because the server does not group it (a grouping key the principal may not
1366
+ read), and never as a page size. The grouped view's rows-per-page selector
1367
+ also lists the size in force when it is not one of its fixed steps.
1368
+ - **`@object-ui/plugin-list`.** `ListView` splits its undeclared fallback by
1369
+ whether the view pages. The grid view, which it pages on the server, falls
1370
+ back to the spec's display default. Every other view falls back to a fetch
1371
+ batch of 100, the value it had. A declared `pagination.pageSize` sizes the
1372
+ window on every view, as before. With no declared size, switching between the
1373
+ grid view and an unpaged view now changes the window, so the list fetches
1374
+ again: one request, whose `$top` is the fetch batch. That qualifies the
1375
+ objectui#10512 note in this same release, which says a switch into a dataset
1376
+ chart with no user filter set issues no new request: it still holds when a
1377
+ page size is declared. A refused page size still warns, and the warning names
1378
+ the fallback that view actually used.
1379
+ - **`@object-ui/plugin-kanban`.** The board's default `limit` is renamed from
1380
+ `DEFAULT_KANBAN_LIMIT` to `DEFAULT_KANBAN_FETCH_BATCH_SIZE` and stays 100. Its
1381
+ diagnostic for a refused `limit` now calls it the board's fetch batch.
1382
+
1383
+ The display default follows the `@objectstack/spec` a host installs. A host
1384
+ that resolves an older spec gets that spec's default, on every surface alike.
1385
+
1386
+ Released as `minor`, not `major`: this repository keeps its major aligned with
1387
+ `@objectstack/spec`, so the behaviour change is spelled out here instead.
1388
+ - e2e8e68: **Behaviour change:** the spec's view-level `map` block on a list view is now read at
1389
+ runtime. `ListMapConfigSchema` (objectstack#9340) has been authorable and validated since
1390
+ the `@objectstack/spec` 17.1.0 pin — it flows into this repo's own `ListViewSchema` by
1391
+ reference — but nothing consumed it: `ListView`'s `case 'map'` forwarded only the legacy
1392
+ `schema.options.map` bag, so declaring `map: { titleField: 'title', locationField:
1393
+ 'location' }` on a view changed nothing and marker titles fell back to the renderer's
1394
+ placeholder.
1395
+
1396
+ The block now reaches `plugin-map` and drives every one of its seven reads — coordinate
1397
+ extraction, marker title and description, and the initial camera. Precedence follows the
1398
+ convention the sibling visualization blocks in the same file already set: the view-level
1399
+ block wins over `options.map`, per key, exactly as `kanban` / `calendar` / `gallery` /
1400
+ `timeline` / `gantt` each merge their spec config over the legacy bag. Both sources go
1401
+ through the existing objectui#5177 key whitelist, and the branch still emits the flat
1402
+ form, so `getMapConfig`'s objectui#5018 precedence rule ("neither flattener emits a `map`
1403
+ key at all") stays true.
1404
+
1405
+ The visualization switcher had the same gap with a sharper consequence: the capability
1406
+ gate that decides which visualizations are offered also read `options.map` alone, so a
1407
+ view binding its coordinates in the spec block was filtered out of its own
1408
+ `appearance.allowedVisualizations` and fell back to `['grid']`. The gate now asks the same
1409
+ merged config the render seam forwards, so the two cannot disagree — including for a
1410
+ binding split across the two sources.
1411
+
1412
+ `InterfaceListPage` (ADR-0047 interface pages) forwards the referenced view's `map` block
1413
+ for the same reason. It is passed alongside the auto-derived `options.map` rather than
1414
+ replacing it, so a partial authored block — `map: { titleField: 'title' }` — keeps the
1415
+ derived coordinate binding instead of dropping it.
1416
+
1417
+ No defaults are introduced for `zoom` / `center`: an undeclared camera stays undeclared,
1418
+ so the fit-to-queried-records behaviour ruled in objectui#5000 is unchanged.
1419
+ - ca39427: Derive `ViewType` from `@objectstack/spec` instead of re-declaring it
1420
+
1421
+ `ViewType` and its zod face `ViewTypeSchema` were hand-written eleven-arm copies of
1422
+ the spec's list-view type vocabulary. `@objectstack/spec@17.3.0` added `page` and
1423
+ neither followed, so every structure keyed on them stayed total over the copy and
1424
+ compiled green while being incomplete — including the one whose doc comment promises
1425
+ that "a kind added to the union fails the build HERE". A spec-valid `type: 'page'`
1426
+ view was left unresolved and fell back to a grid with no error, no warning and no
1427
+ console line, while the published validator accepted it.
1428
+
1429
+ Both faces now derive from `@objectstack/spec/ui` `ListView['type']`, and the
1430
+ renderer-side structures derive from that in turn:
1431
+
1432
+ - `@object-ui/core` exports `ListViewVisualization` and `isListViewVisualization` —
1433
+ the visualizations `ListView` draws, which deliberately exclude `page` for the same
1434
+ reason the spec's own `VisualizationType` does (a `type: 'page'` view mounts a
1435
+ published page through `pageName` rather than drawing records).
1436
+ - A spec-valid but undrawable kind still falls back to a grid, but now warns once
1437
+ instead of degrading identically to a typo.
1438
+
1439
+ Widened surfaces: `ViewType` / `ViewTypeSchema` gain `page`; `UnifiedViewType` gains
1440
+ `tree` (it had drifted); the `list-view` SDUI registry offers `chart` and `tree`,
1441
+ which the renderer has drawn for releases.
1442
+
1443
+ ⚠️ Consumers holding an exhaustive `switch` or a total `Record<ViewType, …>` will
1444
+ now fail to compile until they account for `page`. That failure is the point of the
1445
+ change — it is the guarantee that was silently lost.
1446
+
1447
+ ⚠️ **Dated note, 2026-09-29 — `page` left the vocabulary again in this same release — objectui#11073.**
1448
+ Later in this release this repository began resolving `@objectstack/spec` 17.5.0, which removed `type: 'page'` from the list-view vocabulary (ADR-0049 enforce-or-remove). Because `ViewType` and `ViewTypeSchema` are derived from the spec, as this change made them, they no longer carry `page`. A `type: 'page'` view is refused by name at parse rather than being "spec-valid", and the one-time "cannot draw" warning has no kind left to explain. The rest of this change stands: the vocabulary still follows the spec, now in the narrowing direction.
1449
+ - 2d36552: Pins `@objectstack/spec`, `@objectstack/client`, `@objectstack/formula` and `@objectstack/lint` to `17.1.0`, and adapts the two consumer surfaces the new build moves.
1450
+
1451
+ The pin itself is a lockfile refresh — every manifest already declared `^17.0.0`, which admits `17.1.0`, so no dependency range changed. All four move together: a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history.
1452
+
1453
+ **A `icontains` filter now reaches the driver as a filter.** `icontains` is a canonical `VIEW_FILTER_OPERATORS` member as of `17.1.0`, so an author can declare it on a `ViewFilterRule` and the spec validates it — but `@object-ui/data-objectstack`'s alias table had no row for it, and an unmapped operator is how this adapter shipped an unfiltered query before (objectstack#3948). It is an identity row like `contains`: `icontains` is itself a member of `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the AST takes, and no case-sensitivity is translated away. Declared rather than left to the table's `?? op` fall-through, on the rule its own parity test states — the AST gate accepting a spelling is not the driver compiling it into a `WHERE` clause.
1454
+
1455
+ The same operator reaches the list view's own bridge: `@object-ui/plugin-list`'s `mapOperator` gains an explicit `icontains` arm. The emitted spelling is identical to the input, but the arm is written out rather than left to the `default` passthrough — `icontains` is its own member of `VALID_AST_OPERATORS`, so a raw passthrough is accepted *today*, and depending on that coincidence is what the bridge's own parity test records as how it once stopped discriminating.
1456
+
1457
+ `@object-ui/core` adds `onSuccess` to its spec key inventory, so an author writing the key `17.1.0` now declares is no longer warned that it is unknown. That is a diagnostic statement only — the four declared action surfaces still drop the key before it reaches the runner, which is tracked separately.
1458
+
1459
+ **A stored view filtering case-insensitively still shows that operator when it is reopened.** `@object-ui/plugin-view`'s canonical-to-builder table is keyed by `ViewFilterOperator`, so `17.1.0` adding `icontains` failed to compile rather than letting the operator reach the FilterBuilder as a raw spelling its dropdown cannot select. It maps to the builder's `containsCaseInsensitive` — the id that authors the spec's `$icontains` — and deliberately not to `contains`, which would quietly rewrite a case-insensitive filter into a case-sensitive one the next time the view was saved.
1460
+
1461
+ **The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes.
1462
+
1463
+ **The console eager-closure ceiling is re-baselined, by maintainer ruling.** The release is roughly 930 KB larger uncompressed and nearly all of it lands in `vendor-objectstack-*.js`, which put the closure past a ceiling that was deliberately sized to catch a 89 KiB regression — the gate refused the bump, correctly. Raising it was escalated rather than taken locally, because gate-strength policy had been ruled the maintainer's; the ruling on objectui#5531 authorised the raise. `MAX_EAGER_CLOSURE_GZIP_BYTES` and the `BASELINE` it is derived from move together in one commit, keeping headroom at 2.00% and below the 91,136-byte regression size the gate must still catch. The gate's *sensitivity* is untouched: a repeat of that regression from the new baseline still fails. No behaviour ships from this file — it is CI policy, recorded here because the version it governs is the one this changeset publishes.
1464
+
1465
+ ### Patch Changes
1466
+
1467
+ - 0427036: On a `gantt` list view, the toolbar's Filter control and the `UserFilters` chips now
1468
+ narrow the chart (objectui#10037).
1469
+
1470
+ Both controls were offered on a gantt view and changed nothing the user could see.
1471
+ `ListView`'s own fetch applies the authored filter, the toolbar's filter group and the
1472
+ chips together, but the gantt chart does not draw those rows: it queries for itself —
1473
+ its registered renderer takes only the node's `schema` — and the node it was handed
1474
+ carried the authored `filter` alone.
1475
+
1476
+ The gantt node now carries the same effective filter `ListView`'s own fetch sends, so the
1477
+ chart's query honours both controls. The value keeps its identity while its content is
1478
+ unchanged, so re-rendering the list does not re-query the chart. The row ceiling from
1479
+ objectui#7210 is untouched.
1480
+ - af243c1: On `tree` and `chart` list views, the toolbar's Filter control and the `UserFilters`
1481
+ chips now narrow the view, and on a `gantt` list view the toolbar's Search box now
1482
+ narrows the chart (objectui#10250).
1483
+
1484
+ These three views run their own query instead of drawing the rows `ListView` fetched.
1485
+ The tree and chart nodes carried only the authored `filter`, and the gantt node carried
1486
+ no search term, so each control changed `ListView`'s own fetch and nothing on screen.
1487
+
1488
+ - The `tree` node and the object-bound `chart` node now carry the same effective filter
1489
+ `ListView`'s own fetch sends, as the `gantt` node has since objectui#10037. The value
1490
+ keeps its identity while its content is unchanged, so re-rendering the list does not
1491
+ re-query the view.
1492
+ - `ObjectGanttSchema` declares two optional keys, `search` and `searchableFields`.
1493
+ `ObjectGantt` sends `search` as `$search` and, only alongside a term,
1494
+ `searchableFields` as `$searchFields` — the pair a list's own query sends. A `gantt`
1495
+ list view writes both from its toolbar Search box and the view's `searchableFields`.
1496
+
1497
+ Unchanged: the toolbar state still does not reach the query of a `chart` view bound to a
1498
+ semantic `dataset`, and the toolbar Search box still does not reach the `tree` or `chart`
1499
+ query. objectui#10326 stops offering that Search box on `tree` and `chart` views.
1500
+ - fb7f38b: fix(plugin-list): a list view whose every authored column is denied by field-level security still sends a `$select`
1501
+
1502
+ `ListView`'s `$select` builder drops the authored columns the principal cannot read. When that left no column, it returned no projection, so the request carried no `$select` key and asked the server for every field of the object, the denied ones included. An emptied column list was read as "no restriction", the reading objectui#7215 fixed on `$expand` (objectui#10275).
1503
+
1504
+ The builder now asks whether the author declared any column, not whether any column survived the permission gate. An authored list that field-level security empties projects to `id` plus the fields the builder already adds through its FLS-gated routes (the platform columns every object carries excepted, as before): the `$expand` roots, the view bindings (kanban, calendar, gallery, timeline and gantt fields), the grid's grouping fields, and the operands of the row predicates (conditional formatting; the `visible` / `disabled` predicates and `recordIdField` of the row actions, the bulk actions and the object's declared actions; the object's `userActions` overrides). A denied operand is not added back. That is the shape `ObjectGrid` and `RelatedList` (objectui#10186) send.
1505
+
1506
+ Graded as defence in depth: ObjectStack's server strips denied fields from every returned row (`FieldMasker`), so nothing leaked from that backend. The projection matters for a backend that does not strip.
1507
+
1508
+ Unchanged: a list with no authored `columns` (absent or an empty array) still sends no projection, a partially-denied list projects its surviving columns as before, and nothing is filtered before the permission answer has loaded.
1509
+
1510
+ Behaviour to know about: on a backend that honours `$select`, a list whose every column is denied now receives rows carrying only the projected fields, as a list with one readable column already did.
1511
+ - 59636b3: On `tree` and `chart` list views, the toolbar no longer offers Search (objectui#10326).
1512
+
1513
+ Both views draw what they query for themselves, and neither query carries a search
1514
+ term: the tree's own `find` sends its filter, its row ceiling and its expansions, and
1515
+ the chart's queries, aggregate or dataset, take no term at all. A term typed there
1516
+ changed `ListView`'s own fetch, behind the record count and the export, and nothing
1517
+ the view drew.
1518
+
1519
+ - The Search control is not rendered while a `tree` or `chart` view is on screen,
1520
+ whatever the chart is bound to. An authored `userActions.search: true` does not bring
1521
+ it back on those views. Grid, kanban, calendar, gallery, timeline, map and gantt views
1522
+ keep it as before.
1523
+ - A term that is already set, whether the host restored it at mount through
1524
+ `initialSearchTerm` or the user typed it on another view before switching, is not
1525
+ applied to `ListView`'s own fetch or export while a `tree` or `chart` view is on
1526
+ screen. The term is kept rather than cleared: switching back to a view that offers
1527
+ Search applies it again, and `onSearchChange` is not called for the switch.
1528
+ - 5e0af5a: `ListView` no longer strands a server-paginated grid on a page a write has emptied. When a refetch comes back with zero rows past page 1 and the server's `total` says the last page is lower, the pager steps back to that last page and fetches it, showing a loading state in between. Before, bulk-deleting every row of the last page (or a concurrent delete) left the list on that page with the first-run "no records yet" empty state, although earlier pages still had records. A write that leaves the current page non-empty keeps the page, and an object with no records still shows the first-run empty state on page 1.
1529
+ - 6d1e77c: On a `chart` list view bound to a semantic `dataset`, a user filter that is already set
1530
+ no longer narrows `ListView`'s own fetch (objectui#10512).
1531
+
1532
+ The toolbar withholds the Filter builder and the `userFilters` chips on such a view
1533
+ (objectui#10327), but a filter group the host restored at mount through
1534
+ `initialFilters`, or a group or chip set on another view before switching, was still
1535
+ applied to `ListView`'s own fetch of the object's rows. That fetch still runs on the
1536
+ chart and feeds the record-count bar, so the count was narrowed by a filter nothing on
1537
+ screen showed or could clear.
1538
+
1539
+ - While a dataset-bound chart is on screen, `ListView`'s fetch and its export apply
1540
+ the view's own `filter` only; the toolbar's filter group and the chips are not sent.
1541
+ - The held state is kept rather than cleared: switching back to a view that offers the
1542
+ controls applies the same group again, and `onFilterChange` is not called for the
1543
+ switch.
1544
+ - A chart bound to the list object, and every other view, apply the toolbar's filter
1545
+ group and the chips as before. A switch into a dataset chart with no user filter set
1546
+ issues no new request.
1547
+ - dddbc27: fix(plugin-list): list filters label a lookup's values by the `displayField` the object definition declares (objectui#10545)
1548
+
1549
+ Once a list view's object definition loads, the filter builder's candidates and
1550
+ the toolbar's lookup filter read each lookup field's display field off that
1551
+ definition. They read `display_field`, then `reference_field`, and never
1552
+ `displayField`, the only one of the three `@objectstack/spec`'s `FieldSchema`
1553
+ declares. So a spec-compliant lookup's display field never reached the value
1554
+ picker, which fell back to its default, `name`.
1555
+
1556
+ Both now read `displayField`, and only it. This is the same single spelling
1557
+ `@object-ui/plugin-grid` reads off a definition. `FieldSchema` refuses the other
1558
+ two: `display_field` with a rename to `displayField`, and `reference_field` with
1559
+ a pointer to `referenceVia`.
1560
+
1561
+ Behaviour change: a field definition that carries only the refused
1562
+ `display_field` no longer sets the picker's display field, and neither does one
1563
+ that carries only `reference_field`. The picker falls back to `name`. A stored
1564
+ definition served through the ObjectStack adapter still works, because that
1565
+ adapter folds `display_field` onto `displayField` when the schema is loaded.
1566
+ A definition that reaches the list any other way, such as a host `DataSource`
1567
+ or an `objectDef` passed straight to `UserFilters`, needs `displayField`.
1568
+
1569
+ The two readers also stop reading `id_field` as the lookup's id column.
1570
+ `FieldSchema` declares no id column for a lookup and refuses both `id_field` and
1571
+ `idField`, so the picker now always keys a lookup by `id`, its default. A
1572
+ definition that carried `id_field` used to make the picker match and emit that
1573
+ column's values instead.
1574
+
1575
+ Also (objectui#10547): before a list view's object definition loads, the filter
1576
+ builder's candidates come from the view's declared `columns`, and that fallback
1577
+ no longer reads `options` off a list column. `ListColumnSchema` refuses the key
1578
+ with `unrecognized_keys`, so a spec-compliant view never carries it. A select
1579
+ field's options now reach the filter builder only from the object definition,
1580
+ once it loads, which is where they already came from after the load.
1581
+ - 4a3d500: `list-view` re-reads its rows when the data-invalidation bus (`notifyDataChanged` from `@object-ui/react`) reports a change to the object it queries (objectui#10572). A write that bypasses the data source — a page action over raw HTTP, a flow, a server action — fires no `onMutation`, so a list on a page used to show it only when the whole page was remounted. The re-read keeps the list mounted; rows a host hands the list inline are still the host's to refresh.
1582
+ - 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.
1583
+
1584
+ (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.)
1585
+
1586
+ 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.
1587
+
1588
+ `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.
1589
+
1590
+ `@object-ui/i18n`: new keys `chart.refreshing` and `dashboard.refreshing` in every built-in locale pack.
1591
+ - 0c3f70a: A directly authored `object-grid` or `list-view` node resolves the spec's context tokens in its own filter (objectui#10607).
1592
+
1593
+ `filter: [['owner', '=', '{current_user_id}']]` on an `object-grid` or `list-view` node authored
1594
+ straight into a page, not reached through `object-view`, went out on `$filter` as the literal
1595
+ token: neither package called `@object-ui/core`'s `resolveFilterPlaceholders`. Each now resolves
1596
+ the node's own filter once through that shared resolver, with the session scope the host provides
1597
+ through `useFilterScope` (the console shell mounts `FilterScopeProvider` from the signed-in user
1598
+ and the active organization). On `object-grid` that is `filter` and its deprecated alias
1599
+ `defaultFilters`: the resolved `filter` feeds the grid's query and its server-side export, and the
1600
+ resolved alias feeds the query when `filter` is absent. On
1601
+ `list-view` it is `filter`, and the resolved value feeds the list's query, the child view it
1602
+ renders, the self-querying views (gantt, tree, chart), the export and the empty-state copy.
1603
+ `object-view` has done this since objectui#10506; these two nodes, authored directly, had not.
1604
+
1605
+ The relative-date macros (`{today}`, `{current_quarter_start}`, …) resolve in the same call, in
1606
+ the browser's local time, as on the other surfaces that call the resolver. Where they used to
1607
+ reach a backend that resolves them itself, as the ObjectStack server does in the tenant's
1608
+ configured `localization.timezone` (UTC by default), a browser whose local day differs from the
1609
+ server's day can now get a different day.
1610
+
1611
+ A token the scope cannot resolve is left as written and the resolver warns once, which is the
1612
+ resolver's own rule, so the filter is never widened.
1613
+
1614
+ The resolved value is held against its inputs: the authored filter, compared by structure, and the
1615
+ scope's three members (`currentUserId`, `currentOrgId`, `onUnresolved`), each compared on its own.
1616
+ A re-render that changes none of them does not refetch, including one that rebuilds an equal
1617
+ filter inline; a new signed-in user refetches with the new id. Date macros are therefore resolved
1618
+ once and held until the scope or the authored filter changes, and a refetch for another reason
1619
+ reuses the held value. `object-view` already hands these nodes a filter it has resolved
1620
+ (objectui#10506); resolving that value again changes nothing, because a resolved id or date no
1621
+ longer matches the placeholder pattern.
1622
+
1623
+ On `object-grid`, the load effect now also refetches when the resolved `defaultFilters` changes (a
1624
+ new signed-in user, or an in-place edit of the alias). It read that alias without keying on it
1625
+ before.
1626
+ - 02e6d36: Five more page blocks re-read when the data-invalidation bus (`notifyDataChanged` from `@object-ui/react`) reports a write to the object they read (objectui#10623). These blocks read neither `onMutation` nor the bus, so any write — through the data source or past it (a page action over raw HTTP, a flow, a server action) — showed only when something remounted them. Each fetch effect now names the `useDataInvalidation` nonce for its object; an unscoped change (`'*'`) matches too, and a change to another object does not.
1627
+
1628
+ - `@object-ui/plugin-timeline`: `object-timeline` re-reads its rows; a canvas that already shows rows stays mounted while the re-read runs.
1629
+ - `@object-ui/plugin-list`: `object-gallery` re-reads its cards; collapsed groups stay collapsed.
1630
+ - `@object-ui/plugin-map`: `object-map` re-reads the markers it is showing without its loading placeholder, so the map stays mounted and the camera stays where the user left it. If that re-read fails, the last markers stay on the map and the failure is logged to the console, as `ObjectGantt`'s background refresh does. A changed query still shows the placeholder and re-fits the camera, even when a bus event arrives with it. Two older faults in the same fetch are fixed as well: a slow earlier answer can no longer overwrite a newer one, and a load that succeeds now takes the map off an earlier error screen.
1631
+ - `@object-ui/components`: `element:number` re-reads its aggregate and `element:repeater` its rows.
1632
+
1633
+ Rows a host hands these blocks (`data`, `bind`, authored `items`, an inline `value` set) are still the host's to refresh; only the blocks' own queries follow the bus.
1634
+ - 12809a5: `ListView` hands its grid the object's field types together with the rows, so the grid draws a `password` / `secret` column masked from its first paint (objectui#10657, which folded objectui#10706).
1635
+
1636
+ `ListView` fetches the rows and hands them to `object-grid` as `data`, and those rows
1637
+ painted before the grid's own read of the object definition settled. In that window an
1638
+ untyped column over a `password` / `secret` field had no type: it drew the raw value, and
1639
+ since `object-grid` now withholds such a column instead, it would draw the mask for every
1640
+ untyped column until the read lands. `ListView` reads the object definition before it
1641
+ fetches the rows, so it now passes the definition's fields to the grid as `objectFields`
1642
+ with them, and the grid has no window at all. Only the grid view is handed it. When the
1643
+ definition could not be read, nothing is passed and the grid withholds on its own.
1644
+ - f9c06ef: Every data node that sends its own authored `filter` into a query now resolves the spec's context tokens first (objectui#10666). With `filter: [['owner', '=', '{current_user_id}']]` these nodes used to send the literal token; they now send the signed-in user's id, and `{current_org_id}` resolves to the active organization:
1645
+
1646
+ - `object-calendar`, `object-map` and `object-tree`, on both the object query and the inline (`provider: 'value'`) query;
1647
+ - `object-gantt`, `object-kanban` and `object-timeline`;
1648
+ - `record:related_list` and `record:line_items`, where the node's own filter is combined with the parent-record condition;
1649
+ - the `element:repeater`, `element:number` (both the `aggregate` filter and the `find` fallback) and `element:record_picker` page elements.
1650
+
1651
+ `list-view`, `object-grid` and `object-gallery` already did this, and are unchanged.
1652
+
1653
+ **New in `@object-ui/react`: `useResolvedFilter(filter, scope)`.** Pass it a node's authored filter and `useFilterScope()`. It resolves the filter through `@object-ui/core`'s shared `resolveFilterPlaceholders` and holds the result: re-rendering with an equal filter (even one rebuilt inline on every render) hands back the same value, so a fetch effect keyed on it does not run again, while a structurally different filter or a change of signed-in user or organization resolves again. A filter with no token to resolve is handed back as the value you passed in. The hold is the one `list-view` and `object-gallery` used privately; `plugin-list` now imports it from here, and no second copy remains in that package.
1654
+
1655
+ What changes for an app:
1656
+
1657
+ - A filter with no placeholders reaches the query as before, unchanged.
1658
+ - Nodes that re-queried whenever the host rebuilt an equal filter inline (`object-calendar`, `object-gantt`, `object-kanban`, `object-map`, `object-tree`) no longer issue those redundant queries.
1659
+ - Date macros such as `{today}` in these nodes' own filters are now resolved in the browser's local time, as they already are for `list-view`, `object-grid` and `object-view`. Before, they reached the server as literals, and the server resolved them in the tenant's time zone.
1660
+
1661
+ `@object-ui/types`: the `filter` docblocks on `ObjectMapSchema`, `ObjectTreeSchema`, `ObjectGanttSchema`, `ObjectCalendarSchema`, `ObjectKanbanSchema`, `ObjectChartSchema` and `ObjectGallerySchema` (and the matching zod descriptions) no longer say the filter is forwarded verbatim; they say its context tokens are resolved first. No type changes.
1662
+ - c610990: A directly authored `object-gallery` node now resolves the spec's context tokens in its own `filter` (objectui#10666). With `filter: [['owner', '=', '{current_user_id}']]` the gallery used to send the literal token on `$filter`; it now sends the signed-in user's id, and `{current_org_id}` resolves to the active organization. The filter goes through `@object-ui/core`'s shared `resolveFilterPlaceholders`, against the scope `FilterScopeProvider` supplies, the same call `list-view` and `object-grid` have made since objectui#10607. The resolved value is held and compared by structure, so re-rendering with an equal filter issues no extra query, and a change of signed-in user queries again. A filter without placeholders reaches the query unchanged. A gallery rendered as a `list-view` child was already handed a resolved filter, and nothing changes for it.
1663
+
1664
+ Date macros such as `{today}` in a directly authored gallery's filter are now resolved in the browser's local time, as they already are for `list-view`, `object-grid` and `object-view`. Before this change they reached the server as literals, and the server resolved them in the tenant's time zone.
1665
+ - be0ad00: `list-view` re-reads its rows when a `conditionalFormatting` rule, a row action def or a bulk action def adds a field its predicates read (objectui#10689).
1666
+
1667
+ The list puts each predicate operand of those three into `$select`, but its fetch effect did not re-run when any of them changed. So a rule added to a mounted list never had its field fetched, and the rule never matched. The effect now keys on the operand names by content, so an equal value in a new array, or a change the query cannot see such as a rule's colour, does not re-read.
1668
+ - 256b4c9: A filter refused as malformed (`INVALID_FILTER`) no longer throws out of render or out of a click
1669
+ handler at the sites that merge filters there. That holds whether this layer's filter converter
1670
+ refused it or, at the drill seam, the spec's own `parseFilterAST`. Each site now surfaces the
1671
+ refusal the way its siblings already do (objectui#10789). The filter stays **refused**: nothing
1672
+ that was refused is accepted, and no site falls back to "no filter".
1673
+
1674
+ - **`ElementDataSourceGate` / `useElementDataSource`** (`@object-ui/react`): the gate's merge of
1675
+ the component's `filter` with the composed binding, and the merge of a saved view's filter
1676
+ with the binding's own, lowered through the throwing converter inside render. A refused
1677
+ filter from either now makes the binding `missing` with a new optional `filterRefusal` on the
1678
+ result (`error` carries its message). The gate then draws, in place of the block, the
1679
+ malformed-filter notice the wrapped blocks draw: `view.malformedFilter`, naming the refused
1680
+ operator or field. `element:record_picker`, which reads the same hook, shows its
1681
+ configuration-error panel with the refusal.
1682
+ - **`PeoplePicker` / `RecordPickerDialog`** (`@object-ui/fields`): the recents merge and the
1683
+ dialog's `mergedFilter` lower through `toFilterNodeSafely`; a refusal is shown in the picker's
1684
+ existing error state and no query runs without the filter.
1685
+ - **Drill-downs** (`ObjectChart`, `ObjectPivotTable`, `DatasetWidget`, `DatasetReportRenderer`):
1686
+ a widget or report filter refused at the drill seam made the drill click throw. The converter
1687
+ refuses some (a spec `$not`, say). The spec's `parseFilterAST` refuses others, such as a scalar
1688
+ on a list operator in either dialect: `[['stage', 'in', 'won']]` or `{ stage: { $in: 'won' } }`.
1689
+ The drill is now not opened or emitted (never with the scope dropped), and the refusal is logged
1690
+ with `console.warn`, naming the operator. `composeDrillFilter` (`@object-ui/core`) now throws
1691
+ one refusal type: the spec's `INVALID_FILTER` error is re-raised as a `FilterOperatorError` with
1692
+ its message and the same `INVALID_FILTER` / 400 envelope, and any other error passes through
1693
+ unchanged.
1694
+ - **`ListView` export** (`@object-ui/plugin-list`): the server export builds its filter inside
1695
+ its `try`, so a refusal is named in the export menu instead of escaping the click.
1696
+ - **`convertFiltersToAST`** (`@object-ui/core`): an `$or` member that is the TRUE identity no
1697
+ longer stops the members after it from being read. `{ $or: [{}, { a: {} }] }` is now refused
1698
+ like `{ $or: [{ a: {} }, {}] }` already was, instead of answering TRUE (every row); a TRUE
1699
+ disjunct beside members that lower still absorbs its `$or`.
1700
+ - d0ae5d0: fix(plugin-list,plugin-grid): a grouped list view under a toolbar search groups on the server, and each group counts all its matching rows
1701
+
1702
+ A grouped list view whose data source answers the group header query hands
1703
+ the grid its own fetch, so the groups and their counts come from the server.
1704
+ A toolbar search used to take that back: the list view fetched one page of
1705
+ searched rows and the grid grouped that page in the browser, so once the
1706
+ matches outnumbered the page a group's count was the page's slice of it, and
1707
+ a group whose matches all fell past the page was missing.
1708
+
1709
+ The list view now hands the grid its toolbar term (the grid's `search` prop)
1710
+ and the view's `searchableFields`, and the grid sends them on the group header
1711
+ query and on every group's row query. A searched grouped list view shows only
1712
+ the groups that hold matching rows, each counting all its matches, and
1713
+ clearing the search restores the unsearched groups.
1714
+
1715
+ `ObjectGrid` now reads a host's `search` whenever one is passed, not only
1716
+ under host-driven paging: the host then owns the term, and the grid's own
1717
+ queries carry it. Turning grouping back on no longer asks for, or briefly
1718
+ shows, the groups of the query the grid held when it last grouped (a cleared
1719
+ search, or an old filter or sort). `@object-ui/plugin-grid` now declares
1720
+ `@objectstack/spec` `^17.5.0`, the release whose group header query takes a
1721
+ search.
1722
+ - ae582b7: A number field's authored `useGrouping` now decides whether its value renders with thousands
1723
+ separators (objectui#11026). This is the renderer half of `FieldSchema.useGrouping`, the
1724
+ digit-grouping hint `@objectstack/spec` declares on a field.
1725
+
1726
+ **What an author now controls.** Write `useGrouping` on a `number` field:
1727
+
1728
+ - `useGrouping: false` renders the value with no separators, whatever its `scale`: a
1729
+ `scale: 2` code field reads `12345.50`, not `12,345.50`.
1730
+ - `useGrouping: true` groups it, even at `scale: 0`: a scale-0 headcount reads `2,026`, not
1731
+ `2026`. An authored `true` reaches `Intl.NumberFormat` as `useGrouping: true`, which means
1732
+ "always". So in a locale that leaves a four-digit number alone by default (es-ES, pl-PL),
1733
+ `1234` renders `1.234` / `1 234`.
1734
+ - Leave it out and nothing changes: a declared `scale: 0` still reads as an ordinal and renders
1735
+ ungrouped (a year reads `2026`), and any other number is grouped the locale's way.
1736
+
1737
+ Before this change nothing in the console read the key, so the only way to drop the separators
1738
+ was `scale: 0`, and there was no way to keep them on a scale-0 count.
1739
+
1740
+ **Where it applies.** List and grid cells, record detail sections and highlights, the record
1741
+ quick-look panel, related-record tables and gallery cards. Each of these builds a cell's field
1742
+ from the object's field definition key by key, and each now copies `useGrouping` next to
1743
+ `scale`. Kanban cards and the grid's auto-generated columns hand the cell the whole field
1744
+ definition, so they follow it too. The form's number input and the dashboard's record table
1745
+ read neither `scale` nor `useGrouping` for grouping, and are unchanged.
1746
+
1747
+ **API (additive).**
1748
+
1749
+ - `@object-ui/core`: `DisplayNumberFormatOptions` gains `useGrouping?: boolean`, and
1750
+ `shouldGroupDisplayNumber` takes it as an optional third parameter. An authored boolean
1751
+ answers first; with it unset, the existing `scale`/`currency` rules decide as before.
1752
+ `@object-ui/i18n` re-exports both names, so the same options reach it too.
1753
+ - `@object-ui/types`: `NumberFieldMetadata` declares `useGrouping`, typed from the spec's
1754
+ `FieldSchema.useGrouping`.
1755
+ - 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不是前端的项目吗?后端的元数据只要对,前端按协议执行就行了呀」).
1756
+
1757
+ - **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.
1758
+ - **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.
1759
+ - **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`.
1760
+ - **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.
1761
+
1762
+ `@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.
1763
+
1764
+ ## ⚠️ BREAKING for a host that hands `reference_to` to the widgets directly
1765
+
1766
+ The type change is a compile error for TypeScript that writes `reference_to` on `LookupFieldMetadata`, `MasterDetailFieldMetadata` or `DetailViewField`: rename it to `reference`.
1767
+
1768
+ 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' }`:
1769
+
1770
+ - before this change, opening the picker queried `account` and the cell resolved the record's name;
1771
+ - 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.
1772
+
1773
+ 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.
1774
+
1775
+ ⚠️ **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.
1776
+
1777
+ - "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`.
1778
+ - 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.
1779
+
1780
+ The text above is kept as the reading of this change.
1781
+ - 81f8498: objectui now resolves `@objectstack/*` 17.5.0 and `zod` 4.6.5, and follows every contract move that release makes (objectui#11073). `@objectstack/spec` 17.5.0 and `@objectstack/core` 17.5.0 require `zod ^4.6.1`, so objectui's own `zod` resolves to the same 4.6.5: two zod minors do not type-check against each other.
1782
+
1783
+ ⚠️ Breaking in places, marked `minor` under this repo's version-alignment rule (a `major` in the fixed group would move all of it off the `@objectstack` major). Every narrowing below is the spec's own, and already reaches any consumer that resolves `@objectstack/spec ^17.5`.
1784
+
1785
+ - `@object-ui/types`: declares `@objectstack/spec ^17.5.0` and `zod ^4.6.1`. A named list view's `pageName` and `tabs` are retired, as the spec retired them: typed `never` and refused by name at parse. `DashboardWidgetSchema` attaches the spec's two new checks (`checkDashboardWidgetStageOrder`, `checkDashboardWidgetMetricMeasureArity`), so a widget the spec refuses is refused here too. Where a strict object is an arm of a plain union, its unknown-key refusal is terminal again (the spec's own mechanism, needed under zod 4.6): the union answers with every arm instead of the one that failed only on unknown keys. That changes the error shape, never the verdict (measured on every objectui union). Two by-name named-view checks that can no longer run are retired; the spec's refusal answers those documents.
1786
+ - `@object-ui/core`: declares `@objectstack/spec ^17.5.0`. `page` leaves the list-view kinds the normalizer knows, as the spec removed `type: 'page'`. `isRefusedTextComparand` and `textComparandRefusalReason` are re-exported from `@objectstack/spec/data`, whose copies are byte-identical. `SPEC_ACTION_KEYS` lists `execution`, which `ActionSchema` now declares.
1787
+ - `@object-ui/data-objectstack`: declares `@objectstack/spec ^17.5.0`, up from `^17.4.0`. Its bundle carries `@object-ui/core`'s filter converter, which now takes `isRefusedTextComparand` and `textComparandRefusalReason` from `@objectstack/spec/data`. `@objectstack/spec` 17.4.0 exports neither, so the old range admitted a spec under which the shipped module names exports that are not there (objectui#5793's floor gate). Nothing moves at runtime in this repository, because the lockfile already resolves 17.5.0. `patch`, as the floor raise of objectui#10864 was.
1788
+ - `@object-ui/fields`: the percent and number `scale` clamp is gone, at the sunset its own test named. The spec now refuses a `scale` above 100 at the declaration, so the width a formatter cannot render no longer arrives, and every face passes the declared width through as it did before the clamp.
1789
+ - `@object-ui/components`: two module-local prop interfaces of the action renderers are renamed (`ActionButtonRendererProps`, `ActionIconRendererProps`), because the spec now exports the old names for its authored props. Neither is reachable through the package's exports map; only the shipped per-file declarations change.
1790
+ - `@object-ui/plugin-list`: the README's view-type example no longer lists the retired `page` kind.
1791
+ - `@object-ui/app-shell`: the flow designer follows 17.5.0 (maintainer ruling on objectui#11088, decision 2). A new `wait` node is seeded `waitEventConfig: { eventType: 'timer', timerDuration: 'PT1H' }`, because the spec now refuses a timer wait with no duration; the inspector shows the Duration at once and it can be changed. The decision form offers the spec's new `mode` (exclusive or inclusive), with no default declared because the spec applies none. The screen form's `mode` states `create` again, because the spec now applies that default. `minor` because the decision form gains a control.
1792
+ - `@object-ui/app-shell`: declares `@objectstack/spec ^17.5.0`, up from `^17.4.0`, and `zod ^4.6.1`, up from `^4.4.3`. The designer now states 17.5.0 behaviour. The screen form's declared `create` default equals a default the spec applies only since 17.5.0, and objectui#9109 requires the two to be equal, so a range that admitted 17.4.0 claimed more than the designer holds to. The resolved versions do not move.
1793
+ - `@object-ui/console`: the bundle inlines the 17.5.0 packages, so its client-side validation answers as the published 17.5.0 contract does. The flow preview sample's script nodes use the 17.5.0 script contract. The eager-closure budget is re-baselined for the release's growth, under the maintainer ruling on objectui#11088 (decision 1).
1794
+ - b24f93a: A page now renders the accessible name an author declares in its `aria` bag, and the list view and the `record:*` blocks read the same bag through the one shared reader (objectui#11083).
1795
+
1796
+ **The page root (a fix).** The spec's `PageSchema` declares `aria` as `AriaPropsSchema`, and `PageNodeSchema` mirrors it. The page renderer passed its wrapper only DOM attributes, so the `aria` object was dropped and `{ type: 'page', aria: { ariaLabel: 'Orders' } }` rendered no `aria-label` anywhere. The page's root element now carries:
1797
+
1798
+ - `aria-label` from `ariaLabel`. A locale map (`{ en: 'Orders', 'zh-CN': '订单' }`) gives the entry for the display locale (`useDisplayLocale()`);
1799
+ - `aria-describedby` from `ariaDescribedBy`;
1800
+ - `role` from `role`.
1801
+
1802
+ The page adds no default role, and a page that declares no `aria` renders the same DOM as before. The flat `ariaLabel` on the page node still reaches the root as before; when both spellings name the same attribute, the nested `aria` bag wins.
1803
+
1804
+ **The list view and the `record:*` blocks (no visible change).** `ListView`'s list region and the `record:*` blocks' `useRecordAriaProps` used to map the nested bag themselves. Both now go through `resolveInlineAriaProps` from `@object-ui/react`, the reader the `element:*` blocks use, and keep only their own defaults: the list region's `region` role, and each block's default role, default name and the report of the refused `aria.label` spelling. One difference: an authored empty `role: ''` on a list view used to render an empty `role` attribute, and now falls back to `region`. The list view still reads `aria.live`, which objectui's `ListViewSchema` declares on top of the spec's bag.
1805
+ - 6158e4c: An `object-grid` publishes `description` and `emptyState` now that `@objectstack/spec` 17.6.0
1806
+ declares them on its `object-grid` row, and `emptyState.title` / `.message` take an inline
1807
+ locale map on both the grid and the list view (objectui#11227).
1808
+
1809
+ **`@object-ui/plugin-grid` (feature and fix).**
1810
+
1811
+ - `description` and `emptyState` are in the grid's declared inputs, so the designer panel, the
1812
+ component manifest and the generated `sdui-intrinsics.d.ts` offer them, and the SDUI parser
1813
+ no longer reports an authored `emptyState` as `unknown-prop`. `description` declares both
1814
+ arms of the `I18nLabel` union, so a locale map on it is not a `type-mismatch`.
1815
+ - `emptyState.title` and `emptyState.message` are resolved against the display locale, as
1816
+ `label` and `description` are. Before, a locale map on either reached the empty-state
1817
+ component as an object, and the grid failed to render. A map with no usable entry keeps
1818
+ that member's default: the table's "No results found" heading, and no message line.
1819
+ - `keyboardNavigation`, the row's third 17.6.0 key, is not published. The spec marks it
1820
+ `[EXPERIMENTAL — not enforced]`, and nothing in the grid reads it yet.
1821
+
1822
+ **`@object-ui/plugin-list` (fix).** A list view's authored `emptyState.title` and
1823
+ `emptyState.message` are resolved against the display locale. Before, a locale map on either
1824
+ was treated as absent, and the empty state drew the default copy in every locale. A plain
1825
+ string is drawn as before.
1826
+
1827
+ **`@object-ui/types` (breaking for a reader of the text members, hence `minor`).**
1828
+
1829
+ - `ObjectGridSchema.emptyState` is the spec's `EmptyState`, and the Zod twin takes the spec's
1830
+ `EmptyStateSchema` by reference. `title` and `message` are `string | I18nLabel`, `icon` stays
1831
+ a string, and an unknown member is still refused by name. A locale map on `title` or
1832
+ `message` now parses, and a value that is neither a string nor a map is refused at that
1833
+ member.
1834
+ - `NamedListView.emptyState` is `ListViewSchema['emptyState']`, the same spec type.
1835
+ - TypeScript code that reads `emptyState.title` or `.message` as a `string` no longer
1836
+ type-checks. Resolve the value first, for example with `resolveI18nLabel` from
1837
+ `@objectstack/spec/ui`.
1838
+
1839
+ **Correction, 2026-10-03 (objectui#11068, the `keyboardNavigation` build).** The bullet above
1840
+ that says `keyboardNavigation` "is not published" and that "nothing in the grid reads it yet" is
1841
+ superseded. The grid honours the key, with arrow-key cell navigation that is on by default when
1842
+ the grid renders editable, and it is in the grid's declared inputs, so the designer panel, the
1843
+ component manifest and the generated `sdui-intrinsics.d.ts` offer it
1844
+ (`.changeset/11068-keyboard-navigation.md`).
1845
+ - 4a1adb7: The list view's record-count bar reads the right noun form in every language at every count (objectui#11445): it passes `count` to the `list.recordCount` count family instead of choosing `list.recordCountOne` on `=== 1`, so Russian reads `2 записи` and Arabic uses its dual at 2. `LIST_DEFAULT_TRANSLATIONS` carries the family's `_one` / `_other` rows.
1846
+ - f560ded: The gallery card's field bag now carries the field's `max`, the storage
1847
+ statement the percent cell reads through the spec's `percentScaleOf`
1848
+ (objectui#11475). Without it, a whole-stored `50` (`max: 100`) would read
1849
+ `5000%` on a card beside `50%` in the grid.
1850
+ - 9801765: The selection bar's built-in **Delete** now honours `userActions.delete.visibleWhen`
1851
+ per selected record (objectui#4420). It used to read that key as a bare boolean — the
1852
+ object-level verdict only — so ticking a record the author's predicate excludes still
1853
+ offered the red Delete, and pressing it deleted the record the predicate was written to
1854
+ protect. The row kebab on the same screen hid its Delete correctly, so one declared key
1855
+ meant two different things on two surfaces.
1856
+
1857
+ Ruled by the maintainer on 2026-08-17 (behaviour 1 of the card's three): **filter the
1858
+ operation and report the skipped**. The bar evaluates the predicate once per selected
1859
+ record, the delete runs over the allowed subset, and the excluded records are reported
1860
+ rather than silently dropped. The button itself is never hidden or disabled by the
1861
+ predicate — a mixed selection is not punished for one stray tick — and a selection where
1862
+ every row is excluded is a legible refusal rather than an unexplained absence.
1863
+
1864
+ - `@object-ui/core` gains `partitionRowsByPredicate`, the set-shaped counterpart of
1865
+ `evalRowPredicate`: the fail-closed per-record fold a bulk gate needs, written once.
1866
+ A bulk gate evaluates N records in a loop, which is why it can never be a hook.
1867
+ - `@object-ui/plugin-grid`'s bulk bar routes an excluded selection through
1868
+ `BulkActionDialog`, whose existing `bulk-skipped-notice` slot reports the skipped
1869
+ count; a selection with nothing excluded keeps the consumer's own delete flow
1870
+ untouched. `resolveRowCrudAffordances` now also returns `objectDeletePredicates` —
1871
+ the bulk half of the same predicates, gated on the object verdict rather than on the
1872
+ row `onDelete` wiring. The dialog declines to run over zero records.
1873
+ - `@object-ui/plugin-list`'s non-grid bulk bar (kanban / calendar / gallery / …) filters
1874
+ the built-in `delete` to the eligible subset and states the skipped count inline.
1875
+
1876
+ Custom bulk action ids are untouched: they route through the action runner carrying
1877
+ their own gates. This is a UI affordance — server enforcement was never the leak.
1878
+ - 41b7ce3: **View configuration is explicitly org-wide, and its write path is now gated (objectstack#7494's
1879
+ ruling, maintainer 2026-08-12).** The `sort` / `hiddenFields` / `columnState` / `rowHeight` that a
1880
+ list toolbar persists were never per-user: they are one shared row on the view, so an ordinary user
1881
+ dragging a column or cycling density was re-styling that view for the entire organization. Nothing
1882
+ in the console said so, and nothing stopped it. A per-user scope stays parked (objectstack#7611,
1883
+ v18) and is deliberately not built here — which is precisely why the write has to be gated rather
1884
+ than narrowed: there is no second, private store for it to fall back to.
1885
+
1886
+ `ObjectStackAdapter.updateViewConfig` now refuses when the session's **reported** ADR-0066 capability
1887
+ set does not contain `manage_metadata`, throwing the new `ViewConfigPermissionDeniedError`
1888
+ (`VIEW_CONFIG_PERMISSION_DENIED`, with `isViewConfigPermissionDeniedError` and the
1889
+ `VIEW_CONFIG_CAPABILITY` constant alongside it). The gate is the **first** statement in the method —
1890
+ before `connect()`, before the payload is assembled — so a refused call puts nothing on the wire.
1891
+ It is on the write rather than on the toolbar button on purpose: withholding the affordance would
1892
+ leave the method still accepting the call from anything else holding the adapter, whereas a gate on
1893
+ the write is inherited by every caller, present and future.
1894
+
1895
+ `manage_metadata` is not a newly minted name. It is the capability this repo already treats as
1896
+ metadata-authoring authority — `HomePage`'s `AUTHORING_CAPABILITY`, the one the server itself
1897
+ refuses metadata writes without — and the gated write goes through `client.meta.saveItem`, the very
1898
+ same ADR-0005 metadata door, so this applies the authority the server is already applying instead of
1899
+ inventing a parallel one.
1900
+
1901
+ **Unknown fails open, by doctrine.** A capability set that was never reported (a backend predating
1902
+ ADR-0066, or no permission provider mounted) is not a denial: the server enforces regardless, so a
1903
+ client-side refusal on missing data cannot protect anything and can only break a permitted user. A
1904
+ *reported* empty grant gates strictly. Hosts push the session's capabilities in with the new
1905
+ `setSystemCapabilities`; `ObjectView` wires it from `usePermissions()`.
1906
+
1907
+ The refusal is also **said out loud**. `ObjectView`'s persist path previously swallowed every failure
1908
+ into `console.error`, which for a debounced toggle whose UI has already moved would have left the
1909
+ operator looking at a density they did not get; a denied write now raises a toast. And the "View
1910
+ settings" popover — where density and field visibility are actually changed — now states the scope
1911
+ before the operator acts: *"Grouping, color, density, and visible fields. Applies to everyone who
1912
+ uses this view."*, translated in all ten packs.
1913
+ - 39f4309: Published typings from every `vite-plugin-dts` package now carry an explicit extension on
1914
+ every relative specifier, and a type error in the declaration build now fails the build
1915
+ instead of being printed and ignored (objectui#5439, objectui#5483).
1916
+
1917
+ **Consumers on `moduleResolution: nodenext` or `node16` may see NEW type errors, and that
1918
+ is the fix working.** These packages re-export mostly through NAMED re-exports —
1919
+ `export { useObjectChat } from './useObjectChat'`. TypeScript could not follow the
1920
+ extensionless hop, but it still DECLARED the name, so the symbol resolved to a silent
1921
+ `any`. Nothing errored; consumers simply got no types. With the extension emitted, the
1922
+ symbol carries its real type, and any call site that was relying on the `any` now type
1923
+ checks for the first time. This is the mode that produced the 21 residual `TS7006` on
1924
+ `@object-ui/app-shell` reported against objectui#5365 — a type hole that opened quietly,
1925
+ unlike objectui#5365's own `export * from './ui'` packages where the same defect surfaced
1926
+ immediately as `TS2305: has no exported member`.
1927
+
1928
+ 410 extensionless relative specifiers across 19 packages were emitted before this change;
1929
+ the count is now 0 in all 22 packages that build typings through `vite-plugin-dts`.
1930
+ `@object-ui/fields` was already clean — its sources write explicit `.js` specifiers — and
1931
+ is wired so it stays that way.
1932
+
1933
+ The second half changes no emitted output today: 22/22 packages built green unmodified, so
1934
+ making the declaration step's exit code honest turns nothing red. It changes what a FUTURE
1935
+ regression does — print and exit 0, versus fail the build.
1936
+ - b4393e5: The last three sort-axis consumers read the platform's per-column sortability signal instead
1937
+ of re-deriving it from the field's type (objectui#6108, inheriting objectstack#10235 ruling A
1938
+ through objectui#5729's landed contract). ListView's toolbar sort picker and both of
1939
+ RelatedList's sort entry points — the embedded table's column headers and the `data-list`
1940
+ sort-button row — now go through `isPlatformSortableField`, the same spelling the grid header
1941
+ adopted; their `UNMATERIALIZED_FIELD_TYPES` / `isUnmaterializedFieldType` re-derivations are
1942
+ deleted.
1943
+
1944
+ The re-derivation was not wrong about `formula`: the platform computes its own projection from
1945
+ the same `@objectstack/spec` storage fact, which is why the drift went unnoticed across two
1946
+ cards. It parts company on everything the projection encodes as ABSENCE — an unknown name, a
1947
+ dotted path a caller can put in a related list's `columns`, an unprovisioned audit column —
1948
+ where a type read finds no field definition, answers "sortable", and offers a control the
1949
+ runtime meets with `400 INVALID_SORT`. It parts company again on any refusal that carries no
1950
+ `reason: virtual-type`, and it cannot follow the platform in the other direction either: a
1951
+ field the platform now DOES order by stays withheld forever on its type alone.
1952
+
1953
+ Two behaviours are deliberately unchanged. The relational carve-out stays separate from the
1954
+ signal — the projection answers `sortable: true` for a `lookup` because the platform can order
1955
+ by the stored foreign key, while the UI withholds because that order means nothing beside a
1956
+ column of names — so a relational column does not get its sort back. And ListView's picker
1957
+ still lists a field the CURRENT sort already names, which is the only way to remove a sort the
1958
+ server refuses outright; that exception now covers platform-refused fields, not just formulas.
1959
+
1960
+ A deployment that served no `sortability` key at all is a different case from "nothing is
1961
+ sortable": that branch keeps the type read as a compatibility floor, so behaviour on a backend
1962
+ older than objectstack#10235 (or an inline/mock data source) is byte-identical to before.
1963
+ - bd2f56a: `ListView`'s toolbar sort picker no longer persists a sort the platform refuses to order by.
1964
+
1965
+ The picker keeps a platform-refused field listed while the CURRENT sort names it
1966
+ (#6108). That exception is deliberate and stays: it is the only way a user can
1967
+ REMOVE a sort the server answers `400 INVALID_SORT` for — withholding the option
1968
+ unconditionally renders a blank row nobody can delete, and drops the sort silently
1969
+ on the next edit.
1970
+
1971
+ What was wrong is that the picker rendered and emitted from the same array. Editing
1972
+ anything ELSE in that popover — adding a second sort key, resetting to the view's
1973
+ default — re-emitted the whole array with the refused entry still in it, and the
1974
+ host's `onSortChange` turned that into `persistViewPatch({ sort })`: a
1975
+ personalization PUT storing a refused column, written by a user who never touched
1976
+ that row. A view stored before the sortability signal existed therefore kept
1977
+ re-persisting its refused `$orderby` indefinitely.
1978
+
1979
+ Every `onSortChange` this component emits — the builder, the column-header sort and
1980
+ "reset to default" — now crosses one boundary that drops what the served projection
1981
+ refuses, while `currentSort` keeps the array whole. So what the picker LISTS and
1982
+ what it PERSISTS are separate: the refused entry stays visible and removable,
1983
+ removing it persists the removal, and no write carries it. This is the separation
1984
+ #5729 already made at the grid seam (`ObjectGrid`'s `manualSort` /
1985
+ `manualOnSortChange` pair); the picker was the second door onto the same stored
1986
+ view state.
1987
+
1988
+ Only under a served sortability projection (objectstack#10235 ruling A). `undefined`
1989
+ means NO SIGNAL SERVED — an older deployment, an inline/mock data source — not
1990
+ "nothing is sortable", and that branch is byte-identical in behaviour to before.
1991
+ - babe956: `list-view` stops spelling "the author declared no columns" as an explicit empty
1992
+ projection (objectui#6598).
1993
+
1994
+ A production `kind:'html'` page carried `<list-view objectName="opportunity">`
1995
+ with no `columns` and rendered the row count, the filter/group/sort toolbar and
1996
+ the index column — and **not one data column**, with no diagnostic anywhere.
1997
+ `ObjectGrid` derives default columns for exactly that case ("Default columns
1998
+ priority (when schema doesn't specify columns)"), and it never ran: the
1999
+ derivation is gated on `schema.fields` being ABSENT, `ListView` sent
2000
+ `fields: []`, and an empty array is truthy. `normalizeColumns` had already read
2001
+ the empty `columns` as unauthored, so the two keys disagreed about the same fact
2002
+ and the stricter reading won.
2003
+
2004
+ `ListView` now asks whether the AUTHOR declared a projection — `columns` present
2005
+ and non-empty, after the legacy `fields` fold — and hands the child grid nothing
2006
+ at all when they did not, so the grid's own defaults apply.
2007
+
2008
+ ⚠️ The predicate reads the authored value and never what survived filtering, and
2009
+ that distinction is load-bearing: when the author DID declare columns and the
2010
+ field gate removed every one of them, the empty projection is still sent.
2011
+ `ObjectGrid` re-applies FLS on its derived column path only, never on the
2012
+ explicit-columns path, so falling through to the derivation there would put
2013
+ fields on screen that the author never asked for and the principal may not read.
2014
+
2015
+ Measured single-variable on the html tier: a bare
2016
+ `<object-grid objectName="opportunity" />` renders the object's default columns;
2017
+ the same object behind `<list-view>` rendered none. Pinned at the handoff
2018
+ (`ListView.unauthoredColumnProjection-6598.test.tsx`) and end to end over the
2019
+ real grid on a real html-kind page
2020
+ (`htmlTierListViewDefaultColumns-6598.test.tsx`).
2021
+
2022
+ This is one half of the reported symptom. Which columns the defaults resolve to
2023
+ still depends on who owns the fetch — with a host like `ListView` fetching, the
2024
+ grid takes its inline-data branch and derives from the row payload's keys rather
2025
+ than from the object schema's policy (hidden and readonly system-managed fields
2026
+ dropped, `highlightFields` honoured). That precedence sits in
2027
+ `packages/plugin-grid` and is filed separately.
2028
+ - 3beef6d: The spec's `dataSource` element binding is now DECLARED by the blocks that read
2029
+ it, so the html tier stops reporting the one working saved-view spelling as
2030
+ `unknown-prop` (objectui#6678).
2031
+
2032
+ `PageComponentSchema.dataSource` — `{ object, view, filter, sort, limit }` — is
2033
+ the one spelling that resolves a saved view for an object-bound block. It works,
2034
+ and it drew the identical `unknown-prop` warning as the two spellings that do
2035
+ nothing (`viewName`, `view`), because `validateTree` looks a prop up in the
2036
+ block's declared `inputs` and no registration declared this key. On the tier
2037
+ built to accept AI-authored pages, where the diagnostic IS the contract, the
2038
+ only signal pointed away from the key that works.
2039
+
2040
+ Adopting the maintainer ruling of 2026-08-29 — option B **in the injection
2041
+ form**:
2042
+
2043
+ - `ELEMENT_DATA_SOURCE_INPUT` is the single declaration, in `@object-ui/core`
2044
+ beside the binding's own semantics; `Registry.register` emits it for any
2045
+ registration whose renderer passed through the new `elementDataSourceBlock()`
2046
+ seam. One mechanism, one copy — not a hand-kept declaration per block, which is
2047
+ the shape that drifts and that a new block forgets. The seam lives in
2048
+ `@object-ui/core` and is re-exported by `@object-ui/react` beside
2049
+ `ElementDataSourceGate` for discoverability; call sites take the core import,
2050
+ because a registration runs at module scope and this repo's suites partially
2051
+ mock `@object-ui/react`.
2052
+ - Seventeen renderers, in thirteen files across twelve packages, reach the seam
2053
+ and now publish the key to the save gate, the parser whitelist, the generated
2054
+ JSX authoring types and the block list. The card named nine blocks; the tree
2055
+ also has `plugin-grid`, `plugin-timeline`, two further `plugin-form` blocks and
2056
+ `element:record_picker` — nothing was hand-listed, so the mechanism covered
2057
+ them. `element:record_picker` consumes the gate's HOOK and status panels rather
2058
+ than the wrapper tag (its object lives under `properties`), and was found by a
2059
+ render probe rather than by reading sources.
2060
+ - `dataSource` on a block that does NOT read it (`flex`, `card`) still reports
2061
+ `unknown-prop`. Adding the key to `sdui-parser`'s `BASE_PROPS` was refused for
2062
+ exactly this reason — that set mirrors `BaseSchema`, and silencing the key
2063
+ everywhere would make the diagnostic lie in the other direction.
2064
+ - New `check:element-data-source-declaration` fails any source that consumes the
2065
+ gate without reaching the seam, so a block added tomorrow cannot forget.
2066
+
2067
+ Behaviour of the binding itself is unchanged — this is a declaration, not a
2068
+ resolution change. The saved view still resolves its columns, and an
2069
+ unresolvable `view` still fails loudly rather than widening to the object's full
2070
+ scope.
2071
+
2072
+ The spec/registry parity gates (repo-wide and the `record:related_list` per-block
2073
+ pin) now derive their accepted set from the WHOLE node contract rather than from
2074
+ `ComponentPropsMap[type]` alone. `PageComponentSchema` accepts and keeps
2075
+ `dataSource` on a page-component node — it is a node-level key, a sibling of
2076
+ `type` and `className`, not a per-block prop — so the gates' previous complaint
2077
+ was measurably wrong. Derived from the spec, not exempted, and both still
2078
+ discriminate against an invented key.
2079
+ - 06b8c42: Re-key three more renderer effects onto the primitives they actually read,
2080
+ instead of the memoised object identity that produced them (objectui#6697 —
2081
+ the three census members from objectui#6592 that sit outside its
2082
+ `getDataConfig(schema)` family):
2083
+
2084
+ - `RelatedList`'s collection fetch now depends on `defaultSortKey` /
2085
+ `filterKey` (the `JSON.stringify`-derived content strings the two memos are
2086
+ already keyed on) rather than on `defaultSortSpec` / `listFilterNode`.
2087
+ - `page:tabs`' related-count probe now depends on a serialised `probeKey`
2088
+ rather than on the `probeTargets` `Map`.
2089
+ - `ListView`'s data fetch now depends on the `expandFields` memo's own INPUTS
2090
+ — `schema.columns`, the alternate views' binding blocks and
2091
+ `objectDef?.fields`, all props and state a discard cannot move — rather than
2092
+ on the `expandFields` array the memo returns.
2093
+
2094
+ `useMemo` carries no semantic guarantee — React is permitted to discard a memo
2095
+ cache and recompute even when its dependency array compares equal to the
2096
+ previous render — and all three factories return a FRESH value on every call
2097
+ (`normalizeSortSpec`/`toFilterNode` build a new array / a freshly lowered AST,
2098
+ the probe factory builds a new `Map`, `buildExpandFields` returns a new array
2099
+ in every branch). So each effect re-ran on a discard alone, with nothing an
2100
+ author or a caller controls having changed: an extra `dataSource.find` for the
2101
+ related collection, an extra `dataSource.find` for the list window, and a
2102
+ redundant re-probe of every tab's count. Keying on the primitives makes a
2103
+ cache discard a no-op and returns `useMemo` to being a pure optimisation.
2104
+
2105
+ Severity is low and the fix is deliberately narrow: the observable was a
2106
+ redundant round trip, never incorrect data, so only the re-run condition
2107
+ moves — each effect body still reads the memoised value, and a genuine change
2108
+ still refetches exactly as before.
2109
+
2110
+ The three take two routes on purpose — key on the nearest DISCARD-IMMUNE
2111
+ thing. `RelatedList`'s memos are keyed on exactly one primitive each, and
2112
+ `page:tabs`' probe memo is keyed on another MEMO's output (`items`), which is
2113
+ not discard-immune, so both take a content string. `ListView`'s memo is keyed
2114
+ on props and state, so it names those directly: a value key over
2115
+ `expandFields` would NOT have been content-equivalent there — `buildExpandFields`
2116
+ collapses the collected set down to the relation roots, while the effect body
2117
+ also builds `$select` from `schema.columns` and the view bindings — and it
2118
+ would have defeated objectui#4567's live-dependency pin, which ruled that
2119
+ "ListView's by-identity dependency is correct for a real column change" and
2120
+ put the identity stabilisation at the PRODUCER.
2121
+
2122
+ One correction to the census card's account, measured while pinning it: for
2123
+ `page:tabs` the redundant probe costs nothing on the wire.
2124
+ `RelatedCountStore.fetch` returns the cached count as its first act and dedupes
2125
+ concurrent probes, so the extra work is the effect re-running, not an extra
2126
+ request.
2127
+ - 4dfdcc3: `ListView` reads `exportOptions.streaming` without a cast (objectui#6956). The
2128
+ two `as any` reads — the `exportableFormats` server-availability check and
2129
+ `handleExport`'s server-eligibility gate — and the `'pdf'` in the bare-array
2130
+ fold's cast are gone: the `ListViewSchema` type now carries `streaming` and not
2131
+ `'pdf'`, because `@object-ui/types`' zod mirror binds the spec's `exportOptions`
2132
+ field by reference. No behaviour change: the same formats are offered,
2133
+ `streaming: false` still forces the client-side path, and the bare-array fold
2134
+ (`resolvedExportOptions`, a stored `['csv', 'xlsx']` folded to `{ formats }`)
2135
+ STAYS — nothing on the render path parses and `ObjectView` forwards a stored
2136
+ value verbatim, so the spec's parse-time lift never runs before this renderer
2137
+ and the fold is load-bearing rather than legacy. A `'pdf'` stored before the
2138
+ retirement still arrives as data and is still dropped from the export menu with
2139
+ the existing one-time warning.
2140
+ - d1842ab: `DataEmptyState` now declares `role="status"` by default, so an empty result is
2141
+ distinguishable from a failed one on every surface that renders it
2142
+ (objectui#7132).
2143
+
2144
+ This is the convergence half of the two rulings that landed as objectui#7063 and
2145
+ objectui#7064, both resting on objectstack#13848: uniform behaviour belongs to
2146
+ the platform, and per-surface compensation is the per-app tax being ruled
2147
+ against. Those two fixed their own surfaces deliberately and locally; this card
2148
+ measured whether the shared primitive should carry the property. It did not.
2149
+
2150
+ **Measured, not assumed.** All the surfaces were rendered and their empty boxes
2151
+ read directly:
2152
+
2153
+ | surface | `role` before |
2154
+ |---|---|
2155
+ | `DataEmptyState` bare default | *none* |
2156
+ | `plugin-list` empty list | *none* |
2157
+ | `plugin-list` load-error panel | *none* |
2158
+ | `plugin-detail` activity timelines | *none* |
2159
+ | `ui:empty` schema renderer | *none* |
2160
+ | `plugin-dashboard` `WidgetEmptyState` (#7063) | `status`, typed at the call site |
2161
+ | `plugin-kanban` empty board | `status`, typed at the call site |
2162
+
2163
+ The sibling states in the same file had always declared themselves —
2164
+ `DataLoadingState` is `role="status"`, `DataErrorState` is `role="alert"` — and
2165
+ the empty state alone declared nothing. So the surfaces were not legitimately
2166
+ differing: the ones that wanted the property had each hand-typed the same line,
2167
+ and the ones that had not yet done so were silently missing it. That is one
2168
+ platform default, copied by hand, at package level.
2169
+
2170
+ **It is a default, not a fixed attribute** — `role` is spread from props, so a
2171
+ call site keeps the last word. That is what makes this inert for the two ruled
2172
+ surfaces: both already pass `role="status"` explicitly and receive the identical
2173
+ attribute with or without it. Neither surface's behaviour changes.
2174
+
2175
+ **One real defect fell out of the measurement.** `plugin-list` renders its load
2176
+ FAILURE through `DataEmptyState`, borrowing it for layout — so a 403 saying "You
2177
+ don't have access" and a young object saying "Nothing here yet" were the same
2178
+ node shape, with no role on either. That panel now declares `role="alert"`,
2179
+ which both fixes the pre-existing indistinguishability and stops the new default
2180
+ from announcing an outage as a routine status.
2181
+
2182
+ Metric/KPI widgets are untouched: their carve-out (`rows.length === 0 &&
2183
+ !isMetric`) gates whether an empty state is rendered *at all*, upstream of this
2184
+ component, so a KPI still reads `0` rather than "no data".
2185
+ - a6d8b8d: Fix: a grid grouped by a field it does not also show as a column no longer collapses
2186
+ every row into one `(empty)` group (objectui#7179).
2187
+
2188
+ `$select` was built from the view's `columns` and nothing else, so a view declaring
2189
+ `grouping: { fields: [{ field: 'business_unit' }] }` on a field absent from its columns
2190
+ never asked the server for that field. It was `undefined` on every row by the time
2191
+ grouping ran, and the grouping label builder — correctly, for a genuinely empty value —
2192
+ answered `(empty)` for all of them. The result was one collapsible group holding every
2193
+ record, with no error, no warning and no empty state: a grid that looked like it grouped
2194
+ and did not, reading as "these records have no value for this field".
2195
+
2196
+ The grouping fields are now unioned into the projection, at both places it is built —
2197
+ `ObjectGrid` when it fetches for itself, and `ListView` when it fetches and hands the
2198
+ rows down. Lookup grouping fields are unioned into `$expand` as well: a `select` that
2199
+ fetches a bare foreign key without populating it buckets by raw id instead of by name,
2200
+ which is a different wrong answer rather than a fix.
2201
+
2202
+ Authors do not need to mirror a grouping field in `columns` any more. That was never
2203
+ required by `@objectstack/spec` — `grouping` is a sibling of `columns`, not a subset of
2204
+ it — and the neighbouring view kinds (kanban, gantt, timeline) already unioned their
2205
+ `groupByField` with no column needed. Refusing the configuration at author time was
2206
+ considered and rejected: it would make the grid the odd one out and reject working
2207
+ intent that the schema explicitly allows.
2208
+
2209
+ The union is guarded, and the guard is as load-bearing as the fix. A `grouping.fields[]`
2210
+ entry carries a bare string that has never been through column validation, and some
2211
+ backends answer an unknown `$select` key with an empty result set rather than ignoring
2212
+ it. Unioned unguarded, a grouping field naming something the object does not declare
2213
+ would have turned this bug into a strictly worse one — no rows at all, equally silently.
2214
+ Grouping fields are therefore intersected with the object's declared fields and passed
2215
+ through the same field-level-security gate as columns and predicate operands before they
2216
+ reach the query.
2217
+ - f626808: fix(app-shell,plugin-list): a list view's own `description` now reaches the screen
2218
+
2219
+ A `description` authored on a per-list-view entry (`listViews.<viewName>.description`)
2220
+ was validated, built and served correctly, then silently never rendered. Two
2221
+ independent cuts, both fixed here:
2222
+
2223
+ - **app-shell** — `ObjectView`'s `renderListView` relay copied ~46 keys off the
2224
+ active view onto the schema it hands `ListView` (`label`, `sort`, `filter`,
2225
+ `hiddenFields`, `inlineEdit`, `color`, `allowExport`, …) but had no rung for
2226
+ `description`, so the renderer could only ever see the object-level list's
2227
+ description and a per-view one was unreachable. It is relayed now, with the
2228
+ same two-rung shape as `label`. This is *not* the object's own
2229
+ `objectDef.description`, which stays the page header's subtitle.
2230
+ - **plugin-list** — `ListView` rendered `typeof description === 'string' ? … : ''`,
2231
+ a type test rather than a resolution. `ListViewSchema.description` is
2232
+ `I18nLabel`, so an inline locale map (`{ en, 'zh-CN' }`) — metadata the spec
2233
+ entitles an author to write — rendered a blank strip in every locale. It now
2234
+ resolves through the same shared helper the sibling `label` uses, and the
2235
+ visibility guard reads the resolved text, so a map with no usable entry drops
2236
+ the strip instead of reserving empty space for it.
2237
+
2238
+ `appearance.showDescription: false` still suppresses the description in both arms.
2239
+ - 3ed3eec: fix(plugin-form,plugin-list,plugin-view,react): read `SchemaRendererContext` as declared, not through a cast to `any` (objectui#7209)
2240
+
2241
+ Six reads of the renderer context erased its type. Five cast at the read —
2242
+ `useContext(SchemaRendererContext as React.Context<any>)` in the
2243
+ `embeddable-form` and `object-master-detail-form` bridges, in `ListViewBlock`
2244
+ and in `useResolvedDataSource`, and the same cast spelled with a bare
2245
+ `Context<any>` in `useElementDataSource` — and the `object-view` renderer's
2246
+ module re-declared the imported context as a `React.Context<any>`. Through
2247
+ that cast a read of a member the context does not declare compiled clean,
2248
+ which is how the phantom `ctx.formValues ?? ctx.data` channel retired by
2249
+ objectui#7206 went unnoticed. Each of these reads now sees `SchemaRendererContextType` as
2250
+ `@object-ui/react` declares it, so such a read is a compile error on the day it
2251
+ is written.
2252
+
2253
+ No runtime behaviour changes. Removing the casts surfaced no read of an
2254
+ undeclared member; it surfaced two places where the declared `null` ("no
2255
+ adapter bound") met a prop that does not admit it:
2256
+
2257
+ - `embeddable-form` handed that `null` to `EmbeddableForm`'s optional
2258
+ `dataSource`. It now collapses it to `undefined`, as
2259
+ `object-master-detail-form` already did; `EmbeddableForm` only ever tests the
2260
+ adapter for truthiness, so the two absences behave the same.
2261
+ - `object-view` / `view` hand it to `ObjectViewProps.dataSource`, which is
2262
+ declared required and stays so (objectui#7842). That one value now carries a
2263
+ narrow, commented assertion instead of the whole context being erased; the
2264
+ value passed is the same as before.
2265
+
2266
+ The published signatures of `useResolvedDataSource` and `useElementDataSource`
2267
+ do not move.
2268
+ - 5015fcf: A gantt list view no longer shows the record-count bar, because the bar describes a
2269
+ request that view does not draw (objectui#7210, half 1).
2270
+
2271
+ `ListView` renders one record-count bar for every `viewType`: the row count, the
2272
+ "Showing first N records. More data may be available." warning that goes with its own
2273
+ `$top: pageSize` query, and a rows-per-page selector that re-issues that query. On grid,
2274
+ kanban, calendar, gallery, timeline and map the bar is accurate — those renderers draw
2275
+ the `data` `ListView` hands down.
2276
+
2277
+ On `gantt` it is not. Measured, not inferred: the registered `object-gantt` renderer
2278
+ forwards no prop but `schema`, so the `data` prop never reaches the chart and
2279
+ `ObjectGantt` issues its own query — one that carries no `$top` at all. In a harness of
2280
+ 18 rows with `pagination.pageSize: 6`, the chart drew all 18 while the bar under it read
2281
+ "6 records · Showing first 6 records. More data may be available." Because it is the
2282
+ only paging disclosure on the screen, a reader takes it as describing the chart; that
2283
+ reading already produced a wrong finding in an application repo, which cost a browser
2284
+ session to disprove. Authoring `pagination.pageSize` could not have fixed it either —
2285
+ the chart's request never carried a page size to begin with.
2286
+
2287
+ Scoped to `gantt` alone. Every other surface keeps its bar unchanged, warning included;
2288
+ a kanban over the same result set still reports its page honestly.
2289
+
2290
+ Not changed here, deliberately: the gantt's query still has no ceiling, and `ListView`
2291
+ still issues its own paged query for that view (its rows still drive the loading
2292
+ skeleton and load-error panel, `UserFilters`' option counts, and the client-side CSV /
2293
+ JSON export). Capping a non-grid view's fetch is an open maintainer decision — adding
2294
+ one would turn a complete schedule into a quietly truncated one — and this correction is
2295
+ right whichever way that lands.
2296
+ - 67dadd6: FLS-gate the `$expand` projection at both build sites (objectui#7215).
2297
+
2298
+ objectui#6898 closed field-level security on `$select`. `$expand` was left ungated at
2299
+ both projection sites — `ObjectGrid`'s own fetch and `ListView`'s `expandFields` memo —
2300
+ so a `lookup` / `master_detail` / `user` / `tree` field the current principal cannot
2301
+ read was still handed to the server for expansion. `$select` on a denied lookup asks for
2302
+ its bare foreign key; `$expand` on the same field asks the server to resolve it and
2303
+ return the related record, so the larger of the two disclosures was the ungated one.
2304
+
2305
+ **Reproduced before it was fixed**, as failing tests at both sites, and the same leak
2306
+ reaches further on the `ListView` path: that builder's `$select` gate drops the denied
2307
+ column and then adds the expand roots back unconditionally, so the denied field walked
2308
+ back into `$select` as well. Gating the expansion closes both halves.
2309
+
2310
+ **Grading, measured rather than assumed.** Against ObjectStack's own server this is
2311
+ defence-in-depth, exactly as objectui#6898 is: `plugin-security`'s
2312
+ `FieldMasker.maskRecord` deletes every unreadable key from each returned row, and
2313
+ objectql's expand path writes the resolved record back under that same key, so one
2314
+ statement removes the expanded object and the bare id alike; the expansion sub-read is
2315
+ itself gated (`__expandRead` takes the referenced object's full CRUD + RLS + FLS
2316
+ treatment). It is load-bearing for any backend that does not strip.
2317
+
2318
+ **Nothing a permitted view did stops working.** The gate judges the OUTPUT of
2319
+ `buildExpandFields`, which is already a subset of the object's declared
2320
+ reference-bearing fields, so the "`checkField` answers false for an undeclared key"
2321
+ trap cannot be reached and derived / host-joined columns are untouched. An unanswered
2322
+ permission policy filters nothing. `buildExpandFields` itself is unchanged.
2323
+ - ac257b3: FLS-gate the speculative half of `ListView`'s `$select` projection (objectui#7216).
2324
+
2325
+ `ListView` builds `$select` from two populations and, until now, asked them different
2326
+ questions. The user-declared `columns` were passed through `perms.checkField(...)` by
2327
+ objectui#6898. Everything the builder adds ON TOP — the kanban / gantt / timeline /
2328
+ calendar / gallery field bindings, the timeline's auto-added `status` / `priority`
2329
+ badge fields, and the operands harvested from row-action and conditional-formatting
2330
+ predicates (objectui#3501) — went through `addSpeculative`, which intersected them
2331
+ against the object's declared fields and then added them unconditionally.
2332
+
2333
+ The two gates it needed answer unrelated questions and neither substitutes for the
2334
+ other. The known-field gate keeps an **unknown** key out, because some backends answer
2335
+ an unknown `$select` key with an empty result set rather than ignoring it. The FLS gate
2336
+ keeps a **known but denied** key out, because sending it leaks the value at the server
2337
+ boundary even though the UI hides it. A field can be perfectly well-declared and still
2338
+ denied, and that was the case this path did not handle: a kanban grouped by a denied
2339
+ field, or a gantt bound to a denied date, still named it in the request.
2340
+
2341
+ **Reproduced before it was fixed.** Eight of fourteen new pins fail on the unmodified
2342
+ tree, each reaching the denied field only through a view binding — a denied *column* has
2343
+ been dropped since objectui#6898, so a pin naming one would have proved nothing.
2344
+
2345
+ The gate goes inside `addSpeculative` rather than at the five call sites, so every route
2346
+ into the speculative union is covered at once; gating call sites one at a time is how
2347
+ the asymmetry arose. It runs **after** the known-field intersection, the ordering
2348
+ objectui#7179 established: `checkField` answers false for an undeclared key, so asking
2349
+ it first would drop derived and computed bindings — and would be the reason they were
2350
+ dropped. The platform record columns (`created_at`, `owner_id`, the audit FKs) are
2351
+ carved out for the reason they already are elsewhere in this builder: every object
2352
+ carries them and none declares them, so no field policy mentions them and an FLS answer
2353
+ about them is always false. Without the carve-out a calendar bound to `created_at` would
2354
+ go blank for everybody.
2355
+
2356
+ **Nothing a permitted view did stops working.** A permitted binding is still projected,
2357
+ an unanswered permission policy filters nothing, and the projection is rebuilt when the
2358
+ policy answers — pinned, because a gate that only runs before `/me/permissions` resolves
2359
+ is a dead gate that passes almost every test written for it.
2360
+
2361
+ objectui#7179's `addGroupingField` wrapper is removed: its predicate was identical to
2362
+ the one now inside `addSpeculative`, and two spellings of one gate is the shape that
2363
+ lets them drift. That path keeps its behaviour and gains the FLS pin it never had.
2364
+ - 2c878be: Honour a gantt view's authored `navigation` — forward it down the gantt
2365
+ view-schema path, and give the component a destination for the non-overlay
2366
+ modes (objectui#7334).
2367
+
2368
+ **Leg 1, `@object-ui/plugin-list`.** `ObjectGantt` resolves
2369
+ `schema.navigation ?? { mode: 'drawer' }` and classifies four overlay modes
2370
+ (`drawer` / `modal` / `split` / `popover`). Nothing ever put `navigation` on the
2371
+ node it receives — `ListView`'s shared `baseProps` declares no such key and the
2372
+ `case 'gantt'` branch added only the data keys — so that fallback was the only
2373
+ branch ever taken. A gantt view authoring `navigation: { mode: 'page' }` (or
2374
+ `modal`, or `split`) opened a drawer instead, with no diagnostic. The key was
2375
+ already authorable and already read; this restores declared = enforced. No new
2376
+ authorable key is added.
2377
+
2378
+ The key is forwarded on the `case 'gantt'` branch and deliberately **not** on
2379
+ the shared `baseProps`, because every other child view resolves this same
2380
+ question through the `onRowClick` that `baseProps` already carries:
2381
+ `ObjectGrid`, `ObjectGallery`, `ObjectKanban`, `ObjectMap`, `ObjectTimeline` and
2382
+ `ObjectTree` hand that callback to `useNavigationOverlay` unconditionally, and
2383
+ the hook gives an external `onRowClick` full priority over any `navigation` — so
2384
+ on `baseProps` the authored config would arrive there only to be outranked.
2385
+ `ObjectCalendar` is the one that would genuinely change behaviour: it mirrors
2386
+ gantt's `navIsOverlay ? undefined : onRowClick`, so an authored non-overlay mode
2387
+ would stop it suppressing the host handler. Gantt is the only branch where
2388
+ forwarding settles the question rather than splitting it, because its wrapper
2389
+ drops host props entirely (objectui#7210 / objectui#7222) and there is no
2390
+ `onRowClick` there to outrank anything.
2391
+
2392
+ **Leg 2, `@object-ui/plugin-gantt`.** `ObjectGantt` now supplies its own
2393
+ `onNavigate`, so a click under an authored `page` or `new_window` actually
2394
+ reaches the record page. Leg 1 alone would have made `page` reachable and inert:
2395
+ `useNavigationOverlay`'s `page` branch calls `onNavigate` and returns with no
2396
+ fallback, this component supplied none, and its registration hands it no host
2397
+ `onRowClick` either — both carriers empty at once, turning an authored `page`
2398
+ from "a drawer opens, which is wrong" into "the click does nothing". The
2399
+ destination is the record-page href the component already computes for the
2400
+ drawer's full-page link, so the two cannot diverge; same-tab navigation uses the
2401
+ history plus `popstate` pair (matching `DashboardRenderer` and `PageHeader`),
2402
+ and `new_window` now opens the app-prefixed href rather than the hook's
2403
+ unprefixed fallback. No host prop is forwarded to the chart.
2404
+
2405
+ A view that authors no `navigation` still gets **no** key and still opens its
2406
+ drawer, so the component's default is unchanged for every gantt in the product
2407
+ today; an authored `none` stays inert.
2408
+ - 237e5b8: fix(plugin-list): stop re-issuing the identical records query on every visualization switch
2409
+
2410
+ `ListView`'s fetch effect named `currentView` in its dependency list, so
2411
+ switching between Grid, Kanban, Calendar and the other visualizations re-ran it
2412
+ — but the query it builds never reads the current visualization. The only way a
2413
+ surface reaches the wire is the `$skip` of its window, and on page 1 that number
2414
+ is 0 for a flat grid and 0 for every other surface, so the re-issued request was
2415
+ byte-for-byte the one already on screen. A board reached through a view switch
2416
+ therefore cost two identical `GET /api/v1/data/<object>?top=…&select=…` round
2417
+ trips before its first paint.
2418
+
2419
+ The effect is now keyed on the window itself (`fetchSkip`), and the two
2420
+ surface-shaped readings the fetch used to latch — the server total behind the
2421
+ grid's pager, and the row-cap banner's "…but the real total is known" half — are
2422
+ derived at render instead. Every re-fetch that changes the window is kept:
2423
+ turning the page still refetches, and leaving a paged grid from page 3 for a
2424
+ board that consumes the whole batch still refetches. What is gone is the
2425
+ round trip that changed nothing.
2426
+ - d327b9c: FLS-gate the `$expand` projection at the seven remaining `buildExpandFields`
2427
+ call sites (objectui#7429).
2428
+
2429
+ objectui#7215 / PR #7229 gated the two projection sites in its scope
2430
+ (`ObjectGrid`, `ListView`). objectui#7230 / PR #7428 gated four more
2431
+ (`ObjectCalendar`, `ObjectGantt`, `RecordDetailView`, `DetailView`). This
2432
+ closes the seven that were left: `ObjectKanban`, `ObjectTree`, `ObjectView`
2433
+ (the non-grid record-fetch effect), `ObjectMap`, `ObjectGallery`,
2434
+ `ObjectTimeline`, and the metadata-admin `PagePreview`'s record-binding fetch.
2435
+
2436
+ **All seven pass no column list at all**, which makes every one of them the
2437
+ sharp shape: `buildExpandFields` reads an absent column list as "no column
2438
+ restriction" and falls back to **every declared relation on the object**,
2439
+ denied ones included. So each of these components asked the server to resolve
2440
+ the object's full relation set by default, not by configuration — the
2441
+ ordinary shape of each surface, not a corner of it.
2442
+
2443
+ **`PagePreview` is the one site where the judged principal is not the page's
2444
+ eventual audience.** It calls the browser's own `fetch` with
2445
+ `credentials: 'include'` rather than `DataSource.find`, so it runs under
2446
+ whichever session is loading the Studio preview. Gating on that same session's
2447
+ `usePermissions()` is still the correct principal: it is exactly the request
2448
+ the browser is about to make, on its own credentials, regardless of who later
2449
+ opens the published page.
2450
+
2451
+ **Reproduced before it was fixed**, as a failing test per site (and, for the
2452
+ two sites — `ObjectView`, `PagePreview` — where the gate was implemented
2453
+ before its test was run red, a reverse-verification: the gate was reverted,
2454
+ all four denial-and-set pins on each went red, and the two deferral/positive
2455
+ control pins stayed green, before the gate was restored).
2456
+
2457
+ **Grading, measured rather than assumed** — the same reading objectui#6898,
2458
+ #7215 and #7230 recorded: against ObjectStack's own server this is
2459
+ defence-in-depth, not a live disclosure. `plugin-security`'s
2460
+ `FieldMasker.maskRecord` deletes every unreadable key from each returned row
2461
+ and objectql's expand path writes the resolved record back under that same
2462
+ key, so one statement removes the expanded object and the bare id alike; the
2463
+ expansion sub-read is itself gated (the referenced object's full CRUD + RLS +
2464
+ FLS treatment, objectstack#7626). It is load-bearing for any backend that does
2465
+ not strip, and the client-request side is real regardless.
2466
+
2467
+ **Nothing a permitted view did stops working.** The gate judges each site's
2468
+ `buildExpandFields` OUTPUT, which contains only the object's declared
2469
+ reference-bearing fields, so the "`checkField` answers false for an
2470
+ undeclared key" trap cannot be reached. An unanswered permission policy
2471
+ filters nothing. `buildExpandFields` itself is unchanged.
2472
+
2473
+ `@object-ui/permissions` is added to `dependencies` for `plugin-kanban`,
2474
+ `plugin-tree`, `plugin-map`, `plugin-timeline`, and `plugin-view` — the fifth
2475
+ one objectui#7429's own dependency count missed (it named four); `plugin-list`
2476
+ and `app-shell` already had it.
2477
+ - c6198c2: **Breaking for authored metadata:** `ComponentInput.label`, `ComponentInput.defaultValue` and
2478
+ `ComponentInput.advanced` are RETIRED on both faces (objectui#7493 item ① and objectui#7781;
2479
+ maintainer ruling A of 2026-09-06, immediate, no deprecation window; ADR-0049 enforce-or-remove).
2480
+ They are the three keys the manifest serializer does not forward, and nothing read them on any
2481
+ publication or consumption path.
2482
+
2483
+ No manifest ever published them, so no consumer could ever have read them. `sdui-parser`'s
2484
+ serializer (`packages/sdui-parser/src/index.ts`) forwards exactly seven keys per input — `name`,
2485
+ `type`, `of`, `required`, `enum`, `binding`, `description` — so a value authored under any of the three
2486
+ never reached `sdui.manifest.json`, the generated JSX `.d.ts`, or a diagnostic; its boundary type
2487
+ has no slot for them; the registry's data-source seam reads `name` only; and neither the designer
2488
+ nor the app-shell inspectors consult registry `inputs` at all. A structural census over every
2489
+ `inputs:` array in the repository (re-measured on this change's merge-base, `name` 951 and `type`
2490
+ 951 as the controls) counted the writes: `label` 908, `defaultValue` 245, `advanced` 9 — written on
2491
+ nearly every registration, read by nothing.
2492
+
2493
+ FROM → TO, per key — all three **TOMBSTONED, not removed**, because the route was measured on
2494
+ the built face before it was chosen: `ComponentInputSchema` is a non-strict `z.object`, and an
2495
+ undeclared key parses GREEN and is silently STRIPPED, so a deletion would have swallowed 1,162
2496
+ authored values in silence. The tombstone is what makes the refusal loud and by name.
2497
+
2498
+ - `label?: string` → `label?: never` on the interface, `retirementTombstone()` on the Zod mirror.
2499
+ Migration: delete the key. An input is identified by its `name` on every path that reaches it;
2500
+ nothing ever rendered a label for it.
2501
+ - `defaultValue?: any` → `defaultValue?: never` / `retirementTombstone()`. Migration: delete the
2502
+ key. The renderer's own fallback read IS the default; tell the author about it in `description`,
2503
+ which IS published. (Tightening the type to `unknown` was ruled out: it closes no error class,
2504
+ since nothing reads the value.)
2505
+ - `advanced?: boolean` → `advanced?: never` / `retirementTombstone()`. Migration: delete the key.
2506
+ No designer surface ever hid an "advanced" input; there is nothing to write instead.
2507
+
2508
+ The retirement kit: `?: never` on `ComponentInput` (`packages/types/src/base.ts`), so authoring one
2509
+ is a `tsc` error at the registration site; `retirementTombstone()` on `ComponentInputSchema`
2510
+ (`packages/types/src/zod/base.zod.ts`), so an authored value is REFUSED at parse time with
2511
+ `code: 'invalid_type'`, the key named in the issue `path`, and the migration note as the message
2512
+ (one string, both channels). Pinned in
2513
+ `packages/types/src/__tests__/component-input-retired-keys-7493.test.ts`, which also holds a
2514
+ tree-scoped absence census over every `inputs:` array under `packages/**` and `apps/**`.
2515
+
2516
+ Accept-set change, stated plainly for reviewers: a document that sets any of the three keys on a
2517
+ `ComponentInput` used to parse GREEN (the value was then dropped by the serializer) and now parses
2518
+ RED. Every in-repo authoring site — 1,199 keys across 110 registration files, the three standalone
2519
+ `ComponentInput[]` arrays and the two named input arrays `tsc` found included — is deleted in the same change, as the ruling's split rule
2520
+ requires; the `WidgetRegistry` seam no longer copies the widget-manifest values onto the synthesized
2521
+ `ComponentInput` (they fed nothing), and the data-source declaration `ELEMENT_DATA_SOURCE_INPUT`
2522
+ drops its `label`. The patch entries on the other packages record exactly that: their registrations
2523
+ stop authoring inert keys, with no runtime or published-manifest change.
2524
+
2525
+ The nine test files that read `defaultValue` off a registration were re-pinned against the
2526
+ renderer's ACTUAL default (its own fallback read, or the `defaultProps` it ships) instead of the
2527
+ declaration that went away; two assertions that only restated the shadow default were dropped with
2528
+ the reason on the line.
2529
+
2530
+ The in-repo zero is what was measured. Whether anything OUTSIDE this repository writes these keys
2531
+ is not measurable from here (the objectui#5674 limit); converting such a write from a silent drop
2532
+ into a named refusal is exactly what the tombstones buy. `WidgetInput`'s own `label` /
2533
+ `defaultValue` / `advanced` (the widget-manifest face) stay declared and writable — nothing has
2534
+ ruled on that face; that it now has no reader either is recorded as objectui#7911.
2535
+ - fee1da5: `ListView` no longer reads relational keys off a list column while the object
2536
+ definition is still loading (objectui#7531).
2537
+
2538
+ Until the definition arrives, the filter and sort candidates are built from the
2539
+ view's declared `columns`. That fallback used to read `reference_to` /
2540
+ `reference`, `display_field` / `reference_field` and `id_field` off each
2541
+ column. `ListColumnSchema` declares none of them in either casing, so a
2542
+ spec-compliant producer can never put one there, and the read only kept a
2543
+ second, undeclared route alive (AGENTS.md 0.1). The candidate now carries what
2544
+ a column declares; its relational target (`referenceTo`, `displayField`,
2545
+ `idField`) comes from the object definition once it loads, as it already did.
2546
+
2547
+ Nothing changes for a spec-compliant view. A column that carried one of these
2548
+ keys anyway now gets the same pre-load candidate as a column without it: the
2549
+ filter's lookup value picker waits for the definition instead of taking the
2550
+ column's word for the target.
2551
+ - 245c7b7: The list view's capability gate now resolves `chart` (objectui#7544).
2552
+
2553
+ A `grid` list view that declared a complete `chart:` block and whitelisted
2554
+ `appearance.allowedVisualizations: ['grid', 'chart']` was never offered the Chart
2555
+ toggle. `availableViews` builds the resolvable set from each visualization's binding
2556
+ and intersects it with the whitelist (ADR-0047, whitelist ∩ resolvable); seven
2557
+ visualizations had a capability check there — kanban, gallery, calendar, timeline,
2558
+ gantt, map, tree — and `chart` had none, so the author's own whitelist was filtered
2559
+ down to nothing and the view fell back to `['grid']`. `chart` entered the offered set
2560
+ only through the "always allow switching back to the schema's own viewType" leg, i.e.
2561
+ only when the view was already `viewType: 'chart'`.
2562
+
2563
+ Both halves were spec-legal and authorable the whole time: `chart` is a
2564
+ `VisualizationTypeSchema` switcher target and `chart:` is a list-view key
2565
+ (`ListChartConfigSchema`). Only the gate never asked. This is `map`'s objectui#5042 one
2566
+ visualization over, and is fixed the same way.
2567
+
2568
+ **What now resolves.** A chart block that binds to names the author wrote: the ADR-0021
2569
+ shape (`dataset` plus at least one measure in `values`), or the legacy inline shape (a
2570
+ declared category — `xAxisField` / `categoryField` — together with a declared measure —
2571
+ `yAxisFields[0]` / `valueField`), in the view-level `chart` block or the legacy
2572
+ `options.chart` bag. A block that declares no binding stays unoffered: reaching the
2573
+ renderer with nothing declared lands on the legacy branch's invented `'name'` /
2574
+ `'value'` floor, and the switcher must not offer a route into it. That floor itself is
2575
+ unchanged here (objectui#7547).
2576
+
2577
+ The gate and the `case 'chart'` render branch now read ONE resolver
2578
+ (`resolveListChartBinding`) rather than two copies of the condition, so they cannot
2579
+ drift about what a usable chart block is. `schema.chart` also joins the memo's
2580
+ dependency array, so a block that arrives on a later render is seen.
2581
+ - 5a211b2: `ObjectGallery` waits for the object definition instead of querying twice
2582
+ (objectui#7903).
2583
+
2584
+ It sat outside the set objectui#6482 converged on the shared settled-schema gate
2585
+ — `ObjectKanban`, `ObjectView`, `ObjectCalendar` and `ObjectTree` were named
2586
+ there, `ObjectGantt` was ask 2 of objectui#7225 and `ObjectTimeline` was
2587
+ objectui#7895 — and nothing marked it a deliberate exclusion. It still held the
2588
+ object definition in a local `useState` fed by its own metadata effect, and
2589
+ listed that definition in the record-fetch effect's dependency array.
2590
+
2591
+ **User-visible.** Every object-bound gallery load issued **two** `find` calls
2592
+ instead of one: the first before the definition landed, with `buildExpandFields`
2593
+ seeing no fields and therefore carrying no `$expand` at all, and a second one
2594
+ after. The first paint was therefore a grid of cards rendered from raw
2595
+ foreign-key ids. After this change the gallery paints once, from a query that
2596
+ already carries its expansion.
2597
+
2598
+ Measured on the component with an instrumented renderer, one mount per hold,
2599
+ `getObjectSchema` held 0/1/2/3/4/5/6/7/8/9/10/15/25/50/100 ms, with
2600
+ `ObjectCalendar` as a positive control in the same run: before, 2 `find` calls
2601
+ with expand sets `[null, ['owner']]` at every hold, the issue order always
2602
+ `schema:issued, find(unexpanded), schema:settled, find(expanded)`, two distinct
2603
+ painted states, 3 late writes into the card grid after the first paint, and a
2604
+ first-paint time flat at 3-7 ms across the whole sweep; after, 1 `find` carrying
2605
+ `['owner']`, one painted state, 0 late writes, and a first paint that tracks the
2606
+ hold (9 ms at +3, 15 ms at +10, 35 ms at +25, 106 ms at +100). The control read
2607
+ 1 `find` carrying `['owner']` and a hold-tracking first paint both before and
2608
+ after.
2609
+
2610
+ The cost this component was paying is a **two**-step paint, not the three-step
2611
+ one `ObjectCalendar` and `ObjectTimeline` each measured: those make `loading` an
2612
+ unconditional early return, so their re-run drops back to a placeholder in
2613
+ between, while this component's early return is `loading && !items.length` — the
2614
+ raw ids were replaced in place. Measured here rather than inherited from the
2615
+ matching shape, which is objectui#6482's own per-component standard.
2616
+
2617
+ The resolution half is now `useSettledSchema` from `@object-ui/react`, which
2618
+ settles on **every** exit — no source, no `getObjectSchema`, no object name, and
2619
+ a read that threw alike. That is what makes the gate safe: the replaced effect
2620
+ returned without settling on all four, which cost nothing while nothing waited on
2621
+ it and would have held a gated query open forever. Pinned by
2622
+ `ObjectGallery.fetchGate-7903.test.tsx`, including a gallery whose adapter
2623
+ exposes no `getObjectSchema` and one whose definition read rejects — both still
2624
+ query, unexpanded.
2625
+
2626
+ Two departures, each judged for this component rather than copied. Like
2627
+ `ObjectTimeline` and unlike `ObjectCalendar` / `ObjectGantt`, the metadata read
2628
+ is **not** disabled for a gallery whose records were authored inline: this
2629
+ component reads the definition on every path, for cell semantics and for ADR-0079
2630
+ card titles, not only to expand a query. And the gate window now holds the
2631
+ loading placeholder rather than showing "No items to display", which the two
2632
+ siblings get from their initial `loading` state and this one did not.
2633
+ - abc1b18: Add `isEmptyValue` to `@object-ui/core` — the weakest common claim about "is
2634
+ this value empty": `null`, `undefined`, the empty string, the empty array, and
2635
+ never a fifth member (objectui#8496, director seat, decision batch #86).
2636
+
2637
+ Five surfaces had each grown their own copy of those four members, and
2638
+ objectui#8481 was the third rediscovery of the same hole. They now call the
2639
+ shared floor and state their own answer against it: `record:details`'
2640
+ `hasCellValue` and `RelatedList` extend it with a trim, `BooleanCellRenderer`
2641
+ with every non-boolean, the date cells with every falsy scalar; `JsonCellRenderer`
2642
+ declines its `[]` member out loud (the array literal is drawn on purpose) and
2643
+ `FileCellRenderer` states "0 files" instead.
2644
+
2645
+ Two visible fixes come with it: a gallery card and a kanban card holding an
2646
+ empty array in a card field now OMIT that field, as they already did for `null`,
2647
+ instead of drawing a labelled "No value" em-dash for it.
2648
+ - c10bc4c: fix(plugin-list): `convertFilterGroupToAST` folds the operator, so a canonical value-less row emits its node instead of being dropped (objectui#9359)
2649
+
2650
+ NOT cosmetic, and not a new defect: a filter row carrying the spec's canonical
2651
+ operator spelling QUERIED WITH NO FILTER AT ALL and returned every record,
2652
+ while the filter panel showed the condition applied. Nothing errored and the
2653
+ result set looked plausible.
2654
+
2655
+ `convertFilterGroupToAST` read the row's operator RAW, against
2656
+ `VALUELESS_FILTER_BUILDER_OPERATORS` — the FilterBuilder's six camelCase
2657
+ dropdown ids. The completeness test it falls through to
2658
+ (`isFilterValueComplete`) DOES fold, through the spec's
2659
+ `normalizeFilterOperator`, to decide arity. So the two halves of one predicate
2660
+ spoke different vocabularies: a row spelled `is_null` missed the value-less
2661
+ short-circuit, landed on `scalar`, had its `value: ''` read as an unfinished
2662
+ row, and was dropped. Measured through the real converter on one `text` column:
2663
+
2664
+ isNull (dropdown id) -> ["title","isnull",null]
2665
+ is_null (canonical) -> [] <- the defect
2666
+ equals + value "acme" -> ["title","=","acme"] <- control, fires
2667
+
2668
+ WHO REACHES THIS READER — measured, not assumed. Its one production caller is
2669
+ `buildEffectiveFilter`, and the argument it converts is the list toolbar's own
2670
+ FilterBuilder group: live in the session, or restored per browser by
2671
+ `writeListFilterState`. Both carry the dropdown's camelCase ids, so the
2672
+ canonical spelling has no measured producer into this reader today. A SAVED
2673
+ view does NOT arrive here — its stored `ViewFilterRule[]` travels
2674
+ `schema.filter` into the base-filter argument of that same call and is lowered
2675
+ by `@object-ui/core`'s `toFilterNode` / `viewFilterRuleToNode`, which already
2676
+ folds: `{ field: 'closed_at', operator: 'is_null', value: '' }` lowers to
2677
+ `["closed_at","is_null",""]`, accepted by `isFilterAST`. So what is repaired is
2678
+ not a live saved-view outage. It is a reader that contradicted its OWN declared
2679
+ contract — the comment above it states it accepts both the FilterBuilder
2680
+ vocabulary and the `@objectstack/spec` `ViewFilterRule` vocabulary, and it
2681
+ dropped a COMPLETE row of the second one, emitting no filter rather than an
2682
+ error. A reader that drops a complete row is a defect whether or not today's
2683
+ saved-view path happens to pre-fold.
2684
+
2685
+ This is the same failure objectui#4744 repaired for the dropdown's own
2686
+ spellings, reached by the other vocabulary. Both raw reads in this function now
2687
+ fold, as do the two `isEmpty` / `isNotEmpty` arms that resolve to a null
2688
+ comparison ahead of `mapOperator` — leaving those on literal ids would have
2689
+ fixed `is_null` and left `is_empty` emitting a different node from its twin,
2690
+ which is this defect rather than a repair of it. All four canonical spellings
2691
+ now emit exactly what their camelCase twins emit.
2692
+
2693
+ **The exported set is unchanged, deliberately.** It states a fact about what the
2694
+ BUILDER'S DROPDOWN draws, and `app-shell`'s `foldFilterGroupToSpecRules`
2695
+ documents its own value-less set as that set PLUS the canonical spellings only
2696
+ that layer sees. Widening the export would have made that layer's deliberate
2697
+ compensation redundant by side effect, in a file nobody is editing. The defect
2698
+ was a reader that forgot to normalize its input, so the reader is what was
2699
+ repaired — the same shape the sibling repair used at the builder's value-input
2700
+ gate (objectui#9302). No published type or export changes, and no stored
2701
+ operator is rewritten: converting is not migrating.
2702
+
2703
+ Which operator vocabulary should WIN is a separate, still-open question and is
2704
+ not decided here. The `contains` / `icontains` boundary is untouched and pinned:
2705
+ the fold this reader routes through maps neither onto the other.
2706
+
2707
+ Superseded in this release by objectui#9306: the list toolbar's FilterBuilder
2708
+ now emits the protocol's canonical ids, so the canonical spelling IS a measured
2709
+ producer into this reader, and `VALUELESS_FILTER_BUILDER_OPERATORS` holds
2710
+ `is_empty`, `is_not_empty`, `is_null`, `is_not_null`, `exists` and `notExists` —
2711
+ its membership changed in this release by that change, still one id per
2712
+ operator. A group restored per browser carries whatever ids it was written
2713
+ with, so a group written before objectui#9306 can still reach this reader in
2714
+ camelCase. This reader is unchanged and answers both spellings alike: over the
2715
+ former dropdown ids and the ids they became (the list reader's leg of
2716
+ objectui#9306's census), every pair emits the same node except
2717
+ `containsCaseInsensitive`, which the toolbar never offered and which the builder
2718
+ folds onto `icontains` before a row leaves it. The vocabulary
2719
+ question this entry calls open is answered: the dropdown speaks the protocol's
2720
+ ids, and camelCase is the deprecated alias form.
2721
+
2722
+ **Correction, 2026-10-03 (objectui#10813).** The two `isEmpty` / `isNotEmpty` arms that
2723
+ "resolve to a null comparison ahead of `mapOperator`" are gone in this release. The empty pair
2724
+ now takes the same value-less path as `is_null`, and `mapOperator` emits the spec's `isempty` /
2725
+ `isnotempty`, which the spec lowers to `$empty`; every spelling of the pair still emits one node
2726
+ (`.changeset/10813-list-view-empty-operator.md`).
2727
+ - c1006ed: `ListView`'s record-detail overlay heading resolves an inline locale map.
2728
+
2729
+ `ListViewSchema.label` is an `I18nLabel` — a plain string **or** an inline locale
2730
+ map such as `{ en: 'Accounts', 'zh-CN': '客户' }`. The `detailTitle` computation
2731
+ handed the raw member into the interpolation options of
2732
+ `detail.recordDetailWithLabel`, so a map-valued label reached the interpolator as
2733
+ an object and the heading rendered `[object Object] Detail`. That value is
2734
+ `NavigationOverlay`'s `title` prop, which objectui#3426 established is the visible
2735
+ heading of the record-detail drawer / modal / split / popover — so the corruption
2736
+ was on user-visible chrome, in every locale, on both interpolators (the i18next
2737
+ one and the provider-less fallback that stringifies with `String`).
2738
+
2739
+ No compiler run could have named the site: `createSafeTranslation`'s options bag
2740
+ is declared as a record of `unknown`, which accepts the map without a diagnostic.
2741
+ Nothing but an executed assertion holds it, which is why the repair ships with
2742
+ pins on both interpolator legs.
2743
+
2744
+ The label is now resolved through `resolveI18nLabel` on the same `displayLocale`
2745
+ this component already uses for the view label and the nested `aria` bag —
2746
+ **before** the truthiness test rather than inside the branch, because
2747
+ `resolveI18nLabel` answers `undefined` for a map with no usable entry and `''`
2748
+ for an empty entry, and both have to fall through to the `objectName` branch the
2749
+ way a missing label always did. Every object is truthy, so a resolution placed
2750
+ inside the branch cannot do that.
2751
+
2752
+ Output for a string-valued `label` is unchanged, byte for byte, in every locale
2753
+ and with no provider mounted.
2754
+ - a883ec2: fix(plugin-list): gallery cards now pass a field's declared `scale` to the shared cell renderer, so a number or percent field declaring `scale` renders padded (`25.00%`) exactly as Grid and Detail do, instead of unpadded (`25%`). (objectui#9575)
2755
+ - f5e2fcb: Refuse a page size the contract already refuses before it reaches any of
2756
+ `ListView`'s six consumers of the resolved value, instead of forwarding it to
2757
+ the wire (objectui#9897).
2758
+
2759
+ One authored `pagination.pageSize` is resolved once in this view and then feeds
2760
+ six places: the `$top` window it fetches, the `$skip` step that turns the page,
2761
+ the has-more gate behind the "showing first N" cap, the page size handed down to
2762
+ the child grid, the record cap printed in that banner, and the rows-per-page
2763
+ control's own displayed value. The resolution was a bare `??` chain, and `??`
2764
+ rejects only `null` and `undefined` — so an authored `pageSize: 0` was not
2765
+ nullish and survived as a real page size into all six.
2766
+
2767
+ What the surviving value actually did, measured in the real renderer over a
2768
+ twelve-row fixture rather than inferred from the sibling repair: `$top: 0` went
2769
+ out on the wire, the data source was asked for nothing, nothing came back, and
2770
+ the view drew its EMPTY STATE — the child grid never rendered at all, so there
2771
+ was no table, no record-count bar and no pager, and nothing on screen named the
2772
+ cause. A negative left the same way (`$top: -10`). A non-integer is worse than
2773
+ silent: `25.5` reached the wire, became the child grid's page size, and turning
2774
+ the page asked for a fractional `$skip`; at a size below the row count the
2775
+ refused number was printed to the reader as the record cap ("showing first 2.5
2776
+ records").
2777
+
2778
+ All of it now goes through one resolver, mirroring the shape objectui#9853
2779
+ landed on `ObjectGrid` — one resolver at every entry is what keeps the answer
2780
+ single. A value the contract refuses is dropped, this view's own default is
2781
+ used, and one `console.warn` on the channel this component already uses for
2782
+ "you declared it, the renderer dropped it" names the member and the value. The
2783
+ warning is conditional: an absent key and a usable page size both stay silent.
2784
+ The runtime choice is covered as well, not only the authored one — an author can
2785
+ put a refused number in `pageSizeOptions`, and picking it used to overwrite a
2786
+ perfectly good authored size.
2787
+
2788
+ Refusing these is not this renderer choosing a meaning. `@objectstack/spec`
2789
+ declares the view pagination member a positive integer and its own suite pins
2790
+ the refusals under the names `should reject zero pageSize` and `should reject
2791
+ negative pageSize`; the `limit` this package's `ElementDataSourceMapping` lowers
2792
+ `pagination.pageSize` into is declared positive as well.
2793
+
2794
+ The fallback literal is now a named constant. Whether it belongs next to
2795
+ `ObjectGrid`'s own three defaults is **not** decided here — changing it changes
2796
+ what every list with no authored `pagination` fetches, which is a product
2797
+ decision rather than an execution seat's, and it is handed back as a question on
2798
+ the issue.
2799
+ - 3a54c39: Deliver an authored map `style` on the list-view and object-view paths (objectui#9950).
2800
+
2801
+ `ObjectMapConfigSchema` declares `style` ("MapLibre style URL/spec (overrides the
2802
+ public demo default)"), and both view flatteners dropped it before the renderer ever
2803
+ saw it. A view authoring `map: { style: 'https://…/style.json' }` parsed green, nothing
2804
+ refused it, nothing warned, and the map rendered on MapLibre's **public demo tiles**.
2805
+
2806
+ Each flattener's key whitelist is now a TOTAL spelling table,
2807
+ `FLAT_MAP_CONFIG_SPELLING`, mapping every key `ObjectMapConfigSchema` declares to the
2808
+ name the internal flat form uses for it. Every entry is the identity except `style`,
2809
+ which is delivered as `mapStyle`.
2810
+
2811
+ `mapStyle`, not `style`, because the top-level `style` key is `BaseSchema.style` —
2812
+ inline CSS, legal on every node. Collapsing the two namespaces onto one key is the
2813
+ defect objectui#5177 closed and this change deliberately keeps closed: the flatten
2814
+ still never writes a top-level `style`. `mapStyle` is a declared member of
2815
+ `ObjectMapSchema` and is the first spelling `getMapConfig` reads
2816
+ (`schema.mapStyle || schema.map?.style`), so the authored style now reaches the
2817
+ renderer without inventing an undeclared transport key.
2818
+
2819
+ An undeclared key in the `map` block still never reaches the product — that half of
2820
+ objectui#5177 is unchanged and pinned in both packages' `mapFlatten` suites.
2821
+
2822
+ **Why the anti-drift pins did not catch this.** Both sites already pinned their hand
2823
+ list against the declaration, and both pins were green. They compared the list against
2824
+ `Object.keys(ObjectMapConfigSchema.shape).filter((key) => key !== 'style')` — the
2825
+ comparison set had the same key subtracted from it that the whitelist was missing, so
2826
+ the pin agreed with the omission. Those pins now measure the relation "every declared
2827
+ key is delivered, under its flat spelling" against the declaration read whole, and each
2828
+ suite carries a control that feeds the pre-fix whitelist to the same assertion and
2829
+ shows it rejected. The spelling table is additionally a `Record` over every
2830
+ `keyof ObjectMapConfig`, so a key added to the declaration fails `tsc` until it is
2831
+ given a flat spelling.
2832
+
2833
+ **Migration.** None. Views that never authored `map.style` are byte-identical in
2834
+ behaviour; views that did now render with the style they declared instead of the
2835
+ public demo tiles.
2836
+ - 15236e0: `list-view` harvests row-action predicate fields from the OBJECT's `userActions` block only — a view's toolbar policy can no longer shadow it.
2837
+
2838
+ `userActions` names two different blocks. On a **view** it is toolbar policy —
2839
+ the spec's `UserActionsConfigSchema` (`sort`, `search`, `filter`, `refresh`,
2840
+ `rowHeight`, `addRecordForm`, `editInline`, `buttons`), which rejects `edit` by
2841
+ name. On an **object** it is the CRUD-predicate block (`edit` / `delete` /
2842
+ `create` carrying `visibleWhen` / `disabledWhen`, objectui#2614) — and that is
2843
+ the only shape `listViewPredicates` can read, since its loop skips every
2844
+ non-object value.
2845
+
2846
+ `ListView` read the key view-first when building the `$select` projection
2847
+ (`(schema as any).userActions ?? (objectDef as any)?.userActions`). A view
2848
+ carrying a perfectly legal toolbar block therefore shadowed the object's CRUD
2849
+ predicates, the harvest found none, and the predicate's operand left the
2850
+ projection. CEL then faults on the absent key, fails closed, and the row
2851
+ Edit/Delete button disappears for everyone with nothing pointing at the
2852
+ projection — objectui#3501's failure, reached with a success receipt at every
2853
+ step.
2854
+
2855
+ This is the sibling of the `plugin-grid` read site fixed in objectui#5426, and
2856
+ it was the worse of the two: `app-shell`'s `ObjectView` builds the view-level
2857
+ `userActions` it hands down as an object literal of two spreads, so the left
2858
+ operand was `{}` at worst — never nullish. The `??` never fell through, and the
2859
+ object's CRUD predicates were never consumed at all on that path, whether or
2860
+ not an author wrote any toolbar policy.
2861
+
2862
+ The harvest now reads the object block only. Both `userActions` read sites in
2863
+ `ListView.tsx` carry a comment naming the collision, and
2864
+ `__tests__/ListView.userActionsCollision.test.tsx` pins each clause of it: the
2865
+ two shapes, a producer that manufactures the view one, the harvest's blindness
2866
+ to it, and the projection that must keep the object's operand with a toolbar
2867
+ block — or an empty block — present on the view.
2868
+
2869
+ Toolbar policy itself is untouched — it was never read through this path.
2870
+ - ec9fdaa: `ListView` now resolves the nested `aria.ariaLabel` against the audience's locale
2871
+ instead of casting it to a string (objectui#5134).
2872
+
2873
+ `@objectstack/spec`'s `AriaPropsSchema` types `ariaLabel` as `I18nLabel` — a plain
2874
+ string **or** an inline locale map (`{ en: 'Accounts', 'zh-CN': '客户' }`). The only
2875
+ read site in this repo spread it into the DOM as
2876
+ `{ 'aria-label': schema.aria.ariaLabel as string }`, and `as string` is a cast, not a
2877
+ conversion: a map-valued label reached the DOM as `aria-label="[object Object]"`, which
2878
+ a screen reader announces as the list view's accessible name — in every locale. The
2879
+ read now goes through the spec's own `resolveI18nLabel` (the resolver four other
2880
+ in-repo read sites already use) against `useDisplayLocale()`.
2881
+
2882
+ ⚠️ **Dated note, 2026-09-29 — the read-site count above does not re-derive —
2883
+ objectui#10979.** "the resolver four other in-repo read sites already use" above does not
2884
+ hold even at this change's own base. On the commit this change landed on (`e40b8579bd`),
2885
+ ten non-test source files already imported `@objectstack/spec`'s `resolveI18nLabel` (each
2886
+ as `resolveInlineI18nLabel`) and called it on 17 lines. Re-measured on `main` at
2887
+ `2eaf5be27`, fourteen non-test files besides `ListView` imported it, calling it on 27
2888
+ lines. The text above is kept as the reading of this change.
2889
+
2890
+ Reachability, stated plainly: the path is **live but unexercised**. `I18nLabel` was a
2891
+ plain `string` through `@objectstack/spec` 17.0.0-rc.5, so no stored map-valued label
2892
+ predates rc.6, and no measured author writes one today — but map values are legitimate
2893
+ and arrive via API/import, so an imported list view carrying
2894
+ `aria: { ariaLabel: { en: …, 'zh-CN': … } }` is spec-valid metadata that renders a wrong
2895
+ accessible name. This is the map form working as declared, not a defect users are
2896
+ currently hitting.
2897
+
2898
+ Behaviour on the string arm is byte-identical, including `''` (falsy before and after,
2899
+ so no attribute). One edge changes for the better: a map that matches no locale used to
2900
+ render `aria-label="[object Object]"` (`{}` is truthy) and now omits the attribute — an
2901
+ unnamed region beats a garbage-named one.
2902
+
2903
+ The **flat** `schema.ariaLabel` is deliberately untouched: it carries a different
2904
+ vocabulary (objectui's keyed `{ key, defaultValue?, params? }` ref, resolved by
2905
+ `SchemaRenderer`'s `resolveKeyedI18nLabel`), and neither resolver accepts the other's
2906
+ shape.
2907
+ - d6613a2: Two comment corrections in `ListView.tsx` (objectui#4559, objectui#4966). No runtime
2908
+ behaviour changes and the emitted bundle is byte-identical; the published `.d.ts` does
2909
+ change, which is why this is a `patch` rather than an empty frontmatter.
2910
+
2911
+ **objectui#4559 — the sort rationale stopped prescribing a formula field.** The comment
2912
+ block above the `sortFields` memo still called a formula field "the supported
2913
+ alternative (… which sorts like any text column)". Since objectui#4294 the
2914
+ `list.sortRelationalHint` string in this same file says the opposite ("Not a formula
2915
+ field: it is virtual, so no column is stored for it and the server refuses to sort by
2916
+ one"), the memo underneath filters formula out via `UNMATERIALIZED_FIELD_TYPES`, and the
2917
+ server answers such a sort with `400 INVALID_SORT` (objectstack#6994). The parenthetical
2918
+ now names the remedy the hint, the server's refusal and the README already share — a
2919
+ stored field that denormalizes the name onto this object, written when the source
2920
+ changes. This was the last copy of the retired advice in the repo.
2921
+
2922
+ **objectui#4966 — `formatActionLabel`'s docblock now sits above `formatActionLabel`.**
2923
+ It had drifted two declarations up, so the exported `parseSortConfig` carried two
2924
+ stacked leading comments and the helper carried none. This one was not cosmetic: because
2925
+ `parseSortConfig` is exported, `vite-plugin-dts` copied the misattributed block into
2926
+ `dist/ListView.d.ts`, so every consumer's editor hover and TypeDoc introduced the sort
2927
+ parser with a sentence about action labels. Moving the block removes it from the `.d.ts`;
2928
+ `formatActionLabel` is module-private, so its now-correct docblock does not appear there.
2929
+ It also matters to `scripts/check-spec-symbol-derivation.mjs`, whose rule 2 reads the
2930
+ comment block *attached* to a declaration — a misattributed docblock is the mechanism by
2931
+ which a claim gets scored against the wrong symbol. This block carries no spec-alignment
2932
+ phrase, so nothing fired today.
2933
+
2934
+ No tests accompany this change and none could: both edits are comment-only, and there is
2935
+ no runtime behaviour to pin. The `.d.ts` delta was measured with the package's real
2936
+ `vite build` before and after, not asserted.
2937
+ - fb336df: Move `lucide-react` from `^1.31.0` to `^1.43.0` in every package that declares it, and
2938
+ repair what the jump breaks, so icons resolved from a STRING keep drawing a glyph.
2939
+
2940
+ Measured once against the installed 1.43.0 artifact when this change was made; nothing in
2941
+ the repository re-derives these readings. Across the jump lucide removes no runtime export,
2942
+ no public type name and no `lucide-react/dynamic.mjs` name, and every name this repository
2943
+ imports from `lucide-react` resolves. Exactly one key leaves the runtime `icons` record:
2944
+ `Trash2`, retired in favour of `trash`.
2945
+
2946
+ What a user sees change:
2947
+
2948
+ - Icons the lazy icon seam draws (`resolveIcon`, objectui#9251) get their path data again.
2949
+ lucide 1.43.0 icon modules export their path data inside `__iconData` and no longer
2950
+ export `__iconNode`; the seam now reads both. Reading only `__iconNode` against 1.43.0
2951
+ leaves every such glyph an empty box, with nothing thrown or logged.
2952
+ - `DetailView`'s delete action and three schema-catalog examples spell their icon
2953
+ `trash`, not `trash-2`, so they keep resolving. The glyph is unchanged: 1.43.0's `trash`
2954
+ path data is byte-identical to the `trash-2` path data of 1.31.0 and 1.35.0. Anything
2955
+ that already authored `trash` now draws that same artwork, because lucide moved it under
2956
+ the `trash` name; a few other glyphs were also redrawn upstream.
2957
+ - Icons imported as COMPONENTS carry lucide 1.43.0's own classes: one class per declared
2958
+ alias (a spinner renders `class="lucide lucide-loader-circle lucide-loader-2 ..."`), and
2959
+ no longer the class lucide used to derive from the PascalCase key where that differs
2960
+ (`ArrowDown01` no longer carries `lucide-arrow-down01`). The canonical
2961
+ `lucide-<icon name>` class is still there, so a `.lucide-loader-circle` selector still
2962
+ matches.
2963
+ - Icons resolved from a STRING through the seam keep the classes they had: `lucide`, the
2964
+ canonical `lucide-<icon name>` class and the key-derived class. They do not carry
2965
+ lucide's per-alias classes. That is a maintainer ruling (objectui#8941, option C): the
2966
+ alias list lives only inside each lazily loaded icon module, and it was not moved into
2967
+ the eager name list. A selector that targets an alias class matches a component-imported
2968
+ icon and not a string-resolved one. This also dates the example in the objectui#9251
2969
+ entry: `trash-2` no longer resolves as a string, so no seam-drawn glyph carries
2970
+ `lucide-trash2 lucide-trash-2`; a digit-bearing name that still resolves, such as
2971
+ `arrow-down-0-1`, carries `lucide-arrow-down01 lucide-arrow-down-0-1`.
2972
+ - 6c6cee7: A RETIRED field-type spelling is now refused — out loud, once — by every
2973
+ field-type predicate in the renderer, not just by the widget road
2974
+ (objectui#4914, maintainer ruling B of 2026-08-18).
2975
+
2976
+ `@object-ui/fields` exports a single `isRetiredFieldType(t)` gate, and it runs
2977
+ ahead of six predicate faces that previously granted a retired spelling
2978
+ first-class treatment: the filter builder's operator buckets and its value
2979
+ control (`@object-ui/components`), the detail page's highlight-strip picker
2980
+ (`@object-ui/plugin-detail`), `normalizeFieldType` (`@object-ui/plugin-view`),
2981
+ the dashboard's `$expand` whitelist and `isLookupType`
2982
+ (`@object-ui/plugin-dashboard`), and the list toolbar's lookup-like filter
2983
+ control (`@object-ui/plugin-list`). Each one now fires the migration
2984
+ prescription on the console — once per spelling across all of them, never once
2985
+ per predicate — and then answers as it would for a spelling it does not
2986
+ recognise.
2987
+
2988
+ This closes the whole CLASS rather than one word: the gate is quantified over
2989
+ `RETIRED_FIELD_TYPES`, so the next retirement covers all seven consumers on the
2990
+ day it lands. It is the shape objectui#4932 and objectui#4942 already
2991
+ established for the form and inline-edit roads.
2992
+
2993
+ Measured before the change, and the reason the fix is a gate rather than a
2994
+ deletion: `owner` was not dead in these faces. `operatorsForFieldType('owner')`
2995
+ equalled the `user` bucket item for item, `computeLookupExpand` actively
2996
+ requested `$expand` for it, `isLookupType('owner')` was `true` alongside
2997
+ `reference`, and `normalizeFieldType('owner')` answered `'select'` exactly as
2998
+ `picklist` does. Deleting the members alone would have traded a visible
2999
+ contradiction for a SILENT degradation — a filter picker collapsing to a bare id
3000
+ box, `$expand` quietly stopping so cells show raw foreign-key ids — which is
3001
+ verbatim the failure mode `RETIRED_FIELD_TYPES`' own docblock exists to prevent.
3002
+ The gate keeps that fallback and adds the half that was missing: the author is
3003
+ told.
3004
+
3005
+ The boundary question is answered on record: `owner` arriving through a
3006
+ backend-vocabulary normalizer is an authoring error to refuse loudly, not
3007
+ legitimate foreign input to tolerate. The open backend vocabulary those
3008
+ normalizers exist for is untouched — `reference`, `picklist`, `money`, `int`,
3009
+ `datetime_tz` and the rest are equally absent from the spec's closed `FieldType`
3010
+ and are equally unretired, so they classify exactly as before.
3011
+
3012
+ `RETIRED_FIELD_TYPES`, `reportRetiredFieldType` and `resetRetiredFieldTypeReports`
3013
+ move to `@object-ui/core` and are re-exported from `@object-ui/fields`, so that
3014
+ package's published surface is unchanged apart from the newly ruled gate.
3015
+ `@object-ui/components` is a consumer of the gate and `@object-ui/fields`
3016
+ depends on it, so a single shared table could not live in `fields` — and a
3017
+ second copy would have meant a second dedupe set and two console lines for one
3018
+ spelling. No package gained a new dependency.
3019
+
3020
+ A retired spelling never loses a stored value: `retypeFilterValue` is
3021
+ deliberately not gated, and the refused filter row stays operable rather than
3022
+ drawing a blank operator trigger.
3023
+ - 42887e0: Repair five retired lucide icon spellings that reach a record-reading resolver, and pin
3024
+ the names against the runtime `icons` record so the next lucide bump goes red instead of
3025
+ silently blanking a glyph (objectui#5622).
3026
+
3027
+ lucide retires a spelling by dropping it from its runtime `icons` record while KEEPING it
3028
+ as a deprecated named export. A retired name therefore still imports, still type-checks,
3029
+ and still renders wherever it is used as a COMPONENT — and resolves to `null` wherever it
3030
+ is used as a STRING, because every string lookup here reads that record. Nothing goes red
3031
+ either way. Measured against the installed `lucide-react@1.31.0` (1767 record entries) at
3032
+ implementation time.
3033
+
3034
+ What a user sees change:
3035
+
3036
+ - `DetailView`'s mobile Edit action (`icon: 'edit'` → `'square-pen'`) draws its icon
3037
+ again. Its items become an `action:bar` schema whose renderers resolve `icon` through
3038
+ `renderers/action/resolve-icon.ts`, so the touch-breakpoint edit affordance had been
3039
+ drawing a label with nothing beside it. `Edit === SquarePen`, so the glyph is unchanged.
3040
+ - The `ui:icon` renderer's own declared default (`'smile'` → `'face-slightly-smiling'`, in
3041
+ both the registration `icon` and the `name` input's `defaultValue`) resolves again: the
3042
+ designer palette entry's glyph was blank, and an `icon` dropped from that palette
3043
+ rendered nothing plus a `console.warn`. `Smile === FaceSlightlySmiling`, so the palette
3044
+ looks exactly as it did.
3045
+ - `plugin-list`'s `ViewSwitcher` moves `Grid` → `Grid3x3`, `BarChart3` → `ChartColumn`
3046
+ (both identical objects, no visual change) and `GanttChartSquare` → `ChartGantt`. The
3047
+ gantt one IS a glyph change: it matches the spelling the sibling `plugin-view` switcher
3048
+ landed in objectui#5586, so one view type no longer draws two different icons depending
3049
+ on which switcher is on screen.
3050
+
3051
+ Four resolvability pins are added — in `plugin-detail`, `plugin-list`, `components` and
3052
+ alongside the `DeclaredActionsBar` fixtures. Each asserts `icons`-record MEMBERSHIP rather
3053
+ than resolvability, because every retired spelling repaired here is the SAME component
3054
+ object as its replacement (`Edit === SquarePen`, `Smile === FaceSlightlySmiling`,
3055
+ `Grid === Grid3x3`, `BarChart3 === ChartColumn`, `CheckCircle === CircleCheckBig`,
3056
+ `XCircle === CircleX` are all true): a pin that rendered the glyph, or reached for the
3057
+ export, would pass on the broken name. That is the blindness that let this ship.
3058
+ - Updated dependencies [7b10bef]
3059
+ - Updated dependencies [97abedc]
3060
+ - Updated dependencies [b46c58f]
3061
+ - Updated dependencies [a507334]
3062
+ - Updated dependencies [ad694ac]
3063
+ - Updated dependencies [6f96fca]
3064
+ - Updated dependencies [afb2284]
3065
+ - Updated dependencies [3ac2de8]
3066
+ - Updated dependencies [0aacecc]
3067
+ - Updated dependencies [63f4f92]
3068
+ - Updated dependencies [777fca2]
3069
+ - Updated dependencies [c131d9e]
3070
+ - Updated dependencies [5f00ff4]
3071
+ - Updated dependencies [c9e073a]
3072
+ - Updated dependencies [0361d6b]
3073
+ - Updated dependencies [7b395d8]
3074
+ - Updated dependencies [0879812]
3075
+ - Updated dependencies [8cedb0d]
3076
+ - Updated dependencies [6ce001a]
3077
+ - Updated dependencies [162621b]
3078
+ - Updated dependencies [4c6f549]
3079
+ - Updated dependencies [80c5412]
3080
+ - Updated dependencies [6cc910b]
3081
+ - Updated dependencies [061f5e8]
3082
+ - Updated dependencies [2dd4d3f]
3083
+ - Updated dependencies [e686f4d]
3084
+ - Updated dependencies [212c451]
3085
+ - Updated dependencies [a04b06d]
3086
+ - Updated dependencies [4ab4f1b]
3087
+ - Updated dependencies [b57107d]
3088
+ - Updated dependencies [e3ea4f9]
3089
+ - Updated dependencies [65f1e8d]
3090
+ - Updated dependencies [1f8ef0a]
3091
+ - Updated dependencies [bb5d4ee]
3092
+ - Updated dependencies [dc666f7]
3093
+ - Updated dependencies [3335767]
3094
+ - Updated dependencies [8b1f066]
3095
+ - Updated dependencies [af243c1]
3096
+ - Updated dependencies [cff4b77]
3097
+ - Updated dependencies [31938f0]
3098
+ - Updated dependencies [961ceaa]
3099
+ - Updated dependencies [f3f4e4c]
3100
+ - Updated dependencies [a05c350]
3101
+ - Updated dependencies [1dbb993]
3102
+ - Updated dependencies [2b5f509]
3103
+ - Updated dependencies [808f339]
3104
+ - Updated dependencies [6cf5999]
3105
+ - Updated dependencies [274e14a]
3106
+ - Updated dependencies [8c10f4f]
3107
+ - Updated dependencies [90dac98]
3108
+ - Updated dependencies [6096f20]
3109
+ - Updated dependencies [544aca2]
3110
+ - Updated dependencies [ea02938]
3111
+ - Updated dependencies [a14fb23]
3112
+ - Updated dependencies [ae98f1d]
3113
+ - Updated dependencies [f98eddf]
3114
+ - Updated dependencies [ce6bd99]
3115
+ - Updated dependencies [a5b08c9]
3116
+ - Updated dependencies [86982ac]
3117
+ - Updated dependencies [8c929e6]
3118
+ - Updated dependencies [cdefa2a]
3119
+ - Updated dependencies [2fc2a24]
3120
+ - Updated dependencies [0961d5e]
3121
+ - Updated dependencies [9d25b9b]
3122
+ - Updated dependencies [6276478]
3123
+ - Updated dependencies [5e67837]
3124
+ - Updated dependencies [ff14e29]
3125
+ - Updated dependencies [9b28151]
3126
+ - Updated dependencies [64563a9]
3127
+ - Updated dependencies [1a5003f]
3128
+ - Updated dependencies [09ab32b]
3129
+ - Updated dependencies [8acc51b]
3130
+ - Updated dependencies [ea9d17f]
3131
+ - Updated dependencies [45362a3]
3132
+ - Updated dependencies [2a943bf]
3133
+ - Updated dependencies [111fa4c]
3134
+ - Updated dependencies [93a689d]
3135
+ - Updated dependencies [4357a27]
3136
+ - Updated dependencies [e5f4343]
3137
+ - Updated dependencies [3261e64]
3138
+ - Updated dependencies [f5178a2]
3139
+ - Updated dependencies [2ad3671]
3140
+ - Updated dependencies [39bf246]
3141
+ - Updated dependencies [8740e86]
3142
+ - Updated dependencies [d22b37b]
3143
+ - Updated dependencies [8c0e550]
3144
+ - Updated dependencies [fde4caf]
3145
+ - Updated dependencies [caf0ed0]
3146
+ - Updated dependencies [1daf477]
3147
+ - Updated dependencies [3d614ea]
3148
+ - Updated dependencies [a7df45f]
3149
+ - Updated dependencies [6c2f3c5]
3150
+ - Updated dependencies [fb13e85]
3151
+ - Updated dependencies [c2d8659]
3152
+ - Updated dependencies [6516320]
3153
+ - Updated dependencies [98b1a7c]
3154
+ - Updated dependencies [e0f8202]
3155
+ - Updated dependencies [ac2d6f1]
3156
+ - Updated dependencies [9fbbb17]
3157
+ - Updated dependencies [c3a26cc]
3158
+ - Updated dependencies [a66e58e]
3159
+ - Updated dependencies [d89492c]
3160
+ - Updated dependencies [9a5f998]
3161
+ - Updated dependencies [1c5ee33]
3162
+ - Updated dependencies [9327397]
3163
+ - Updated dependencies [17cc3a3]
3164
+ - Updated dependencies [a9c5ea0]
3165
+ - Updated dependencies [02e6d36]
3166
+ - Updated dependencies [4758b33]
3167
+ - Updated dependencies [4758b33]
3168
+ - Updated dependencies [4758b33]
3169
+ - Updated dependencies [f905090]
3170
+ - Updated dependencies [3cd6c5e]
3171
+ - Updated dependencies [5bb855d]
3172
+ - Updated dependencies [9c78ebe]
3173
+ - Updated dependencies [12809a5]
3174
+ - Updated dependencies [9c78ebe]
3175
+ - Updated dependencies [12809a5]
3176
+ - Updated dependencies [7afc81d]
3177
+ - Updated dependencies [f9c06ef]
3178
+ - Updated dependencies [5ad3b88]
3179
+ - Updated dependencies [f9d772b]
3180
+ - Updated dependencies [bf14b64]
3181
+ - Updated dependencies [4345558]
3182
+ - Updated dependencies [97b6c21]
3183
+ - Updated dependencies [26ca2ad]
3184
+ - Updated dependencies [41ae65b]
3185
+ - Updated dependencies [baac95a]
3186
+ - Updated dependencies [13220af]
3187
+ - Updated dependencies [29b45f6]
3188
+ - Updated dependencies [39b8d51]
3189
+ - Updated dependencies [17b323e]
3190
+ - Updated dependencies [b956e69]
3191
+ - Updated dependencies [b32e7de]
3192
+ - Updated dependencies [ff94a12]
3193
+ - Updated dependencies [33e58d8]
3194
+ - Updated dependencies [256b4c9]
3195
+ - Updated dependencies [97672ba]
3196
+ - Updated dependencies [c5ec15c]
3197
+ - Updated dependencies [fec3b1a]
3198
+ - Updated dependencies [b8e0941]
3199
+ - Updated dependencies [0c50f18]
3200
+ - Updated dependencies [1dae95a]
3201
+ - Updated dependencies [e32dae1]
3202
+ - Updated dependencies [d0c0c7f]
3203
+ - Updated dependencies [30b11ad]
3204
+ - Updated dependencies [4aebea0]
3205
+ - Updated dependencies [f976774]
3206
+ - Updated dependencies [3d6badf]
3207
+ - Updated dependencies [25cb364]
3208
+ - Updated dependencies [a60539b]
3209
+ - Updated dependencies [de1b879]
3210
+ - Updated dependencies [c6678b1]
3211
+ - Updated dependencies [0638322]
3212
+ - Updated dependencies [e3782d2]
3213
+ - Updated dependencies [db0beb2]
3214
+ - Updated dependencies [997ce38]
3215
+ - Updated dependencies [ae0b9d3]
3216
+ - Updated dependencies [ad1785c]
3217
+ - Updated dependencies [3b469c8]
3218
+ - Updated dependencies [990a2d6]
3219
+ - Updated dependencies [990a2d6]
3220
+ - Updated dependencies [1c84036]
3221
+ - Updated dependencies [1c84036]
3222
+ - Updated dependencies [6650259]
3223
+ - Updated dependencies [4f8b7f8]
3224
+ - Updated dependencies [9e6619f]
3225
+ - Updated dependencies [f6ae5e2]
3226
+ - Updated dependencies [eb97ce6]
3227
+ - Updated dependencies [7343376]
3228
+ - Updated dependencies [b2683a2]
3229
+ - Updated dependencies [dded788]
3230
+ - Updated dependencies [b45d463]
3231
+ - Updated dependencies [54a7830]
3232
+ - Updated dependencies [f3135a4]
3233
+ - Updated dependencies [b5696d3]
3234
+ - Updated dependencies [3f9d926]
3235
+ - Updated dependencies [e978ed5]
3236
+ - Updated dependencies [6a7f24e]
3237
+ - Updated dependencies [b3c96d6]
3238
+ - Updated dependencies [8d0ca91]
3239
+ - Updated dependencies [c30c8dd]
3240
+ - Updated dependencies [244d516]
3241
+ - Updated dependencies [deca847]
3242
+ - Updated dependencies [ac15833]
3243
+ - Updated dependencies [ac15833]
3244
+ - Updated dependencies [e2dffc9]
3245
+ - Updated dependencies [328abeb]
3246
+ - Updated dependencies [dd0d78f]
3247
+ - Updated dependencies [24d3e65]
3248
+ - Updated dependencies [95a7c8d]
3249
+ - Updated dependencies [e227156]
3250
+ - Updated dependencies [cc4e476]
3251
+ - Updated dependencies [92970c4]
3252
+ - Updated dependencies [d570eaa]
3253
+ - Updated dependencies [4b742f4]
3254
+ - Updated dependencies [42687ba]
3255
+ - Updated dependencies [6cd8f66]
3256
+ - Updated dependencies [24a0f14]
3257
+ - Updated dependencies [797a30f]
3258
+ - Updated dependencies [b4075c0]
3259
+ - Updated dependencies [9b85600]
3260
+ - Updated dependencies [e327c89]
3261
+ - Updated dependencies [2eaf5be]
3262
+ - Updated dependencies [bf7ab35]
3263
+ - Updated dependencies [99878d8]
3264
+ - Updated dependencies [3c13675]
3265
+ - Updated dependencies [63ab761]
3266
+ - Updated dependencies [0eb9f36]
3267
+ - Updated dependencies [ae582b7]
3268
+ - Updated dependencies [51c2949]
3269
+ - Updated dependencies [a4b017e]
3270
+ - Updated dependencies [8732846]
3271
+ - Updated dependencies [559a2e2]
3272
+ - Updated dependencies [db11afd]
3273
+ - Updated dependencies [154075a]
3274
+ - Updated dependencies [582edef]
3275
+ - Updated dependencies [19f484f]
3276
+ - Updated dependencies [0a78a20]
3277
+ - Updated dependencies [615346d]
3278
+ - Updated dependencies [75dcc81]
3279
+ - Updated dependencies [55a12a8]
3280
+ - Updated dependencies [edfcf5a]
3281
+ - Updated dependencies [0a3e540]
3282
+ - Updated dependencies [c021b35]
3283
+ - Updated dependencies [f61dab1]
3284
+ - Updated dependencies [b0a05dd]
3285
+ - Updated dependencies [dd5ff19]
3286
+ - Updated dependencies [54997ff]
3287
+ - Updated dependencies [81f8498]
3288
+ - Updated dependencies [a782fa7]
3289
+ - Updated dependencies [1d6a23d]
3290
+ - Updated dependencies [0645133]
3291
+ - Updated dependencies [76e9df0]
3292
+ - Updated dependencies [0ecaa7d]
3293
+ - Updated dependencies [b24f93a]
3294
+ - Updated dependencies [6158e4c]
3295
+ - Updated dependencies [cff8641]
3296
+ - Updated dependencies [c27b575]
3297
+ - Updated dependencies [84b275c]
3298
+ - Updated dependencies [0e6e76b]
3299
+ - Updated dependencies [bf43afa]
3300
+ - Updated dependencies [385ebc5]
3301
+ - Updated dependencies [3a0e7ab]
3302
+ - Updated dependencies [858eafb]
3303
+ - Updated dependencies [a8c5509]
3304
+ - Updated dependencies [c2a8d23]
3305
+ - Updated dependencies [0ffc423]
3306
+ - Updated dependencies [3cc4fe5]
3307
+ - Updated dependencies [cd5b19a]
3308
+ - Updated dependencies [cd5b19a]
3309
+ - Updated dependencies [17dc167]
3310
+ - Updated dependencies [20d23be]
3311
+ - Updated dependencies [20d23be]
3312
+ - Updated dependencies [2e3da72]
3313
+ - Updated dependencies [1263e40]
3314
+ - Updated dependencies [e6bc087]
3315
+ - Updated dependencies [9419df1]
3316
+ - Updated dependencies [8bab157]
3317
+ - Updated dependencies [a7557a7]
3318
+ - Updated dependencies [7d074ba]
3319
+ - Updated dependencies [6158e4c]
3320
+ - Updated dependencies [6158e4c]
3321
+ - Updated dependencies [52aad5c]
3322
+ - Updated dependencies [18d1a0a]
3323
+ - Updated dependencies [7fed09d]
3324
+ - Updated dependencies [c1763e5]
3325
+ - Updated dependencies [58da8ae]
3326
+ - Updated dependencies [a8b9889]
3327
+ - Updated dependencies [138ad45]
3328
+ - Updated dependencies [138ad45]
3329
+ - Updated dependencies [5262f7d]
3330
+ - Updated dependencies [6aa029b]
3331
+ - Updated dependencies [2124d04]
3332
+ - Updated dependencies [be52115]
3333
+ - Updated dependencies [770cc5b]
3334
+ - Updated dependencies [1a88ce2]
3335
+ - Updated dependencies [a1a44d6]
3336
+ - Updated dependencies [e0a9c67]
3337
+ - Updated dependencies [5638529]
3338
+ - Updated dependencies [5638529]
3339
+ - Updated dependencies [d0fba91]
3340
+ - Updated dependencies [c476be0]
3341
+ - Updated dependencies [c82ff39]
3342
+ - Updated dependencies [6c3da53]
3343
+ - Updated dependencies [31987bd]
3344
+ - Updated dependencies [3c3ce15]
3345
+ - Updated dependencies [063119f]
3346
+ - Updated dependencies [e100589]
3347
+ - Updated dependencies [304f611]
3348
+ - Updated dependencies [e46ee77]
3349
+ - Updated dependencies [f9c8c4e]
3350
+ - Updated dependencies [6e9c8d2]
3351
+ - Updated dependencies [9547063]
3352
+ - Updated dependencies [3f6efd6]
3353
+ - Updated dependencies [52c95a1]
3354
+ - Updated dependencies [55d18c6]
3355
+ - Updated dependencies [c4ab6d0]
3356
+ - Updated dependencies [0e9058b]
3357
+ - Updated dependencies [c4ab6d0]
3358
+ - Updated dependencies [5988b6b]
3359
+ - Updated dependencies [6158e4c]
3360
+ - Updated dependencies [00ccdf7]
3361
+ - Updated dependencies [9d9ed54]
3362
+ - Updated dependencies [6007dd4]
3363
+ - Updated dependencies [4a1adb7]
3364
+ - Updated dependencies [4a1adb7]
3365
+ - Updated dependencies [4a1adb7]
3366
+ - Updated dependencies [185b7a0]
3367
+ - Updated dependencies [50c73fe]
3368
+ - Updated dependencies [ca3de72]
3369
+ - Updated dependencies [83e3f83]
3370
+ - Updated dependencies [401611b]
3371
+ - Updated dependencies [2c0ddf2]
3372
+ - Updated dependencies [4abc0aa]
3373
+ - Updated dependencies [f560ded]
3374
+ - Updated dependencies [2b188fa]
3375
+ - Updated dependencies [b654d4e]
3376
+ - Updated dependencies [f68e0a0]
3377
+ - Updated dependencies [aea682a]
3378
+ - Updated dependencies [fcdc8ec]
3379
+ - Updated dependencies [2d576e4]
3380
+ - Updated dependencies [8366acc]
3381
+ - Updated dependencies [95e58a3]
3382
+ - Updated dependencies [9d7419b]
3383
+ - Updated dependencies [fc7db05]
3384
+ - Updated dependencies [9ed8d0f]
3385
+ - Updated dependencies [c73cdb5]
3386
+ - Updated dependencies [6f5719e]
3387
+ - Updated dependencies [d0097af]
3388
+ - Updated dependencies [432882b]
3389
+ - Updated dependencies [64dae8e]
3390
+ - Updated dependencies [b06e374]
3391
+ - Updated dependencies [06a8af5]
3392
+ - Updated dependencies [6a91586]
3393
+ - Updated dependencies [a04d7c6]
3394
+ - Updated dependencies [5ccc500]
3395
+ - Updated dependencies [f3c2bb0]
3396
+ - Updated dependencies [978507b]
3397
+ - Updated dependencies [778138e]
3398
+ - Updated dependencies [9801765]
3399
+ - Updated dependencies [9cebfca]
3400
+ - Updated dependencies [460575f]
3401
+ - Updated dependencies [d796c8d]
3402
+ - Updated dependencies [594704f]
3403
+ - Updated dependencies [d3995fe]
3404
+ - Updated dependencies [1b1d772]
3405
+ - Updated dependencies [d88e20f]
3406
+ - Updated dependencies [2d7304d]
3407
+ - Updated dependencies [636b236]
3408
+ - Updated dependencies [4172589]
3409
+ - Updated dependencies [d6d8fb9]
3410
+ - Updated dependencies [64d624d]
3411
+ - Updated dependencies [053fdc8]
3412
+ - Updated dependencies [41b7ce3]
3413
+ - Updated dependencies [ae476b8]
3414
+ - Updated dependencies [39f4309]
3415
+ - Updated dependencies [95bad12]
3416
+ - Updated dependencies [d2fb6ef]
3417
+ - Updated dependencies [7cd3987]
3418
+ - Updated dependencies [ee3b878]
3419
+ - Updated dependencies [e304a4e]
3420
+ - Updated dependencies [fda49e5]
3421
+ - Updated dependencies [490d9a9]
3422
+ - Updated dependencies [fc62bb4]
3423
+ - Updated dependencies [41df893]
3424
+ - Updated dependencies [0cba1b7]
3425
+ - Updated dependencies [00f3eb5]
3426
+ - Updated dependencies [1ec291c]
3427
+ - Updated dependencies [453dbaa]
3428
+ - Updated dependencies [95f8704]
3429
+ - Updated dependencies [f8cdbf2]
3430
+ - Updated dependencies [69a2163]
3431
+ - Updated dependencies [24e027e]
3432
+ - Updated dependencies [2c3cd1b]
3433
+ - Updated dependencies [e176053]
3434
+ - Updated dependencies [e30ed15]
3435
+ - Updated dependencies [90665e0]
3436
+ - Updated dependencies [8d3a529]
3437
+ - Updated dependencies [5ac2e2c]
3438
+ - Updated dependencies [194fae1]
3439
+ - Updated dependencies [7e19d03]
3440
+ - Updated dependencies [b08b7eb]
3441
+ - Updated dependencies [1e946c9]
3442
+ - Updated dependencies [546ddf7]
3443
+ - Updated dependencies [864154e]
3444
+ - Updated dependencies [b023625]
3445
+ - Updated dependencies [75bd83d]
3446
+ - Updated dependencies [44d075b]
3447
+ - Updated dependencies [40c479a]
3448
+ - Updated dependencies [971d387]
3449
+ - Updated dependencies [ee851c3]
3450
+ - Updated dependencies [6414dfd]
3451
+ - Updated dependencies [a8d5c71]
3452
+ - Updated dependencies [905b21f]
3453
+ - Updated dependencies [88e9109]
3454
+ - Updated dependencies [2c45966]
3455
+ - Updated dependencies [db3a600]
3456
+ - Updated dependencies [3a3db76]
3457
+ - Updated dependencies [0d723a3]
3458
+ - Updated dependencies [0c95d3d]
3459
+ - Updated dependencies [3e4fa2c]
3460
+ - Updated dependencies [b5b928a]
3461
+ - Updated dependencies [6fd2cf7]
3462
+ - Updated dependencies [5fa06c4]
3463
+ - Updated dependencies [52a43de]
3464
+ - Updated dependencies [195052f]
3465
+ - Updated dependencies [e4559d1]
3466
+ - Updated dependencies [2c71482]
3467
+ - Updated dependencies [129bcc5]
3468
+ - Updated dependencies [a26b9e4]
3469
+ - Updated dependencies [5ef9c4f]
3470
+ - Updated dependencies [46f0bb4]
3471
+ - Updated dependencies [06b82b8]
3472
+ - Updated dependencies [8ec11e1]
3473
+ - Updated dependencies [6f81384]
3474
+ - Updated dependencies [22ba927]
3475
+ - Updated dependencies [8631c32]
3476
+ - Updated dependencies [f8c70f4]
3477
+ - Updated dependencies [5d3a2d1]
3478
+ - Updated dependencies [8f1d995]
3479
+ - Updated dependencies [b362c1b]
3480
+ - Updated dependencies [f9c34df]
3481
+ - Updated dependencies [dddb942]
3482
+ - Updated dependencies [00c665e]
3483
+ - Updated dependencies [29754cf]
3484
+ - Updated dependencies [d7de534]
3485
+ - Updated dependencies [3c2b6f7]
3486
+ - Updated dependencies [6e88630]
3487
+ - Updated dependencies [b84dc18]
3488
+ - Updated dependencies [ac8abb0]
3489
+ - Updated dependencies [9d86e1d]
3490
+ - Updated dependencies [3a5817f]
3491
+ - Updated dependencies [99a3c2d]
3492
+ - Updated dependencies [5961030]
3493
+ - Updated dependencies [f24de8b]
3494
+ - Updated dependencies [c8ea8af]
3495
+ - Updated dependencies [9602dc8]
3496
+ - Updated dependencies [3190414]
3497
+ - Updated dependencies [4e480f5]
3498
+ - Updated dependencies [38a123c]
3499
+ - Updated dependencies [299102e]
3500
+ - Updated dependencies [30c73cd]
3501
+ - Updated dependencies [830ed58]
3502
+ - Updated dependencies [d7acad6]
3503
+ - Updated dependencies [45a9aeb]
3504
+ - Updated dependencies [713db46]
3505
+ - Updated dependencies [c71e14d]
3506
+ - Updated dependencies [bf3a03c]
3507
+ - Updated dependencies [cb55718]
3508
+ - Updated dependencies [748494b]
3509
+ - Updated dependencies [5967be0]
3510
+ - Updated dependencies [831be72]
3511
+ - Updated dependencies [29cb85b]
3512
+ - Updated dependencies [3e028c8]
3513
+ - Updated dependencies [d0889e2]
3514
+ - Updated dependencies [ce503e5]
3515
+ - Updated dependencies [f20dcf0]
3516
+ - Updated dependencies [12402a9]
3517
+ - Updated dependencies [aff3d7a]
3518
+ - Updated dependencies [4ca30d0]
3519
+ - Updated dependencies [7a5da14]
3520
+ - Updated dependencies [fff9645]
3521
+ - Updated dependencies [9c3b7ce]
3522
+ - Updated dependencies [2c1c967]
3523
+ - Updated dependencies [9486ac6]
3524
+ - Updated dependencies [9486ac6]
3525
+ - Updated dependencies [4d5f9b4]
3526
+ - Updated dependencies [d6ceb8d]
3527
+ - Updated dependencies [dc4365c]
3528
+ - Updated dependencies [e321d52]
3529
+ - Updated dependencies [969ba84]
3530
+ - Updated dependencies [98188c2]
3531
+ - Updated dependencies [4c68077]
3532
+ - Updated dependencies [7977ff9]
3533
+ - Updated dependencies [3beef6d]
3534
+ - Updated dependencies [06b8c42]
3535
+ - Updated dependencies [46b9bc9]
3536
+ - Updated dependencies [f46bd39]
3537
+ - Updated dependencies [b98352a]
3538
+ - Updated dependencies [b76ca67]
3539
+ - Updated dependencies [45ac2cb]
3540
+ - Updated dependencies [b97790a]
3541
+ - Updated dependencies [dbd5194]
3542
+ - Updated dependencies [7c9b044]
3543
+ - Updated dependencies [e552c31]
3544
+ - Updated dependencies [d47de51]
3545
+ - Updated dependencies [3fe6463]
3546
+ - Updated dependencies [b392674]
3547
+ - Updated dependencies [4f3a1e2]
3548
+ - Updated dependencies [31ab372]
3549
+ - Updated dependencies [846889b]
3550
+ - Updated dependencies [2acd8e1]
3551
+ - Updated dependencies [7b90231]
3552
+ - Updated dependencies [26896c6]
3553
+ - Updated dependencies [67fc3b0]
3554
+ - Updated dependencies [8579e34]
3555
+ - Updated dependencies [d57db5d]
3556
+ - Updated dependencies [33a3b3c]
3557
+ - Updated dependencies [b87f15b]
3558
+ - Updated dependencies [045d20b]
3559
+ - Updated dependencies [a2d2515]
3560
+ - Updated dependencies [c18d099]
3561
+ - Updated dependencies [0caacca]
3562
+ - Updated dependencies [adb2a86]
3563
+ - Updated dependencies [03380aa]
3564
+ - Updated dependencies [4562ea5]
3565
+ - Updated dependencies [3619792]
3566
+ - Updated dependencies [3561bd2]
3567
+ - Updated dependencies [bf97b98]
3568
+ - Updated dependencies [320374d]
3569
+ - Updated dependencies [b0d308d]
3570
+ - Updated dependencies [b458300]
3571
+ - Updated dependencies [40f34b4]
3572
+ - Updated dependencies [bd0376d]
3573
+ - Updated dependencies [8063bcb]
3574
+ - Updated dependencies [b74a859]
3575
+ - Updated dependencies [d4493fd]
3576
+ - Updated dependencies [240b80f]
3577
+ - Updated dependencies [77cb489]
3578
+ - Updated dependencies [bfaa158]
3579
+ - Updated dependencies [777e5c6]
3580
+ - Updated dependencies [0c386dd]
3581
+ - Updated dependencies [39d69ad]
3582
+ - Updated dependencies [9e37d9b]
3583
+ - Updated dependencies [5ad86dd]
3584
+ - Updated dependencies [16a725f]
3585
+ - Updated dependencies [4dfdcc3]
3586
+ - Updated dependencies [6a449fc]
3587
+ - Updated dependencies [446d93d]
3588
+ - Updated dependencies [ecd9cb2]
3589
+ - Updated dependencies [f08bcd9]
3590
+ - Updated dependencies [98d4108]
3591
+ - Updated dependencies [0e3b3be]
3592
+ - Updated dependencies [a29ae2d]
3593
+ - Updated dependencies [220c18d]
3594
+ - Updated dependencies [00d3f09]
3595
+ - Updated dependencies [4388f71]
3596
+ - Updated dependencies [0b1ac58]
3597
+ - Updated dependencies [c93b4d5]
3598
+ - Updated dependencies [c1fe272]
3599
+ - Updated dependencies [3cab570]
3600
+ - Updated dependencies [8ad218d]
3601
+ - Updated dependencies [3e41187]
3602
+ - Updated dependencies [5f78953]
3603
+ - Updated dependencies [639114c]
3604
+ - Updated dependencies [639114c]
3605
+ - Updated dependencies [1490691]
3606
+ - Updated dependencies [e8e4c4d]
3607
+ - Updated dependencies [1f31d3a]
3608
+ - Updated dependencies [d1842ab]
3609
+ - Updated dependencies [78ca238]
3610
+ - Updated dependencies [d8ec8d6]
3611
+ - Updated dependencies [351eb31]
3612
+ - Updated dependencies [866cd1d]
3613
+ - Updated dependencies [20c04b2]
3614
+ - Updated dependencies [01c9023]
3615
+ - Updated dependencies [48c19bd]
3616
+ - Updated dependencies [a6d8b8d]
3617
+ - Updated dependencies [4b5bb95]
3618
+ - Updated dependencies [b652514]
3619
+ - Updated dependencies [adbda1b]
3620
+ - Updated dependencies [adbda1b]
3621
+ - Updated dependencies [8952395]
3622
+ - Updated dependencies [e2b3826]
3623
+ - Updated dependencies [0348bc9]
3624
+ - Updated dependencies [e8c553b]
3625
+ - Updated dependencies [2e32ed4]
3626
+ - Updated dependencies [554e647]
3627
+ - Updated dependencies [3ed3eec]
3628
+ - Updated dependencies [7c3df8f]
3629
+ - Updated dependencies [a4514e8]
3630
+ - Updated dependencies [db3896c]
3631
+ - Updated dependencies [b9f5ff1]
3632
+ - Updated dependencies [e75f4c9]
3633
+ - Updated dependencies [19f1639]
3634
+ - Updated dependencies [4704aa4]
3635
+ - Updated dependencies [47547d0]
3636
+ - Updated dependencies [1bee5d0]
3637
+ - Updated dependencies [858cd72]
3638
+ - Updated dependencies [cfc9b6d]
3639
+ - Updated dependencies [554f2b6]
3640
+ - Updated dependencies [72f55c9]
3641
+ - Updated dependencies [26e06d7]
3642
+ - Updated dependencies [669d71b]
3643
+ - Updated dependencies [ed27d7c]
3644
+ - Updated dependencies [52c8cf7]
3645
+ - Updated dependencies [2ceb43a]
3646
+ - Updated dependencies [7cdd2b9]
3647
+ - Updated dependencies [52c8cf7]
3648
+ - Updated dependencies [3399704]
3649
+ - Updated dependencies [71a4a53]
3650
+ - Updated dependencies [7bf244b]
3651
+ - Updated dependencies [f0bb9fa]
3652
+ - Updated dependencies [81a2eb1]
3653
+ - Updated dependencies [caa0cd3]
3654
+ - Updated dependencies [caa0cd3]
3655
+ - Updated dependencies [20cb8db]
3656
+ - Updated dependencies [25c7d58]
3657
+ - Updated dependencies [00d2fa6]
3658
+ - Updated dependencies [77b2a18]
3659
+ - Updated dependencies [c6198c2]
3660
+ - Updated dependencies [721d1e0]
3661
+ - Updated dependencies [c2f0f48]
3662
+ - Updated dependencies [1237ae4]
3663
+ - Updated dependencies [2f61238]
3664
+ - Updated dependencies [51eb515]
3665
+ - Updated dependencies [c354ce5]
3666
+ - Updated dependencies [8fe8e5c]
3667
+ - Updated dependencies [feac439]
3668
+ - Updated dependencies [9ae871d]
3669
+ - Updated dependencies [efbd566]
3670
+ - Updated dependencies [2a5bf45]
3671
+ - Updated dependencies [9587fc9]
3672
+ - Updated dependencies [e62c44e]
3673
+ - Updated dependencies [daf9d57]
3674
+ - Updated dependencies [23b9958]
3675
+ - Updated dependencies [c15d7ec]
3676
+ - Updated dependencies [5d0876c]
3677
+ - Updated dependencies [f7ace0a]
3678
+ - Updated dependencies [b041b9c]
3679
+ - Updated dependencies [ce2aaef]
3680
+ - Updated dependencies [544ecba]
3681
+ - Updated dependencies [2ce2612]
3682
+ - Updated dependencies [bc640ec]
3683
+ - Updated dependencies [1e215c4]
3684
+ - Updated dependencies [da6e191]
3685
+ - Updated dependencies [3e377c9]
3686
+ - Updated dependencies [a3eb5d0]
3687
+ - Updated dependencies [4ce14f1]
3688
+ - Updated dependencies [aef97e5]
3689
+ - Updated dependencies [2af1fa7]
3690
+ - Updated dependencies [c14d3a0]
3691
+ - Updated dependencies [a137d0c]
3692
+ - Updated dependencies [caf477f]
3693
+ - Updated dependencies [f6375da]
3694
+ - Updated dependencies [967e5d8]
3695
+ - Updated dependencies [c907a9c]
3696
+ - Updated dependencies [a4611b3]
3697
+ - Updated dependencies [20316ba]
3698
+ - Updated dependencies [d3499b3]
3699
+ - Updated dependencies [91f9276]
3700
+ - Updated dependencies [309c75e]
3701
+ - Updated dependencies [c9f9bae]
3702
+ - Updated dependencies [18897a4]
3703
+ - Updated dependencies [8b7ea39]
3704
+ - Updated dependencies [a915064]
3705
+ - Updated dependencies [dcbf0b2]
3706
+ - Updated dependencies [52cac38]
3707
+ - Updated dependencies [93fea2e]
3708
+ - Updated dependencies [1422a92]
3709
+ - Updated dependencies [d05fe17]
3710
+ - Updated dependencies [a480f79]
3711
+ - Updated dependencies [f08d1a8]
3712
+ - Updated dependencies [64a252d]
3713
+ - Updated dependencies [786bc91]
3714
+ - Updated dependencies [75fca96]
3715
+ - Updated dependencies [7ca6ddd]
3716
+ - Updated dependencies [f1cd290]
3717
+ - Updated dependencies [5a41ce7]
3718
+ - Updated dependencies [8d50bc2]
3719
+ - Updated dependencies [604476d]
3720
+ - Updated dependencies [d1bebb0]
3721
+ - Updated dependencies [95bf128]
3722
+ - Updated dependencies [335abea]
3723
+ - Updated dependencies [edea22a]
3724
+ - Updated dependencies [0f5cadf]
3725
+ - Updated dependencies [4f9f1ee]
3726
+ - Updated dependencies [66e8b2a]
3727
+ - Updated dependencies [aa083cd]
3728
+ - Updated dependencies [12b5992]
3729
+ - Updated dependencies [b93e245]
3730
+ - Updated dependencies [c842594]
3731
+ - Updated dependencies [290de37]
3732
+ - Updated dependencies [1cfdff8]
3733
+ - Updated dependencies [e1c27e4]
3734
+ - Updated dependencies [8c8da45]
3735
+ - Updated dependencies [8cd8eb5]
3736
+ - Updated dependencies [f52a9d7]
3737
+ - Updated dependencies [cf1d29e]
3738
+ - Updated dependencies [1bd79c8]
3739
+ - Updated dependencies [af9e957]
3740
+ - Updated dependencies [b1030c7]
3741
+ - Updated dependencies [c974edf]
3742
+ - Updated dependencies [ad852b6]
3743
+ - Updated dependencies [6778809]
3744
+ - Updated dependencies [3a3dd28]
3745
+ - Updated dependencies [7fb22a1]
3746
+ - Updated dependencies [ad66d79]
3747
+ - Updated dependencies [0758bd8]
3748
+ - Updated dependencies [ee4d19f]
3749
+ - Updated dependencies [496d31d]
3750
+ - Updated dependencies [7ed9808]
3751
+ - Updated dependencies [0ea7054]
3752
+ - Updated dependencies [ac0e39a]
3753
+ - Updated dependencies [9a853f2]
3754
+ - Updated dependencies [cb847fd]
3755
+ - Updated dependencies [ee70287]
3756
+ - Updated dependencies [3e98e13]
3757
+ - Updated dependencies [a695f50]
3758
+ - Updated dependencies [fc32921]
3759
+ - Updated dependencies [4eaa835]
3760
+ - Updated dependencies [8f9d87a]
3761
+ - Updated dependencies [b1777ae]
3762
+ - Updated dependencies [24845c4]
3763
+ - Updated dependencies [6f864cf]
3764
+ - Updated dependencies [24d1edd]
3765
+ - Updated dependencies [645087c]
3766
+ - Updated dependencies [33f4a19]
3767
+ - Updated dependencies [6e9a3d4]
3768
+ - Updated dependencies [4a292d2]
3769
+ - Updated dependencies [5323168]
3770
+ - Updated dependencies [841dd2b]
3771
+ - Updated dependencies [3014fc0]
3772
+ - Updated dependencies [dacb402]
3773
+ - Updated dependencies [846cec0]
3774
+ - Updated dependencies [91facae]
3775
+ - Updated dependencies [b38014e]
3776
+ - Updated dependencies [474797d]
3777
+ - Updated dependencies [704e695]
3778
+ - Updated dependencies [a407bd6]
3779
+ - Updated dependencies [317dbce]
3780
+ - Updated dependencies [309728c]
3781
+ - Updated dependencies [c03d03b]
3782
+ - Updated dependencies [aa08d7e]
3783
+ - Updated dependencies [3a43a15]
3784
+ - Updated dependencies [868e825]
3785
+ - Updated dependencies [f76f436]
3786
+ - Updated dependencies [ce45a03]
3787
+ - Updated dependencies [421544b]
3788
+ - Updated dependencies [fb01022]
3789
+ - Updated dependencies [e9d9212]
3790
+ - Updated dependencies [ecfb693]
3791
+ - Updated dependencies [639ca9d]
3792
+ - Updated dependencies [2152962]
3793
+ - Updated dependencies [abc1b18]
3794
+ - Updated dependencies [81a51db]
3795
+ - Updated dependencies [67749c7]
3796
+ - Updated dependencies [507b61b]
3797
+ - Updated dependencies [512c84b]
3798
+ - Updated dependencies [c300267]
3799
+ - Updated dependencies [fb3a101]
3800
+ - Updated dependencies [d4733f2]
3801
+ - Updated dependencies [7c9145f]
3802
+ - Updated dependencies [1570eac]
3803
+ - Updated dependencies [f391ede]
3804
+ - Updated dependencies [f5cfbbd]
3805
+ - Updated dependencies [f5cfbbd]
3806
+ - Updated dependencies [8b532cb]
3807
+ - Updated dependencies [64c3cdd]
3808
+ - Updated dependencies [4d65991]
3809
+ - Updated dependencies [c42554e]
3810
+ - Updated dependencies [3e71b26]
3811
+ - Updated dependencies [b89583b]
3812
+ - Updated dependencies [70c4523]
3813
+ - Updated dependencies [555b4ec]
3814
+ - Updated dependencies [64f1cf1]
3815
+ - Updated dependencies [da5e4f6]
3816
+ - Updated dependencies [1ccfc23]
3817
+ - Updated dependencies [542718f]
3818
+ - Updated dependencies [7f27bc5]
3819
+ - Updated dependencies [0a174f3]
3820
+ - Updated dependencies [676f677]
3821
+ - Updated dependencies [f95b140]
3822
+ - Updated dependencies [541ce4e]
3823
+ - Updated dependencies [6479086]
3824
+ - Updated dependencies [d79f525]
3825
+ - Updated dependencies [d1865d2]
3826
+ - Updated dependencies [88a629d]
3827
+ - Updated dependencies [cdd2542]
3828
+ - Updated dependencies [ab77513]
3829
+ - Updated dependencies [55ba3ff]
3830
+ - Updated dependencies [3ecc369]
3831
+ - Updated dependencies [f1190b0]
3832
+ - Updated dependencies [561abef]
3833
+ - Updated dependencies [ef52001]
3834
+ - Updated dependencies [6a4680b]
3835
+ - Updated dependencies [c3a4273]
3836
+ - Updated dependencies [abf710d]
3837
+ - Updated dependencies [093af32]
3838
+ - Updated dependencies [1bd1be7]
3839
+ - Updated dependencies [d234fa9]
3840
+ - Updated dependencies [adf5812]
3841
+ - Updated dependencies [0eaed83]
3842
+ - Updated dependencies [e36acd4]
3843
+ - Updated dependencies [5058336]
3844
+ - Updated dependencies [2f6b2bf]
3845
+ - Updated dependencies [2028b31]
3846
+ - Updated dependencies [63601ab]
3847
+ - Updated dependencies [c372b29]
3848
+ - Updated dependencies [152f0a7]
3849
+ - Updated dependencies [8693b85]
3850
+ - Updated dependencies [e82dad1]
3851
+ - Updated dependencies [58b7b3d]
3852
+ - Updated dependencies [84defab]
3853
+ - Updated dependencies [681d3f1]
3854
+ - Updated dependencies [969d4f2]
3855
+ - Updated dependencies [f3bc481]
3856
+ - Updated dependencies [b79aac2]
3857
+ - Updated dependencies [93fc0e7]
3858
+ - Updated dependencies [a4b723f]
3859
+ - Updated dependencies [2b10ca0]
3860
+ - Updated dependencies [7db4a81]
3861
+ - Updated dependencies [19a0b0e]
3862
+ - Updated dependencies [526fc11]
3863
+ - Updated dependencies [f8e3e9a]
3864
+ - Updated dependencies [b9d47ec]
3865
+ - Updated dependencies [3b6bc69]
3866
+ - Updated dependencies [6214db6]
3867
+ - Updated dependencies [6732df4]
3868
+ - Updated dependencies [fe9e0d0]
3869
+ - Updated dependencies [63fb72c]
3870
+ - Updated dependencies [804831c]
3871
+ - Updated dependencies [279e48e]
3872
+ - Updated dependencies [8700d6d]
3873
+ - Updated dependencies [8db2a0f]
3874
+ - Updated dependencies [689953a]
3875
+ - Updated dependencies [30443fb]
3876
+ - Updated dependencies [8d3dbb2]
3877
+ - Updated dependencies [efc1c9c]
3878
+ - Updated dependencies [da45e6b]
3879
+ - Updated dependencies [7533465]
3880
+ - Updated dependencies [835f0f3]
3881
+ - Updated dependencies [6d5db7b]
3882
+ - Updated dependencies [a9d97be]
3883
+ - Updated dependencies [ed35b44]
3884
+ - Updated dependencies [9ba7e9c]
3885
+ - Updated dependencies [729e851]
3886
+ - Updated dependencies [96919a4]
3887
+ - Updated dependencies [345e24a]
3888
+ - Updated dependencies [20b507a]
3889
+ - Updated dependencies [2e471dc]
3890
+ - Updated dependencies [6748587]
3891
+ - Updated dependencies [be50942]
3892
+ - Updated dependencies [775e079]
3893
+ - Updated dependencies [7e8b3c0]
3894
+ - Updated dependencies [53374dc]
3895
+ - Updated dependencies [f6fb83f]
3896
+ - Updated dependencies [2049b03]
3897
+ - Updated dependencies [2bf34f7]
3898
+ - Updated dependencies [15b33ae]
3899
+ - Updated dependencies [7cbc724]
3900
+ - Updated dependencies [4a94c38]
3901
+ - Updated dependencies [7098eed]
3902
+ - Updated dependencies [3df7c5c]
3903
+ - Updated dependencies [fb91ac9]
3904
+ - Updated dependencies [fb91ac9]
3905
+ - Updated dependencies [641fb55]
3906
+ - Updated dependencies [8524372]
3907
+ - Updated dependencies [7cbefa5]
3908
+ - Updated dependencies [72d6587]
3909
+ - Updated dependencies [a272a4f]
3910
+ - Updated dependencies [55f39ee]
3911
+ - Updated dependencies [0ce32d5]
3912
+ - Updated dependencies [0970a0e]
3913
+ - Updated dependencies [e427e9c]
3914
+ - Updated dependencies [bbc9dc3]
3915
+ - Updated dependencies [02f1813]
3916
+ - Updated dependencies [1ef89c0]
3917
+ - Updated dependencies [ba0b61a]
3918
+ - Updated dependencies [ba0b61a]
3919
+ - Updated dependencies [ac716ff]
3920
+ - Updated dependencies [e05553c]
3921
+ - Updated dependencies [f0f3cd5]
3922
+ - Updated dependencies [ab856ed]
3923
+ - Updated dependencies [f0f2046]
3924
+ - Updated dependencies [20f3e65]
3925
+ - Updated dependencies [bbba098]
3926
+ - Updated dependencies [87af769]
3927
+ - Updated dependencies [3be720e]
3928
+ - Updated dependencies [43c0d17]
3929
+ - Updated dependencies [c3df43a]
3930
+ - Updated dependencies [d16d0e9]
3931
+ - Updated dependencies [bbe57fd]
3932
+ - Updated dependencies [272a530]
3933
+ - Updated dependencies [1779e8d]
3934
+ - Updated dependencies [9aa2a57]
3935
+ - Updated dependencies [f7fcc2c]
3936
+ - Updated dependencies [f0f4d6c]
3937
+ - Updated dependencies [4128188]
3938
+ - Updated dependencies [b253c4e]
3939
+ - Updated dependencies [78a9c67]
3940
+ - Updated dependencies [2e97c8c]
3941
+ - Updated dependencies [4a7ef0d]
3942
+ - Updated dependencies [4598f6d]
3943
+ - Updated dependencies [dea17b4]
3944
+ - Updated dependencies [89bb77a]
3945
+ - Updated dependencies [06611e4]
3946
+ - Updated dependencies [44152c4]
3947
+ - Updated dependencies [3939545]
3948
+ - Updated dependencies [0ee6e31]
3949
+ - Updated dependencies [66abbde]
3950
+ - Updated dependencies [dc3893d]
3951
+ - Updated dependencies [1bbaa16]
3952
+ - Updated dependencies [6ee259a]
3953
+ - Updated dependencies [7649f43]
3954
+ - Updated dependencies [e708426]
3955
+ - Updated dependencies [3b6d53b]
3956
+ - Updated dependencies [e22fa12]
3957
+ - Updated dependencies [1560d46]
3958
+ - Updated dependencies [276d174]
3959
+ - Updated dependencies [2982ed9]
3960
+ - Updated dependencies [741864f]
3961
+ - Updated dependencies [a8198de]
3962
+ - Updated dependencies [0c789a4]
3963
+ - Updated dependencies [05a49f2]
3964
+ - Updated dependencies [b234a84]
3965
+ - Updated dependencies [a78cd37]
3966
+ - Updated dependencies [5ea623e]
3967
+ - Updated dependencies [4d7d322]
3968
+ - Updated dependencies [5eabe86]
3969
+ - Updated dependencies [ca5d671]
3970
+ - Updated dependencies [32bf2d6]
3971
+ - Updated dependencies [ff0c384]
3972
+ - Updated dependencies [af4fb29]
3973
+ - Updated dependencies [c698a81]
3974
+ - Updated dependencies [ff5ef1c]
3975
+ - Updated dependencies [befd40c]
3976
+ - Updated dependencies [9a97800]
3977
+ - Updated dependencies [6bca0e4]
3978
+ - Updated dependencies [81c0bc4]
3979
+ - Updated dependencies [3c76801]
3980
+ - Updated dependencies [60500cb]
3981
+ - Updated dependencies [2fcefb9]
3982
+ - Updated dependencies [77f846a]
3983
+ - Updated dependencies [bc5870c]
3984
+ - Updated dependencies [b55a346]
3985
+ - Updated dependencies [065bba7]
3986
+ - Updated dependencies [f760064]
3987
+ - Updated dependencies [dd19463]
3988
+ - Updated dependencies [6791717]
3989
+ - Updated dependencies [8ea3bee]
3990
+ - Updated dependencies [100547e]
3991
+ - Updated dependencies [3a58149]
3992
+ - Updated dependencies [6d1c155]
3993
+ - Updated dependencies [d7573b3]
3994
+ - Updated dependencies [bf3edfe]
3995
+ - Updated dependencies [2c8474c]
3996
+ - Updated dependencies [6ce89da]
3997
+ - Updated dependencies [0e05aac]
3998
+ - Updated dependencies [ae61ad4]
3999
+ - Updated dependencies [5aed9e4]
4000
+ - Updated dependencies [83c77dc]
4001
+ - Updated dependencies [3c9fca3]
4002
+ - Updated dependencies [18a8e7d]
4003
+ - Updated dependencies [e7957ab]
4004
+ - Updated dependencies [f7e34ca]
4005
+ - Updated dependencies [e719ebd]
4006
+ - Updated dependencies [516583b]
4007
+ - Updated dependencies [f9e4f91]
4008
+ - Updated dependencies [6ef48b1]
4009
+ - Updated dependencies [58be55e]
4010
+ - Updated dependencies [fa429cf]
4011
+ - Updated dependencies [ed8df3e]
4012
+ - Updated dependencies [fe76ece]
4013
+ - Updated dependencies [8b446f5]
4014
+ - Updated dependencies [8e74b27]
4015
+ - Updated dependencies [7102b20]
4016
+ - Updated dependencies [8ebd57f]
4017
+ - Updated dependencies [968dc1e]
4018
+ - Updated dependencies [9a1fb41]
4019
+ - Updated dependencies [617707a]
4020
+ - Updated dependencies [c40f3b8]
4021
+ - Updated dependencies [58770f3]
4022
+ - Updated dependencies [aefe428]
4023
+ - Updated dependencies [485f096]
4024
+ - Updated dependencies [7357447]
4025
+ - Updated dependencies [199d31b]
4026
+ - Updated dependencies [b655a9d]
4027
+ - Updated dependencies [a865c73]
4028
+ - Updated dependencies [3e01cb5]
4029
+ - Updated dependencies [7138bc1]
4030
+ - Updated dependencies [cef27e2]
4031
+ - Updated dependencies [4e8622b]
4032
+ - Updated dependencies [dffd752]
4033
+ - Updated dependencies [06973aa]
4034
+ - Updated dependencies [50798f3]
4035
+ - Updated dependencies [6a576c9]
4036
+ - Updated dependencies [105f3c5]
4037
+ - Updated dependencies [3ccd9e8]
4038
+ - Updated dependencies [689b979]
4039
+ - Updated dependencies [c70f865]
4040
+ - Updated dependencies [e546222]
4041
+ - Updated dependencies [fd13f52]
4042
+ - Updated dependencies [d7bd274]
4043
+ - Updated dependencies [98c3a74]
4044
+ - Updated dependencies [fffa30d]
4045
+ - Updated dependencies [e4e9557]
4046
+ - Updated dependencies [7a28e1e]
4047
+ - Updated dependencies [ebce5a3]
4048
+ - Updated dependencies [fb336df]
4049
+ - Updated dependencies [4dc80d0]
4050
+ - Updated dependencies [9d9040d]
4051
+ - Updated dependencies [20e317c]
4052
+ - Updated dependencies [0fce2ef]
4053
+ - Updated dependencies [42df928]
4054
+ - Updated dependencies [0e2ddd4]
4055
+ - Updated dependencies [b7479ab]
4056
+ - Updated dependencies [9850c6e]
4057
+ - Updated dependencies [de570cc]
4058
+ - Updated dependencies [b2ea297]
4059
+ - Updated dependencies [5b5a5c3]
4060
+ - Updated dependencies [14582b8]
4061
+ - Updated dependencies [51e144e]
4062
+ - Updated dependencies [19cbf10]
4063
+ - Updated dependencies [b6e83be]
4064
+ - Updated dependencies [ab92940]
4065
+ - Updated dependencies [a691c0b]
4066
+ - Updated dependencies [0b1326d]
4067
+ - Updated dependencies [1e66879]
4068
+ - Updated dependencies [c5200f0]
4069
+ - Updated dependencies [af3861f]
4070
+ - Updated dependencies [2609812]
4071
+ - Updated dependencies [515f171]
4072
+ - Updated dependencies [1f4e029]
4073
+ - Updated dependencies [4f14ad7]
4074
+ - Updated dependencies [258d264]
4075
+ - Updated dependencies [cac64b3]
4076
+ - Updated dependencies [4bb940b]
4077
+ - Updated dependencies [8033ad1]
4078
+ - Updated dependencies [fa140b8]
4079
+ - Updated dependencies [71cba28]
4080
+ - Updated dependencies [190fbd0]
4081
+ - Updated dependencies [c00bf28]
4082
+ - Updated dependencies [93127bd]
4083
+ - Updated dependencies [f2158ec]
4084
+ - Updated dependencies [759606e]
4085
+ - Updated dependencies [fd8dace]
4086
+ - Updated dependencies [72ffc34]
4087
+ - Updated dependencies [a51fa0c]
4088
+ - Updated dependencies [51f3d8d]
4089
+ - Updated dependencies [bf28341]
4090
+ - Updated dependencies [78cbdb5]
4091
+ - Updated dependencies [b7543a9]
4092
+ - Updated dependencies [6c6cee7]
4093
+ - Updated dependencies [42887e0]
4094
+ - Updated dependencies [f1690d4]
4095
+ - Updated dependencies [83fe6e7]
4096
+ - Updated dependencies [d1ab06f]
4097
+ - Updated dependencies [38a9568]
4098
+ - Updated dependencies [f90b8fb]
4099
+ - Updated dependencies [91783c4]
4100
+ - Updated dependencies [982885d]
4101
+ - Updated dependencies [dba7d84]
4102
+ - Updated dependencies [ca39427]
4103
+ - Updated dependencies [bd09957]
4104
+ - Updated dependencies [5a07e67]
4105
+ - Updated dependencies [2d36552]
4106
+ - Updated dependencies [45d8288]
4107
+ - Updated dependencies [b2437a7]
4108
+ - Updated dependencies [f157423]
4109
+ - Updated dependencies [7a90afd]
4110
+ - Updated dependencies [eddc1dd]
4111
+ - Updated dependencies [490f482]
4112
+ - Updated dependencies [27308c5]
4113
+ - Updated dependencies [8689166]
4114
+ - Updated dependencies [c9327c9]
4115
+ - Updated dependencies [920165d]
4116
+ - Updated dependencies [9101be5]
4117
+ - Updated dependencies [f53a8d0]
4118
+ - Updated dependencies [30266cf]
4119
+ - Updated dependencies [968dc1e]
4120
+ - Updated dependencies [57f9b07]
4121
+ - Updated dependencies [3c73d99]
4122
+ - Updated dependencies [d91aed9]
4123
+ - Updated dependencies [ed71d9e]
4124
+ - Updated dependencies [7776fc2]
4125
+ - Updated dependencies [e76634c]
4126
+ - Updated dependencies [c86185e]
4127
+ - Updated dependencies [fb96ecb]
4128
+ - Updated dependencies [1170ed1]
4129
+ - Updated dependencies [92814db]
4130
+ - Updated dependencies [4d73b07]
4131
+ - @object-ui/react@17.7.0
4132
+ - @object-ui/core@17.7.0
4133
+ - @object-ui/types@17.7.0
4134
+ - @object-ui/i18n@17.7.0
4135
+ - @object-ui/components@17.7.0
4136
+ - @object-ui/fields@17.7.0
4137
+ - @object-ui/mobile@17.7.0
4138
+ - @object-ui/permissions@17.7.0
4139
+
4140
+ ## 17.6.0
4141
+
4142
+ ### Minor Changes
4143
+
4144
+ - 0a73b51: `ObjectView` and `ListView` now flatten a view's `map` block through a
4145
+ whitelist instead of spreading the whole (untyped) block to the top level.
4146
+
4147
+ Both `case 'map'` flatteners used to build the `object-map` schema with
4148
+ `...(options.map || {})` — a raw spread of an untyped bag
4149
+ (`NamedListView.options?: Record<string, any>`), so any key an author wrote in
4150
+ the `map` block reached the top level unfiltered. `ObjectMap`'s own
4151
+ `FlatMapConfigKeys = Omit<ObjectMapConfig, 'style'>` declares `style` OUT of
4152
+ this flat form (`style` is also `BaseSchema.style`, inline CSS legal on every
4153
+ node), so the two disagreed about the same shape. `style` was the live
4154
+ specimen: `map: { style: '<url>' }` reached the top level as a CSS-shaped
4155
+ `style` key it was never supposed to carry.
4156
+
4157
+ Behavior narrowing, stated because it changes what reaches the flattened
4158
+ schema: a `map` block key that is not one of `ObjectMapConfig`'s declared
4159
+ flat keys (`latitudeField` / `longitudeField` / `locationField` / `titleField`
4160
+ / `descriptionField` / `zoom` / `center`) — including `style` — no longer
4161
+ reaches the top level of the flattened `object-map` schema. This closes a gap
4162
+ rather than removing working behavior: the pinned strict spec view schemas
4163
+ accept no `map` block at all today, so no author-facing surface could reach
4164
+ this path, and `ObjectMap` already stopped reading a top-level `style` as a
4165
+ map style (a dev warning names the correct spelling instead).
4166
+
4167
+ The whitelist is DERIVED from `ObjectMapConfigSchema` (`@object-ui/types/zod`)
4168
+ rather than hand-listed, so the flatteners and the declaration cannot drift
4169
+ apart again — a key added to (or removed from) the schema reaches both
4170
+ flatteners without a second edit. `ObjectMap`'s own `FLAT_MAP_CONFIG_KEYS` is
4171
+ derived from the same schema for the same reason.
4172
+ - f1d4748: Remove the retired `striped` / `bordered` / `virtualScroll` list-view surface
4173
+
4174
+ objectstack#7176 retired `list.striped`, `list.bordered` and `list.virtualScroll`
4175
+ from the spec after measuring every objectui reader as pass-through: each one
4176
+ copied the key onward and no renderer ever applied it. objectui stops declaring,
4177
+ typing and forwarding them.
4178
+
4179
+ Off the chain: the `@objectstack/spec` list-view bridge in `@object-ui/react`,
4180
+ `ListView`'s child-view props in `@object-ui/plugin-list`, both `ObjectView`
4181
+ relays (`@object-ui/plugin-view` and `@object-ui/app-shell`), the `ObjectGridSchema`
4182
+ and `NamedListView` declarations in `@object-ui/types` (interface and zod),
4183
+ `ObjectGrid.component.yml` in `@object-ui/components`, and the page-block
4184
+ inspector's `striped` / `bordered` toggles in the metadata-admin designer.
4185
+
4186
+ Behaviour is unchanged: nothing read these keys, so nothing rendered differently
4187
+ for them. Stored view metadata that still carries one keeps validating — the keys
4188
+ are simply no longer relayed. `ListViewSchema` continues to take the spec's
4189
+ list-view fields by reference, so the protocol's own retirement tombstones
4190
+ arrive with the next `@objectstack/spec` bump and reject the keys at the
4191
+ authoring boundary. Restoring any of the three as live surface requires an
4192
+ implementation card filed first, per the ruling.
4193
+ - b1119ec: Project every declared `recordIdField`, and refuse an action that names no record
4194
+
4195
+ objectstack#8018. An `api` action declaring `recordIdParam` identifies the record
4196
+ it acts on by a row field — `recordIdField`, default `id`. The grid built
4197
+ `$select` from the listView columns, `id` and the predicate refs only, so an
4198
+ action keyed on any other field asked the server for everything except the key
4199
+ naming its own record. The row arrived without it, and the injection was skipped
4200
+ silently: the request went out anyway, minus the parameter. A backend reading a
4201
+ missing selector as "match nothing" then answers success for having changed
4202
+ nothing, so a record-scoped mutation reports success and does nothing.
4203
+
4204
+ Two independent repairs, both in this change:
4205
+
4206
+ - **Projection.** `listViewPredicates` (`@object-ui/core`) now also harvests
4207
+ `recordIdField` from `rowActionDefs`, `bulkActionDefs` and the object's
4208
+ `actions`, spelled as a synthetic `record.<name>` so the one existing harvester
4209
+ handles it. Both projection builders — `ObjectGrid` and `ListView` — read that
4210
+ function, so both gain the key with no call-site change. The existing guards
4211
+ still apply: a name the object does not declare, or one that is not a bare
4212
+ identifier, is dropped rather than put in `$select`, because an unknown key
4213
+ there is not ignored by every backend.
4214
+ - **Loud failure.** New `resolveRecordIdParamSeed` (`@object-ui/core`) is the one
4215
+ definition of "can this row identify the record?". `useConsoleActionRuntime`'s
4216
+ api handler now refuses the dispatch — `{ success: false, error }`, before the
4217
+ request — when the row lacks the key, or holds `null` for it. The two refusals
4218
+ are worded differently because they point at different repairs: an absent key
4219
+ is a projection or read-visibility problem, a null value is a data one. Falsy
4220
+ real values (`0`, `''`, `false`) are values and still dispatch.
4221
+
4222
+ The second half is what closes the class rather than the common case: a row can
4223
+ lack the key for reasons projection cannot fix — a server-side read mask that
4224
+ strips the field regardless of `$select`, a partial payload, a field the
4225
+ principal cannot read.
4226
+
4227
+ Behaviour change worth noting: an action that previously dispatched an
4228
+ under-specified request now fails visibly instead. That is the point — the old
4229
+ path could not report the failure it was causing.
4230
+
4231
+ ### Patch Changes
4232
+
4233
+ - 516663d: RecordAttachmentsPanel no longer offers a Retry for an api-disabled `sys_attachment` read.
4234
+
4235
+ `OBJECT_API_DISABLED` (404, `enable.apiEnabled: false`) and its sibling
4236
+ `OBJECT_API_METHOD_NOT_ALLOWED` (405, the operation is absent from
4237
+ `enable.apiMethods`) are pure functions of the object's metadata — no user, no
4238
+ session, no request body — so every retry of every persona re-fetches the
4239
+ identical refusal. Before this change both landed in `RecordAttachmentsPanel`'s
4240
+ `unavailable` state and offered a Retry that was guaranteed to change nothing,
4241
+ the same wrong advice `ListView`'s error panel already stops giving for list
4242
+ reads.
4243
+
4244
+ The panel gains a fifth status, `api-unavailable`: no Retry button, and honest
4245
+ copy ("The attachments list is not available on this object.", new
4246
+ `detail.attachmentsApiUnavailable` key in all ten locale packs) instead of
4247
+ "We couldn't load the attachments for this record." The pre-existing `denied`
4248
+ (authorization) and `unavailable` (network/5xx/expired-session) states and
4249
+ their affordances are unchanged.
4250
+
4251
+ `ListView.classifyLoadError` — the classifier that already separated this case
4252
+ into its own `api-disabled` kind for list views — is lifted out of
4253
+ `packages/plugin-list/src/ListView.tsx`'s module scope into
4254
+ `@object-ui/react` (`classifyLoadError`, `LoadErrorKind`), so both surfaces
4255
+ consume one classification instead of `RecordAttachmentsPanel` re-deriving it.
4256
+ `ListView`'s own behavior is unchanged — it now imports the function it
4257
+ previously defined locally. The classifier delegates its api-disabled check to
4258
+ `isApiAccessDeniedError` (`@object-ui/data-objectstack`), removing a second,
4259
+ independently-maintained copy of the same code list.
4260
+ - a1609a6: Console list filters: a `between` range is submitted only when both bounds are filled, and six operator labels stop rendering as raw i18n keys.
4261
+
4262
+ Two defects in the list-view filter panel (objectstack#8815), both in the Console
4263
+ render layer, with no workaround available downstream.
4264
+
4265
+ **A half-filled range no longer refuses the whole view.** Picking a date column
4266
+ and 「介于」 draws two inputs — that part landed in objectui#3958 — but typing
4267
+ only one bound produced `["2024-01-01", ""]`, and both write paths read "is this
4268
+ row filled in?" with one shape-blind predicate (`null` / `''` / empty array).
4269
+ An array of length 2 passed it, so the empty bound went to the server, which
4270
+ refuses the query outright (`400 INVALID_FILTER`): the list showed
4271
+ 「该视图的查询被拒绝」 and the filters the user had already applied stopped
4272
+ applying too. The saved-view fold persisted the same half-range, so the refusal
4273
+ came back on every later read of that view, for every user of it.
4274
+
4275
+ The spec cannot intercept this — `ViewFilterRuleSchema` accepts
4276
+ `["2024-01-01", ""]` because it counts the two slots rather than what is in
4277
+ them, while refusing a scalar or a one-element array. Authoring validation is
4278
+ therefore green on exactly the shape that fails at query time, which makes not
4279
+ emitting it the producer's job. `@object-ui/components` now exports
4280
+ `isFilterValueComplete(operator, value)` — arity-aware, so a `pair` row needs
4281
+ both bounds — and the two consumers that had each kept a copy of the old
4282
+ predicate (`plugin-list`'s `convertFilterGroupToAST`, `app-shell`'s
4283
+ `foldFilterGroupToSpecRules`) read it instead. A half-filled range is now
4284
+ dropped exactly as a half-typed `equals` row already was: no filter, rather than
4285
+ a filter the server will reject. Bounds of `0` and `false` stay real bounds.
4286
+
4287
+ **Six operator labels are translated in all ten locale packs.**
4288
+ `startsWith`, `endsWith`, `isNull`, `isNotNull`, `exists` and `notExists` were
4289
+ missing from every pack, so i18next resolved them to the raw key and the dropdown
4290
+ showed `filterBuilder.operators.isNull` beside translated entries. The
4291
+ component's own defaults table could not cover it: that table serves only the
4292
+ no-provider path, and the Console mounts a provider. The report named four —
4293
+ a `date` column's bucket offers the four nullness operators; a `text` column
4294
+ showed all six.
4295
+
4296
+ Because the label key is built dynamically (`t(\`filterBuilder.operators.${op}\`)`),
4297
+ no existing gate could see the gap: the call-site checker classifies a template
4298
+ key as `missing-prefix` and only asks whether the prefix resolves, and
4299
+ cross-pack parity is satisfied when all ten packs are missing a key together.
4300
+ A new parity test pins the packs against `FILTER_BUILDER_OPERATORS` in both
4301
+ directions, so an operator added to the dropdown now fails loudly until every
4302
+ pack labels it.
4303
+ - c1ef923: Grid and related-list column headers no longer offer a sort on a `formula` column.
4304
+
4305
+ A `formula` value is computed on read: no driver materialises a column for it, so
4306
+ a server `$orderby` naming one has nothing to order by. That sort never worked.
4307
+ Until objectstack#6994 the platform did not say so — the response carried the very
4308
+ values it had been asked to order by, out of order, under a `200`, with ascending
4309
+ and descending byte-identical on a real SQL driver — and it now answers
4310
+ `400 INVALID_SORT`. So the header was wrong before the platform's refusal and is
4311
+ wrong after it, for the same reason: it offers a sort that cannot be performed.
4312
+
4313
+ `ObjectGrid` withheld the affordance only from reference-bearing columns
4314
+ (objectui#3096). Unmaterialized types are a SECOND reason a server sort is
4315
+ impossible, not a different mechanism, so it now reads both — and so do the two
4316
+ sort entry points of a related list (the embedded table's headers and the
4317
+ sort-button row a `data-list` card keeps), which each derived that rule
4318
+ separately.
4319
+
4320
+ Client-side sorting is deliberately unchanged. There the rows are all in the
4321
+ browser and the formula value is the one the server hydrated on read, so ordering
4322
+ by what the cell shows is honest — the same split the relational carve-out makes.
4323
+ A sort DECLARED in view metadata is also unchanged: it still goes out and is still
4324
+ refused by name, because silently dropping an author's declaration would hide the
4325
+ authoring error instead of surfacing it (the toolbar's sort picker keeps such a
4326
+ field listed for exactly that reason — it is the only way to remove it).
4327
+
4328
+ The membership — `formula` alone — moved out of a private set in `ListView` into
4329
+ `@object-ui/core` (`UNMATERIALIZED_FIELD_TYPES` / `isUnmaterializedFieldType`),
4330
+ bound to `@objectstack/spec`'s own storage predicate so the renderer cannot drift
4331
+ from what the drivers actually store. It is deliberately narrower than the spec's
4332
+ write contract `COMPUTED_VALUE_TYPES`: a `summary` and an `autonumber` each get a
4333
+ real maintained column and sort correctly, and withholding their headers would
4334
+ have broken two affordances that work.
4335
+ - 616a2a5: The list filter builder no longer offers `Is set` / `Is not set`, which its query dialects cannot express.
4336
+
4337
+ **User-visible before/after.** The operator dropdown in the list toolbar's
4338
+ filter popover — and in the Studio view/tab/page filter inspectors, the dataset
4339
+ inspector and the generic `filter` config field — loses two rows: **"Is set"**
4340
+ and **"Is not set"**. The sharing-rule criteria builder (`FilterConditionField`)
4341
+ keeps both, unchanged. `Is null` / `Is not null` and `Is empty` / `Is not empty`
4342
+ are untouched everywhere and remain the way to filter on a missing value from
4343
+ the list.
4344
+
4345
+ Nothing that worked stops working. Every save path behind those two rows was
4346
+ already broken, in three different ways depending on the surface:
4347
+
4348
+ - **Live grid** — `ListView.mapOperator` had no row for either id, so its
4349
+ `default:` arm returned the id verbatim and the query went out as
4350
+ `['name', 'exists', 'x']`. `exists` is not a member of the spec's
4351
+ `VALID_AST_OPERATORS`, so `isFilterAST()` rejects the shape: an unfiltered
4352
+ read or a 400, never the filter the user asked for.
4353
+ - **Save as view** — `foldFilterGroupToSpecRules` normalizes through the spec's
4354
+ `normalizeFilterOperator`, which does not know the pair, and
4355
+ `ViewFilterRuleSchema`'s enum then refuses the rule.
4356
+ - **Dataset inspector** — `groupToCondition` has no row either and drops the
4357
+ condition silently, so the filter simply never applied.
4358
+
4359
+ **Why withheld rather than mapped.** Measured on `@objectstack/spec`
4360
+ 17.0.0-rc.6: neither `VIEW_FILTER_OPERATORS` nor `VALID_AST_OPERATORS` contains
4361
+ an existence operator, under any spelling — both sets have zero members matching
4362
+ `/exist/`. Only the MongoDB-style `FieldOperatorsSchema` criteria carries
4363
+ `$exists`, and that is precisely the dialect `FilterConditionField` writes, so
4364
+ the pair moves behind the existing `OPT_IN_OPERATORS` gate and that widget opts
4365
+ in. Collapsing them onto `isNotNull` / `isNull` was rejected: the builder
4366
+ already draws those as their own rows, the round trip is lossy (a saved
4367
+ `exists` reads back as `isNotNull`), and the spec's own note records `$exists` =
4368
+ has-value as still unsettled across drivers — `driver-memory`'s live mingo path
4369
+ and `driver-mongodb` read key-presence.
4370
+
4371
+ **The class is now closed by an assertion, not by discipline.** objectui's three
4372
+ existing operator-parity guards all sweep spec vocabulary → objectui; none asked
4373
+ whether an id the dropdown draws is an id the consumer can persist, which is the
4374
+ direction that broke. `plugin-list`'s new
4375
+ `list-offered-operator-expressible-parity.test.ts` forces the set the list
4376
+ toolbar offers to **equal** the set its two dialects can express, in both
4377
+ directions — so an unexpressible operator cannot be offered, and an operator
4378
+ that becomes expressible upstream cannot stay needlessly withheld.
4379
+ - dfc6975: Related-list "+ New" now honours `userActions.create` predicates, and the grid
4380
+ toolbar's inline-edit affordance is gated on `update` permission (objectui#4646,
4381
+ objectui#4647).
4382
+
4383
+ Two declared-but-unenforced gaps on the same toolbar surface.
4384
+
4385
+ **#4646 — `createPredicates` had a producer and no consumer.**
4386
+ `@objectstack/spec@17.0.0` widened `userActions.create` to
4387
+ `z.union([z.boolean(), RowCrudActionOverrideSchema])`, so `resolveCrudAffordances`
4388
+ emits `createPredicates` — and nothing in objectui read them, against roughly
4389
+ fifteen consumption sites apiece for `editPredicates` / `deletePredicates`. The
4390
+ symptom: a parent record entering a frozen state correctly greyed its children's
4391
+ row Edit/Delete while the related list's "+ New" stayed fully live, so the user
4392
+ filled in the whole child form to earn a server 409. The related-list toolbar now
4393
+ evaluates `visibleWhen` / `disabledWhen` **once against the host parent record**,
4394
+ per the spec docblock's binding for this key, on top of the existing
4395
+ `o.create ∧ can(child, 'create')` check. `visibleWhen` hides "+ New" and fails
4396
+ CLOSED; `disabledWhen` greys it and fails SOFT — the same evaluator, fail
4397
+ directions and hidden-vs-disabled split the record header already uses for
4398
+ edit/delete (objectui#4419 / PR #4515). A bare-boolean `userActions.create` is
4399
+ untouched: with no predicates there is nothing to evaluate.
4400
+
4401
+ **#4647 — the inline-edit toggle was the one ungated affordance on its toolbar.**
4402
+ It rendered on "grid view ∧ the host wired `onInlineEditChange` ∧ not the compact
4403
+ toolbar", and every host wires that callback unconditionally. New and Import are
4404
+ hidden for an account without the grant and the bulk-delete entry on the same
4405
+ toolbar ANDs `can(obj, 'delete')`, but a read-only principal could flip inline
4406
+ edit, modify cells and press "Save all" to earn a server 403. It is now gated on
4407
+ the object's resolved edit affordance ∧ `can(object, 'update')`, mirroring that
4408
+ bulk-delete gate. The gate is applied at all three sites that carry this
4409
+ affordance — the wide toolbar's toggle, the compact toolbar's settings-popover
4410
+ entry (which previously had no gate at all, not even the callback), and the
4411
+ `editable` mode handed to the grid, so a stored view carrying `inlineEdit: true`
4412
+ can no longer drop a read-only principal into editable cells with no toggle to
4413
+ press.
4414
+
4415
+ `ListViewSchema.userActions.editInline` is also consumed now: an explicit `false`
4416
+ withholds the affordance wholesale, which authors previously could not do.
4417
+
4418
+ **Behaviour change for read-only users, stated plainly.** Where the UI used to
4419
+ offer inline editing and let the server refuse it, it now declines to offer the
4420
+ entry point at all. No data access changes — the server gate was and remains the
4421
+ enforcement boundary; this only stops the UI walking users into round-trips
4422
+ guaranteed to fail. Accounts *with* the grant see no change, and hosts with no
4423
+ `PermissionProvider` mounted (standalone embeds, the Studio designer) keep
4424
+ today's behaviour, since `can()` answers `true` there by design.
4425
+
4426
+ One deliberate non-change: the absent case of `userActions.editInline` defers to
4427
+ the host's existing `inlineEdit` channel rather than enforcing the spec's
4428
+ `.default(false)`. Enforcing that default would remove the toggle from every
4429
+ stored console list view in one release, since nothing folds a legacy key into
4430
+ `editInline` and no existing view declares it. This follows the rule the
4431
+ surrounding toolbar-flag block already states for itself — defaults chosen to
4432
+ match what the flags have always done. `InterfaceListPage`, the key's other
4433
+ consumer, reads the absent case as OFF, because the ADR-0047 interface page has
4434
+ no such host channel to defer to.
4435
+ - 138ab04: Fix a list filter that silently applied nothing when the first thing you picked was **Is null** or **Is not null**.
4436
+
4437
+ Adding a filter seeds the row with no value, and changing the operator keeps it that way — which is correct for an operator that takes no value, since the panel draws no input for one. The live grid read that row as unfinished and dropped it: the filter appeared in the panel, the query went out without it, and every record came back with no error to explain it. Only `Is empty` / `Is not empty` were exempt; the builder renders six operators value-less.
4438
+
4439
+ Which operators are complete without a value now has a single owner — `VALUELESS_FILTER_BUILDER_OPERATORS`, exported from `@object-ui/components` beside the code that decides it. The live grid and the saved-view fold both read it instead of keeping their own lists, so the two halves of one interaction can no longer disagree about whether a row is finished.
4440
+ - Updated dependencies [88085e3]
4441
+ - Updated dependencies [69251bf]
4442
+ - Updated dependencies [57e668f]
4443
+ - Updated dependencies [516663d]
4444
+ - Updated dependencies [41ac1b7]
4445
+ - Updated dependencies [1eaf0a1]
4446
+ - Updated dependencies [a09bc33]
4447
+ - Updated dependencies [460c4d0]
4448
+ - Updated dependencies [0ae27f7]
4449
+ - Updated dependencies [2533ec5]
4450
+ - Updated dependencies [78c0f9a]
4451
+ - Updated dependencies [bbe8b86]
4452
+ - Updated dependencies [8477be5]
4453
+ - Updated dependencies [279fb13]
4454
+ - Updated dependencies [2e82ab2]
4455
+ - Updated dependencies [ad07b65]
4456
+ - Updated dependencies [41f498b]
4457
+ - Updated dependencies [ef0d150]
4458
+ - Updated dependencies [f34226e]
4459
+ - Updated dependencies [564b605]
4460
+ - Updated dependencies [e1d4251]
4461
+ - Updated dependencies [40d3a33]
4462
+ - Updated dependencies [8b9dc62]
4463
+ - Updated dependencies [1184192]
4464
+ - Updated dependencies [a2a9747]
4465
+ - Updated dependencies [65e88e6]
4466
+ - Updated dependencies [a1609a6]
4467
+ - Updated dependencies [53f23bc]
4468
+ - Updated dependencies [c4533dc]
4469
+ - Updated dependencies [be60815]
4470
+ - Updated dependencies [37f6844]
4471
+ - Updated dependencies [93de4f6]
4472
+ - Updated dependencies [2b50261]
4473
+ - Updated dependencies [384f30d]
4474
+ - Updated dependencies [ac600e5]
4475
+ - Updated dependencies [97fba31]
4476
+ - Updated dependencies [232f61a]
4477
+ - Updated dependencies [d374caf]
4478
+ - Updated dependencies [5673576]
4479
+ - Updated dependencies [c1ef923]
4480
+ - Updated dependencies [911ceaa]
4481
+ - Updated dependencies [98eab36]
4482
+ - Updated dependencies [af5e292]
4483
+ - Updated dependencies [3fbbea1]
4484
+ - Updated dependencies [0bffb18]
4485
+ - Updated dependencies [800f455]
4486
+ - Updated dependencies [5458414]
4487
+ - Updated dependencies [3241559]
4488
+ - Updated dependencies [7f96b10]
4489
+ - Updated dependencies [167ec42]
4490
+ - Updated dependencies [616a2a5]
4491
+ - Updated dependencies [6c68b13]
4492
+ - Updated dependencies [0046d8f]
4493
+ - Updated dependencies [f1d4748]
4494
+ - Updated dependencies [bea374e]
4495
+ - Updated dependencies [b1119ec]
4496
+ - Updated dependencies [5607092]
4497
+ - Updated dependencies [9f23d2b]
4498
+ - Updated dependencies [578e025]
4499
+ - Updated dependencies [af025ee]
4500
+ - Updated dependencies [d109a4d]
4501
+ - Updated dependencies [598c89a]
4502
+ - Updated dependencies [4a0bd17]
4503
+ - Updated dependencies [b8b9af4]
4504
+ - Updated dependencies [d8b9259]
4505
+ - Updated dependencies [31676be]
4506
+ - Updated dependencies [8c0d52e]
4507
+ - Updated dependencies [aff10e2]
4508
+ - Updated dependencies [70a774b]
4509
+ - Updated dependencies [9ce096f]
4510
+ - Updated dependencies [e05db88]
4511
+ - Updated dependencies [7458a41]
4512
+ - Updated dependencies [ad13d63]
4513
+ - Updated dependencies [5ffcc14]
4514
+ - Updated dependencies [d971e51]
4515
+ - Updated dependencies [97abb24]
4516
+ - Updated dependencies [deb157a]
4517
+ - Updated dependencies [9c60144]
4518
+ - Updated dependencies [e7747f1]
4519
+ - Updated dependencies [d2ce342]
4520
+ - Updated dependencies [9695da7]
4521
+ - Updated dependencies [ac2f332]
4522
+ - Updated dependencies [a777058]
4523
+ - Updated dependencies [75444e3]
4524
+ - Updated dependencies [58b8346]
4525
+ - Updated dependencies [2d0bd16]
4526
+ - Updated dependencies [a9e17b4]
4527
+ - Updated dependencies [b8ce7dc]
4528
+ - Updated dependencies [dad51e5]
4529
+ - Updated dependencies [1c9c342]
4530
+ - Updated dependencies [787c738]
4531
+ - Updated dependencies [8396656]
4532
+ - Updated dependencies [dbbd38a]
4533
+ - Updated dependencies [61556dc]
4534
+ - Updated dependencies [8871c14]
4535
+ - Updated dependencies [93fe362]
4536
+ - Updated dependencies [dfc6975]
4537
+ - Updated dependencies [3cf4de0]
4538
+ - Updated dependencies [c9dc811]
4539
+ - Updated dependencies [144ef9b]
4540
+ - Updated dependencies [138ab04]
4541
+ - Updated dependencies [a0b9e91]
4542
+ - Updated dependencies [99bd015]
4543
+ - Updated dependencies [21e4585]
4544
+ - @object-ui/types@17.6.0
4545
+ - @object-ui/fields@17.6.0
4546
+ - @object-ui/i18n@17.6.0
4547
+ - @object-ui/react@17.6.0
4548
+ - @object-ui/components@17.6.0
4549
+ - @object-ui/core@17.6.0
4550
+ - @object-ui/permissions@17.6.0
4551
+ - @object-ui/mobile@17.6.0
4552
+
3
4553
  ## 17.5.0
4
4554
 
5
4555
  ### Minor Changes