@object-ui/core 16.1.0 → 17.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +1881 -4
  2. package/dist/actions/ActionEngine.d.ts +11 -1
  3. package/dist/actions/ActionEngine.js +14 -3
  4. package/dist/actions/ActionRunner.d.ts +199 -24
  5. package/dist/actions/ActionRunner.js +189 -84
  6. package/dist/actions/actionKeys.d.ts +135 -0
  7. package/dist/actions/actionKeys.js +283 -0
  8. package/dist/actions/index.d.ts +1 -0
  9. package/dist/actions/index.js +1 -0
  10. package/dist/adapters/ApiDataSource.js +10 -1
  11. package/dist/adapters/ValueDataSource.d.ts +2 -2
  12. package/dist/evaluator/fieldRules.d.ts +0 -2
  13. package/dist/evaluator/fieldRules.js +3 -2
  14. package/dist/evaluator/optionLint.js +5 -2
  15. package/dist/evaluator/optionRules.d.ts +13 -6
  16. package/dist/evaluator/optionRules.js +0 -23
  17. package/dist/index.d.ts +4 -0
  18. package/dist/index.js +7 -0
  19. package/dist/query/query-ast.d.ts +6 -6
  20. package/dist/query/query-ast.js +2 -2
  21. package/dist/registry/PluginSystem.d.ts +15 -5
  22. package/dist/registry/PluginSystem.js +1 -1
  23. package/dist/registry/Registry.d.ts +81 -2
  24. package/dist/registry/Registry.js +102 -2
  25. package/dist/registry/WidgetRegistry.d.ts +7 -7
  26. package/dist/registry/WidgetRegistry.js +1 -1
  27. package/dist/registry/public-blocks.js +52 -1
  28. package/dist/styling/scoped-styles.d.ts +14 -8
  29. package/dist/theme/ThemeEngine.js +0 -12
  30. package/dist/utils/chart-series.d.ts +42 -2
  31. package/dist/utils/chart-series.js +55 -0
  32. package/dist/utils/column-identity.d.ts +110 -0
  33. package/dist/utils/column-identity.js +210 -0
  34. package/dist/utils/dashboard-filters.d.ts +11 -2
  35. package/dist/utils/dashboard-filters.js +99 -4
  36. package/dist/utils/dataset-format.d.ts +13 -1
  37. package/dist/utils/dataset-format.js +10 -2
  38. package/dist/utils/expand-fields.js +8 -8
  39. package/dist/utils/filter-converter.d.ts +47 -0
  40. package/dist/utils/filter-converter.js +94 -7
  41. package/dist/utils/filter-tokens.d.ts +108 -0
  42. package/dist/utils/filter-tokens.js +180 -0
  43. package/dist/utils/freeze-schema.d.ts +14 -6
  44. package/dist/utils/freeze-schema.js +14 -6
  45. package/dist/utils/managedBy.d.ts +97 -51
  46. package/dist/utils/managedBy.js +117 -53
  47. package/dist/utils/normalize-list-view.d.ts +109 -0
  48. package/dist/utils/normalize-list-view.js +255 -0
  49. package/dist/utils/sort-values.d.ts +52 -0
  50. package/dist/utils/sort-values.js +125 -0
  51. package/dist/validation/index.d.ts +10 -1
  52. package/dist/validation/index.js +10 -1
  53. package/dist/validation/required-presence.d.ts +31 -0
  54. package/dist/validation/required-presence.js +36 -0
  55. package/dist/validation/schema-validator.d.ts +24 -8
  56. package/dist/validation/validators/index.d.ts +7 -1
  57. package/dist/validation/validators/index.js +7 -1
  58. package/dist/validation/validators/object-validation-engine.d.ts +151 -31
  59. package/dist/validation/validators/object-validation-engine.js +275 -142
  60. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,1882 @@
1
1
  # @object-ui/core
2
2
 
3
+ ## 17.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bca45cc: Declare the 18 spec-owned action keys `ActionDef` had been absorbing silently.
8
+
9
+ `ActionDef` ends with `[key: string]: any`, so it accepted any key of any type —
10
+ a typo (`targt`) and a retired spec key (`execute`) both type-checked, then the
11
+ runner silently bound no handler (the #2169 "Mark Done does nothing" shape).
12
+ Step 1 (objectstack#4075) made that audible with a dev-mode warning. This is
13
+ step 2: the keys the warning identified as legitimate are now real fields.
14
+
15
+ - **18 keys promoted to explicit optional fields** — `ai`, `aria`, `bodyExtra`,
16
+ `bodyShape`, `bulkEnabled`, `component`, `icon`, `locations`, `mode`,
17
+ `objectName`, `order`, `recordIdField`, `recordIdParam`, `requiredPermissions`,
18
+ `requiresFeature`, `shortcut`, `variant`, `visible`. Every type is **derived**
19
+ from `@objectstack/spec`'s `ActionInput` (`SpecActionInput['locations']`, …),
20
+ never hand-copied: a hand-written duplicate of a spec shape is a second
21
+ contract that drifts, which is the failure this issue is about. Wrong-typed
22
+ values are now compile errors — `order: 'first'`, `variant: 'chartreuse'`,
23
+ `locations: ['nope']` — where before they were absorbed silently.
24
+ - **Derived from `z.input`, not `z.infer`.** `ActionSchema` is a `ZodPipe` whose
25
+ transform narrows `visible` from `string | { dialect, source }` to the
26
+ envelope alone. This runner consumes authored/stored rows, which are
27
+ rehydrated unparsed, so it sees the input shape; deriving from the inferred
28
+ `Action` would have rejected the raw-string predicate `ActionEngine`
29
+ explicitly supports.
30
+ - **Three `as any` casts deleted** in `ActionEngine` — `visible` and
31
+ `requiredPermissions` at the location filter, `locations` at registration.
32
+ They existed only because the fields were undeclared.
33
+ - **Four objectui-dialect keys marked `@deprecated`** with the spec spelling to
34
+ use instead — `actionType` (→ `type`), `api` and `endpoint` (→ `target`;
35
+ `executeAPI` already resolves `api || endpoint || target`), and `navigate`
36
+ (→ flat `target` / `openIn`). Only these four: the remaining dialect keys are
37
+ runner mechanics (chaining, toasts, post-execution reload/close) with no spec
38
+ counterpart, and pointing them at a spelling that does not exist would be
39
+ worse than leaving them declared.
40
+
41
+ **Breaking edge, deliberate.** `shortcut` and `bulkEnabled` were retired by
42
+ `@objectstack/spec` 17 as `retiredKey()` tombstones (`z.never()`), so authoring
43
+ either is already a hard parse rejection. Deriving their types rather than
44
+ hand-writing them turns that runtime rejection into a **compile error**: code
45
+ that assigned `shortcut: 'ctrl+k'` to an `ActionDef` compiled before and does
46
+ not now. Such metadata was already refused by the platform — this only moves the
47
+ failure to where it can be fixed. A host may still pass either explicitly via
48
+ `ActionEngine.registerAction(action, { shortcut, bulkEnabled })`; only authored
49
+ metadata stopped carrying them. `bulkEnabled`'s replacement is the list view's
50
+ `bulkActions` / `bulkActionDefs`; `shortcut` has none.
51
+
52
+ The index signature **stays** — removing it is step 3, and the inverted pin
53
+ asserting it is still present remains the issue's own completion check.
54
+
55
+ - 4bf612c: Aggregate single-call mode for bulk actions: `execution: 'aggregate'` (objectui#3139).
56
+
57
+ A `bulkActionDefs` entry with `operation: 'custom'` used to have exactly one
58
+ dispatch shape: one action-runner call per selected record (`_rowRecord`
59
+ attached). "Select N rows → ONE call that receives every selected id" — the
60
+ zip-of-QR-codes / merged-PDF / batch-print shape — could not be expressed, so
61
+ downstream projects fell back to per-row `window.open` storms or gave up.
62
+
63
+ `BulkActionDef` now carries `execution?: 'perRecord' | 'aggregate'` (default
64
+ `'perRecord'`, existing views untouched). An aggregate def dispatches its
65
+ action exactly once for the whole selection with `params._selectedIds:
66
+ string[]` injected and the full records published as
67
+ `context.selectedRecords`. The authored form usually just names a declared
68
+ object action — `{ name, operation: 'custom', execution: 'aggregate' }` —
69
+ and `resolveBulkActions` attaches the declaration. Results are
70
+ all-or-nothing: a failure is attributed to every id with the real error and
71
+ per-row Retry is hidden (re-running the action is the retry; a total failure
72
+ keeps the selection). `batchSize` does not apply; `maxRecords` still gates.
73
+
74
+ The executor rides the existing `executeBulkBatch` bulk-first decision tree —
75
+ the aggregate call is its `bulkCall`, and the per-row "fallback" only
76
+ re-throws the captured error for attribution, never fans out N dispatches
77
+ against an endpoint written for one `_selectedIds` call.
78
+
79
+ Also: url/api target interpolation now exposes `${ctx.selection.ids}` (comma
80
+ -joined) and `${ctx.selection.count}` from the grid's checkbox selection, so
81
+ a plain `list_toolbar` action can carry the selection without bulk plumbing;
82
+ the console's server-action handler recognizes `_selectedIds` and skips the
83
+ single-record multi-select guard for aggregate dispatches.
84
+
85
+ - 335041c: Stop declaring 13 `@object-ui/core` symbols under names `@objectstack/spec` owns
86
+ (objectui#3158, objectstack#4115 batch 4).
87
+
88
+ **Breaking for importers of `@object-ui/core`** — seven exported names changed,
89
+ because the spec exports the same name for a _different_ thing:
90
+
91
+ | was | now | what the spec's same-named export actually is |
92
+ | :----------------------- | :-------------------------------- | :--------------------------------------------------------------------------- |
93
+ | `ChartSeries` | `ChartSeriesBinding` | the authored dataset-binding descriptor (a measure `name`, no `data`) |
94
+ | `ActionHandler` | `ActionRunnerHandler` | the SERVER-side objectql handler, `(ctx) => unknown` |
95
+ | `PluginDefinition` | `RegistryPluginDefinition` | the platform PACKAGE manifest (`id`/`slug`/`staticPath`/install hooks) |
96
+ | `ValidationError` | `SchemaNodeValidationError` | plugin-manifest validation, keyed by `field`, no severity |
97
+ | `ValidationResult` | `SchemaNodeValidationResult` | ditto, with both arrays optional |
98
+ | `defineView` | `defineSystemView` | the VIEW-DOCUMENT factory: parses a `ViewSchema`, returns a validated `View` |
99
+ | `resolveCrudAffordances` | `resolveEffectiveCrudAffordances` | the object-level affordance matrix, with no notion of server API operations |
100
+
101
+ The other six keep their names and are now **imported from the spec** instead of
102
+ re-declared: `StyleMap`, `ResponsiveStyles` (ADR-0065), `RowHeight`,
103
+ `CONTEXT_TOKENS`, `CrudAffordances`, `RowCrudPredicates`.
104
+
105
+ **The copies were live misdescriptions, not just duplicates.** Three said so in
106
+ their own comments:
107
+
108
+ - `CONTEXT_TOKENS` carried a note that the duplication was "temporary until the
109
+ next coordinated release… because the installed `@objectstack/spec` predates
110
+ that export". The installed spec (17.0.0-rc.0) exports it, and the copy was
111
+ byte-identical — so it passed every value comparison and every behavioural
112
+ test for the whole interval in which its stated reason was false.
113
+ - `RowHeight` advertised itself as "the spec's `RowHeightSchema` vocabulary"
114
+ while being a hand-written union. It happened to be correct; nothing would
115
+ have caught the day it stopped being.
116
+ - `managedBy.ts` described itself as a "UI-side mirror of the framework's
117
+ `resolveCrudAffordances()`" and carried its own `DEFAULTS` table — a
118
+ line-for-line copy of the spec's `CRUD_AFFORDANCE_DEFAULTS`, plus a copy of
119
+ its override parser.
120
+
121
+ `resolveEffectiveCrudAffordances` now **delegates** the bucket/`userActions` half
122
+ to the spec's `resolveCrudAffordances()`, so the bucket table has exactly one
123
+ definition on the platform. What stays objectui's is the part the spec has no
124
+ notion of: intersecting that matrix with the server-resolved effective API
125
+ operation set (#3391), so the UI never offers a button the server would 405 —
126
+ and the name now says that instead of claiming to be the spec's function.
127
+
128
+ Deriving `RowCrudPredicates` also **tightens** it: the local copy typed
129
+ `visibleWhen`/`disabledWhen` as `unknown`, where the spec types them as
130
+ `Expression | ExpressionInput`. That was imprecision, not a deliberate dialect.
131
+
132
+ - b414983: fix(dashboard): a date globalFilter's preset-name default becomes a range, not an equality
133
+
134
+ Setup → System Overview rendered EVERY KPI tile as 0 while its period selector
135
+ read "All time" (objectstack#4475). Every request was `200 OK`, the widgets
136
+ rendered normally, and nothing in the UI signalled a failure — zeros read as
137
+ "nothing has happened yet" rather than as an error, which is why this survived
138
+ to an RC.
139
+
140
+ Both symptoms are one missing normalization. `resolveDashboardFilterDefs` lifts
141
+ the built-in `dateRange` declaration's preset NAME to `{ preset }`, but passed a
142
+ `globalFilters` entry's `defaultValue` through raw. `@objectstack/spec`'s
143
+ `GlobalFilterSchema.defaultValue` is `string | number | boolean`, so a bare
144
+ preset name is the ONLY spelling an author can write — and nothing ever mapped
145
+ it. System Overview declares
146
+ `{ field: 'created_at', type: 'date', defaultValue: 'last_7_days' }`, so:
147
+
148
+ - `buildFilterCondition` fell through to its "a bare string date means equality
149
+ on that day" branch and the widget sent
150
+ `runtimeFilter: { created_at: 'last_7_days' }`. The backend compiled
151
+ `SELECT COUNT(*) AS "user_count" FROM "sys_user" WHERE created_at = $1`
152
+ — verified against a live server, byte-for-byte the SQL in the issue. The
153
+ actual `sys_user` count is 4; that equality matches no row.
154
+ - `DateRangeFilter` derives its selected item from `value.preset` / `.from` /
155
+ `.to`, all `undefined` on a bare string, so the control fell through to its
156
+ ALL sentinel and displayed "All time" while sending that equality. The tiles
157
+ therefore looked deliberately unfiltered and merely empty.
158
+
159
+ `normalizeDateDefault` now applies the same lift the sibling `dateRange`
160
+ declaration already receives, for `date`/`dateRange` filters whose default names
161
+ a preset this module actually knows. This is not consumer-side leniency: it is
162
+ one normalization function completing the same conversion for the sibling
163
+ declaration, and the spec admits no other spelling for an author to fix at the
164
+ producer. A genuine ISO date string still means equality on that day (the
165
+ documented behaviour), and numbers, booleans and unrecognised strings are left
166
+ exactly as declared.
167
+
168
+ No backend change is needed: given a real range the dataset path already lowers
169
+ it correctly (`WHERE (created_at >= $1 AND created_at < $2)` → 4). The
170
+ framework's dashboard metadata needs none either — it is spec-compliant as
171
+ written, and editing it would only hide the defect.
172
+
173
+ Levelled `minor` rather than `patch` because the change is visible in rendered
174
+ dashboards rather than internal: any dashboard declaring a date-typed
175
+ `globalFilters` default now emits a different query shape, its numbers change
176
+ (from 0 to real values), and its filter control's displayed label changes with
177
+ them. Anything asserting on the previously-emitted condition will see it move.
178
+
179
+ Known residual, filed separately rather than widened into here: a `date` filter
180
+ whose value is neither a known preset nor a parseable ISO date still degrades
181
+ silently to an equality that matches nothing, producing the same
182
+ healthy-looking zero. Preset names are covered by this change; a misspelled
183
+ custom value is not.
184
+
185
+ - 256f8cc: fix(dashboard): an unrecognised date filter value is skipped and named, not compared
186
+
187
+ The residual the preset-name fix (objectui#3150 / objectstack#4475) left behind,
188
+ and the more deceptive half of it: a `date`/`dateRange` filter value that is
189
+ neither a known preset name nor a parseable date used to fall through to the
190
+ "a bare string date means equality on that day" branch. A misspelled default —
191
+ `defaultValue: 'last_7_dayz'` — therefore reached the widget query as
192
+ `runtimeFilter: { created_at: 'last_7_dayz' }`, which the backend faithfully
193
+ compiled to `WHERE created_at = $1`. `200 OK`, widget renders, count is 0 —
194
+ indistinguishable from "this range genuinely has no data". No 4xx, no console
195
+ warning, no UI signal. objectstack#4475 took a full RC cycle to catch for
196
+ exactly this reason: **0 looks like a legitimate answer**.
197
+
198
+ `buildFilterCondition` now holds a date value to three spellings, and only
199
+ three:
200
+
201
+ 1. a known preset name → range bounds (unchanged, objectui#3150);
202
+ 2. an ISO date (`2026-01-15`, `2026-01-15T08:30:00Z`) or a date-macro token
203
+ (`{today}`, `{7_days_ago}`) → equality on that day (the documented
204
+ behaviour, unchanged);
205
+ 3. **anything else → the filter is skipped and `console.warn` names the filter,
206
+ the offending value, and the accepted spellings.**
207
+
208
+ The `{ preset: '<unknown>' }` object form gets the same voice. It already
209
+ dropped the filter — silently — because the preset lookup missed and no
210
+ `from`/`to` remained; that drop is now announced. When explicit bounds ride
211
+ along with an unknown preset the bounds are still honoured, and the warning says
212
+ which of the two won.
213
+
214
+ Rule 3 is deliberately the same strictness `buildWidgetScopedFilter` already
215
+ applies to a _default binding on a field the object does not have_ — skip and
216
+ warn, with the same rationale spelled out there: never emit a query the backend
217
+ can only empty-match. Field _names_ had that guard; field _values_ did not.
218
+
219
+ The macro-token check asks `resolveDateMacros` itself whether it recognises the
220
+ string, rather than restating its token grammar in a second place. One
221
+ vocabulary, no dialect to drift — and a token that resolver does not know
222
+ (`{last_7_dayz}`) is precisely the typo this guard exists to catch.
223
+
224
+ Levelled `minor`, matching objectui#3150, because the emitted query shape
225
+ changes: a dashboard carrying a misspelled date value stops sending a
226
+ never-matching equality and instead sends no constraint for that filter (its
227
+ numbers go from 0 to unfiltered) while the console says why. Anything asserting
228
+ on the previously-emitted equality will see it disappear.
229
+
230
+ Note the direction of the relaxation is chosen, not incidental: skipping widens
231
+ the result set, so the number visibly changes and the warning explains it —
232
+ whereas the old behaviour narrowed it to zero, which is the one outcome an
233
+ author cannot tell from a correct answer. Author-time rejection (validating
234
+ `GlobalFilterSchema.defaultValue` at publish, in `@objectstack/spec`) is the
235
+ stricter complement and belongs on the platform side; it is filed separately.
236
+
237
+ - d3584c6: Bring the whole `@objectstack` family to `17.0.0-rc.1`, so the dependency graph resolves a
238
+ single copy of `@objectstack/spec`.
239
+
240
+ #3178 bumped **only** `@objectstack/spec` to `17.0.0-rc.1`. The rest of the family —
241
+ `client`, `core`, `formula`, `lint` (and `sdui-parser`, reached through `lint`) — stayed on
242
+ `17.0.0-rc.0`, and each of them depends on spec at an **exact** version rather than a
243
+ caret:
244
+
245
+ ```
246
+ @objectstack/client@17.0.0-rc.0 -> spec "17.0.0-rc.0"
247
+ @objectstack/core@17.0.0-rc.0 -> spec "17.0.0-rc.0"
248
+ @objectstack/formula@17.0.0-rc.0 -> spec "17.0.0-rc.0"
249
+ @objectstack/lint@17.0.0-rc.0 -> spec "17.0.0-rc.0"
250
+ ```
251
+
252
+ So `main` carried **two** spec copies: objectui's own code read `17.0.0-rc.1` while every
253
+ `@objectstack/*` package read `17.0.0-rc.0` from its own nested `node_modules`. That breaks
254
+ the single-contract invariant this repo's guards are built on, and it breaks them
255
+ _silently_ — the affected checks depend on identity, not on version strings:
256
+
257
+ - `spec-subschema-parity.test.ts` distinguishes a genuine re-export from a fork by
258
+ **reference identity** of the zod schema object. Two spec copies make every schema a
259
+ distinct object, so a real re-export starts reading as a fork (or a fork slips through,
260
+ depending on which copy each side resolved).
261
+ - `scripts/check-spec-symbol-derivation.mjs` and `spec-symbol-parity.test.ts` use
262
+ `createRequire` to resolve spec's `.d.ts` and run it through the TS checker. With two
263
+ copies installed, _which_ declaration file the checker sees is a function of resolution
264
+ order rather than of intent.
265
+
266
+ The declared ranges were already `^17.0.0-rc.0`, which technically admits rc.1 — the pin
267
+ lived in the lockfile. Raising the remaining ranges to `^17.0.0-rc.1` makes the floor
268
+ explicit and forbids a future install from silently sliding back onto a family member that
269
+ drags rc.0 along with it. The rc.1 family members pin spec at `17.0.0-rc.1` exactly, so the
270
+ graph now converges on one copy by construction, not by luck.
271
+
272
+ No product behaviour changes here. `check:spec-symbols` reconciliation was already
273
+ completed by #3178 and stays green under the unified graph; this changeset is `minor`
274
+ per the repo's fixed-group version policy.
275
+
276
+ - 444457c: feat!: follow the framework's `managedBy: 'system'` → `'system-data'` retirement (objectstack#3355)
277
+
278
+ **FROM → TO: `managedBy: 'system'` → `managedBy: 'system-data'`.** The framework
279
+ retired the residual `system` bucket in protocol 17; this is the UI half of that
280
+ change, landing with it so the closed `ManagedByBucket` union stays a mirror
281
+ rather than a fork.
282
+
283
+ ADR-0103 split the overloaded `system` bucket additively in v16 — the
284
+ engine-owned objects moved to the explicit `engine-owned`, the admin/user-writable
285
+ ones stayed on `system` — which left that value named after the half that had
286
+ already moved out. `system-data` names what it actually holds: the SCHEMA is the
287
+ platform's, the DATA is the admin's or the user's.
288
+
289
+ **The derivation this deletes is the point.** Because v16's `system` doubled as
290
+ both the engine-owned default and the writable set, three UI surfaces had to
291
+ RECOVER the distinction from `userActions` at render time:
292
+
293
+ - `isSystemWritable()` probed `userActions` for any opted-in write. It is now
294
+ `managedBy === 'system-data'` — the bucket answers directly.
295
+ - `ManagedByBadge` derived a synthetic `'system-writable'` variant key. The
296
+ variant map is now 1:1 with the bucket union, so a new bucket is a compile
297
+ error to miss instead of a silent fallthrough. The `systemWritable` /
298
+ `system` i18n keys are **unchanged**, so no locale bundle moves.
299
+ - `resolveManagedByEmptyState()` asked the resolved `create` affordance whether a
300
+ `system` list should read "entries appear automatically" or show the New
301
+ button. `system-data` now falls through to the generic empty state by
302
+ definition; `engine-owned` keeps the automatic-entries copy.
303
+
304
+ **Breaking (UI API):** `ManagedByBadge`'s `userActions` prop and the exported
305
+ `ManagedByUserActions` interface are **removed**. The bucket alone selects the
306
+ variant now, so the prop had become metadata nothing read — the exact defect the
307
+ framework change exists to remove; shipping it as an accepted-but-ignored prop
308
+ would have reproduced it one layer up. Drop the prop from call sites; no other
309
+ change is needed.
310
+
311
+ `MANAGED_BY_BUCKETS` and `ManagedByBucket` no longer contain `'system'`.
312
+
313
+ - 850033c: Stop offering the retired `action.shortcut` / `action.bulkEnabled` keys.
314
+
315
+ `@objectstack/spec` 17 retired both as `retiredKey()` tombstones: authoring
316
+ either one is a hard PARSE REJECTION, so a draft carrying it cannot be saved
317
+ at all. The designer still offered controls for both — a "Bulk — apply to
318
+ multiple selected rows" checkbox and a "Shortcut" text field — which meant the
319
+ Studio action inspector let an author build a draft the platform would then
320
+ refuse, with the rejection arriving later and nowhere near the checkbox.
321
+
322
+ - **Action inspector**: both controls removed. The keys stay hidden from the
323
+ fallback form (the server's live schema still advertises them, so dropping
324
+ them from the hidden list would put the inputs straight back) — now under a
325
+ `RETIRED_FIELDS` list that says why, so nobody "restores the missing
326
+ control". `bulkEnabled`'s replacement is the list view's `bulkActions` /
327
+ `bulkActionDefs`; `shortcut` has none.
328
+ - **Action preview**: the `shortcut` and `bulk` pills are gone — they could
329
+ only ever render for metadata the platform now refuses.
330
+ - **`ActionEngine.registerActions`**: no longer harvests the two retired keys
331
+ from authored metadata, which made two dead registration options look
332
+ load-bearing. Both are still accepted on the single-action
333
+ `registerAction(action, options)` overload, where a HOST passes them
334
+ explicitly.
335
+
336
+ - 009e25d: Report / chart / query symbols stop wearing `@objectstack/spec`'s names
337
+ (objectui#3155, objectstack#4115).
338
+
339
+ **Breaking for TypeScript imports** — six exported names change. Each was a
340
+ different concept than the spec export it collided with, so an author reading
341
+ the objectui declaration as "the spec's" was reading a false claim:
342
+
343
+ | was | now | why they were never the same thing |
344
+ | :------------------ | :----------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- |
345
+ | `ChartSeries` | `ChartDataSeries` | ours is a display name plus literal `data: number[]`; the spec's is a dataset-bound series descriptor (`type`/`stack`/`yAxis`/`variant`) with no data at all |
346
+ | `ChartSeriesSchema` | `ChartDataSeriesSchema` | zod twin of the above |
347
+ | `QueryAST` | `SqlQueryAST` | ours is a compiled SQL syntax tree (`select`/`from`/`join`/`group_by`); the spec's is the ObjectQL request descriptor (`object`/`fields`/`where`/`expand`) |
348
+ | `QuerySchema` | `DriverQueryConfig` | ours is the high-level config `QueryASTBuilder` compiles; the spec exports that name as a zod schema value |
349
+ | `DriverInterface` | `SqlDriverInterface` | ours is objectui's SQL-oriented client abstraction (`query(sql, params)`); the spec's is the platform runtime driver contract |
350
+ | `DatasourceSchema` | `DatasourceRegistration` | ours is the in-memory record `DatasourceManager` holds — its `driver` is a live instance; the spec's is the authored metadata document, where `driver` is a name |
351
+
352
+ Three more are now DERIVED from the spec instead of hand-restated, which fixes
353
+ live silent-stripping defects, since a `z.object()` drops unknown keys:
354
+
355
+ - **`DashboardWidgetSchema`** declared 10 of the spec's 22 keys, so
356
+ `objectui validate` deleted the other 12 without a word — `chartConfig`,
357
+ `colorVariant`, `filter`, `responsive`, `aria`,
358
+ `actionUrl`/`actionType`/`actionIcon`, `compareTo`, `suppressWarnings` and the
359
+ `requiresObject` / `requiresService` capability gates the dashboard renderer
360
+ honours at runtime. The TS interface had declared most of them all along, so a
361
+ widget could type-check and still lose half its configuration on validation.
362
+ Pinned divergences kept: `id` stays optional, `type` stays widened for the
363
+ objectui-only `list` / `custom` families, and the legacy `component` envelope
364
+ stays.
365
+ - **`GlobalFilterSchema`** took `scope` as a free-form string (any typo
366
+ validated); it now uses the spec's `widget | dashboard` vocabulary. The three
367
+ objectui widenings that back a real runtime normalizer are kept and pinned:
368
+ the bare-string `options` shorthand, the normalized `{ preset }` date default,
369
+ and an optional `optionsFrom.labelField`.
370
+ - **`AppContextSelectorSchema`** was a full restatement; spec keys and their
371
+ defaults now flow in by reference, with `label` widened for objectui's i18n
372
+ label envelope — which `AppContextSelectors` already renders.
373
+
374
+ `ListViewSchema`'s zod node now names the spec in its own initializer rather
375
+ than one hop away through a local const, so its long-standing derivation is
376
+ visible where it is declared.
377
+
378
+ Drift guard: `packages/types/src/__tests__/report-chart-query-spec-parity.test.ts`.
379
+
380
+ - 726b89c: `@object-ui/types` stops declaring sixteen symbols under names `@objectstack/spec` owns (objectui#3156, objectstack#4115).
381
+
382
+ Seven are now **derived** from the spec, nine are **renamed** to the local
383
+ dialect they always were. Both halves remove the same hazard: a local
384
+ declaration under a spec export's name reads as the spec's own definition to
385
+ the next reader, so a copy that is merely _correct today_ is a planted premise
386
+ tomorrow.
387
+
388
+ **Derived** — the spec now supplies the keys, by reference:
389
+
390
+ | symbol | derivation |
391
+ | :----------------------- | :-------------------------------------------------------------------------------- |
392
+ | `ActionParam` | `z.input<typeof ActionParamSchema>`, `type` widened to the local legacy spellings |
393
+ | `CreateExportJobRequest` | `Omit<CreateExportJobInput, 'object'>` (`object` is the method argument) |
394
+ | `CreateExportJobResult` | re-export from `@objectstack/spec/contracts` |
395
+ | `ImportRowResult` | re-export from `@objectstack/spec/api` |
396
+ | `NavigationArea` | spec keys, with `navigation` / `visible` pinned locally |
397
+ | `NavigationAreaSchema` | `specFieldsExcept(NavigationAreaSchema.shape, …)` |
398
+ | `Theme` | re-export of the spec's `ThemeInput` (the authoring shape) |
399
+ | `ExportJobFormat` | re-export of the spec's `ExportFormat` |
400
+
401
+ Four of these close real gaps rather than tidy names. `ActionParam` never
402
+ declared `reference` — the key `resolveActionParams()` actually reads for an
403
+ inline lookup target — nor `defaultFromRow`, which the metadata designer's own
404
+ inspector writes; it also narrowed `visible` to a bare string although the
405
+ resolver has always accepted the `{ dialect, source }` envelope too.
406
+ `CreateExportJobResult.createdAt` and `ImportRowResult.action` were optional
407
+ here and required by the server, leaving every consumer a branch that could
408
+ never run. And `NavigationArea`'s `id` now carries the spec's own length rule
409
+ instead of accepting any string.
410
+
411
+ **Renamed** — same word, different concept:
412
+
413
+ | was | now | why |
414
+ | :----------------- | :----------------------- | :----------------------------------------------------------------------------------------------------------------------------- |
415
+ | `FileMetadata` | `UploadedFileMetadata` | field-VALUE payload (`url`, `original_name`), not the storage file record |
416
+ | `GestureType` | `TouchGestureType` | direction-fused (`swipe-left`), not the spec's type+direction pair |
417
+ | `GestureConfig` | `TouchGestureConfig` | gesture→`action` binding, not per-gesture tuning |
418
+ | `OfflineConfig` | `PWAOfflineConfig` | service-worker route caching, not the offline data/sync model |
419
+ | `PageRegion` | `PageNodeRegion` | region of the renderer page NODE, holding `SchemaNode`s |
420
+ | `PageRegionSchema` | `PageNodeRegionSchema` | zod twin of the above |
421
+ | `ResponsiveConfig` | `MobileResponsiveConfig` | mobile box config, not the spec's SDUI grid contract |
422
+ | `WidgetManifest` | `RuntimeWidgetManifest` | SDUI component manifest, not the field-widget plugin manifest |
423
+ | `WidgetSource` | `RuntimeWidgetSource` | `module`/`inline`/`registry` loader union — and its `inline` carries a resolved component where the spec's carries source code |
424
+
425
+ **Migration**: the old names are gone, not deprecated — an alias would preserve
426
+ exactly the ambiguity being removed. Import the new name; nothing about the
427
+ shapes changed. `@object-ui/types` already re-exports the spec's own
428
+ `SpecResponsiveConfig`, and `@object-ui/react`'s `useOffline` config remains the
429
+ spec-shaped `OfflineConfig`, so both concepts stay reachable under
430
+ distinguishable names.
431
+
432
+ Each rename carries a bidirectional tripwire
433
+ (`packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts`): it fails if
434
+ the spec ever claims the new name, and also if the spec retires the old one —
435
+ at which point the natural name can be taken back rather than the workaround
436
+ outliving its reason.
437
+
438
+ ### Patch Changes
439
+
440
+ - d9668a7: Honor the server's declared percent scale, so a ratio of exactly 1 renders as 100.0% (#3136)
441
+
442
+ A dataset measure declared `format: '0.0%'` rendered every ratio below 1
443
+ correctly and got the single most consequential one wrong: a rate of exactly
444
+ `1` printed as **`1.0%`**. On an SLA / pass-rate dashboard that turns
445
+ "everything met the SLA" into "1% met the SLA", on both surfaces the issue
446
+ names — the KPI card and the dataset-bound table (they share `formatMeasure`).
447
+
448
+ The cause was never a bad multiplier; it was a missing fact. `formatMeasure`
449
+ scaled by magnitude — `percentDisplayValue` multiplies by 100 only strictly
450
+ inside `(-1, 1)` — because the column arrived with a `%` format string and
451
+ nothing saying what scale its numbers were on. That guess is undecidable at
452
+ exactly 1, which is both a full-compliance ratio ("100%") and one percentage
453
+ point ("1%"), and it resolved to the reading almost nobody means.
454
+
455
+ The server now answers the question instead (framework: `percentScaleOf` +
456
+ `AnalyticsResult.fields[].percentScale`, the sibling of the ADR-0053 currency
457
+ chain): a `derived: { op: 'ratio' }` measure is a `fraction` by definition, and
458
+ a measure over a `percent` field inherits that field's scale. `formatMeasure`
459
+ takes the declared scale as a fourth argument and, when present, scales by it —
460
+ `fraction` ×100, `whole` verbatim — instead of inspecting the value. Every
461
+ dataset-bound call site passes the column's `percentScale`: the dashboard
462
+ metric/table/pivot cells, the report renderer's cells, totals and KPI, and the
463
+ dataset preview.
464
+
465
+ `percentDisplayValue` is untouched and still the fallback for a column that
466
+ arrives without the annotation (an older server, or a non-dataset percent cell
467
+ in a list view), so nothing that renders correctly today changes.
468
+
469
+ - a8ad6c0: A required boolean must be savable in its UNCHECKED state — `false` and `0` are values.
470
+
471
+ Reported against an AI-built task tracker whose 任务 object has a required
472
+ `是否完成` boolean: the create form showed the switch OFF, answered "是否完成不能
473
+ 为空", and saved instantly once the switch was turned ON. The app could only ever
474
+ create ALREADY-DONE tasks — the one state the control shows by default was the
475
+ one value it refused to save (cloud#972).
476
+
477
+ Two defects stacked, and either alone is enough to break it:
478
+
479
+ **The `required` verdict read truthiness, not presence.** `@objectstack/spec`
480
+ FieldSchema.required (ADR-0113) is "an insert must provide a NON-NULL value",
481
+ and objectql's record validator implements exactly that. react-hook-form's
482
+ built-in rule instead fails whenever `isBoolean(value) && !value` — its
483
+ accept-the-terms checkbox heritage — silently redefining every required boolean
484
+ as "must be TRUE", including a select whose chosen option value is `false`. It
485
+ also disagreed the other way, letting a whitespace-only string through for the
486
+ server to reject with a 400. The form renderer no longer hands RHF its own
487
+ `required`: the check is now a `validate` entry keyed `required` (so the error
488
+ still surfaces as `type: 'required'`, which the conditional-required cleanup
489
+ keys on) backed by a new shared `isMissingForRequired` in `@object-ui/core`, a
490
+ deliberate mirror of objectql `record-validator.isMissing` — `undefined`,
491
+ `null`, blank-after-trim string, empty array. Deleting the inherited rule also
492
+ stops a `required` that rode in on `validation` from outliving a `requiredWhen`
493
+ that resolved to FALSE.
494
+
495
+ **A boolean field held `undefined` while displaying "off".** A two-state control
496
+ has no third state, but a field with no entry in `defaultValues` rendered an OFF
497
+ switch backed by nothing: the create payload omitted the column (it lands null,
498
+ which reads as unchecked but isn't) and the presence check above would still
499
+ refuse it. The form renderer now folds `false` into `defaultValues` for every
500
+ boolean-widget field the caller left unset — in `defaultValues` itself, not
501
+ per-Controller, because that object is also the dirty-check baseline and what
502
+ the defaults-reset window replays. Every surface gets it, including the
503
+ modal/drawer create dialogs that start from a bare `{}`. An authored default
504
+ (or a loaded record, `null` included) still wins.
505
+
506
+ `WizardForm`'s cross-step gate had its own copy of the empty-value predicate; it
507
+ now imports the shared one so it cannot drift from the per-field verdict. And
508
+ the field-demo renderer read `schema.defaultValue || schema.value`, throwing
509
+ away an authored default of `false` / `0` / `''` — same falsy-as-empty class,
510
+ now `??`.
511
+
512
+ Verified end to end on a local stack against the exact metadata shape
513
+ `apply_blueprint` materializes (`{ type: 'boolean', required: true }`, no
514
+ default): a 是否完成 = 否 task with 工时 = 0 now creates and persists as
515
+ `{ hours: 0, is_done: false }`, turning the switch on still stores `true`, and a
516
+ blank required text is still refused.
517
+
518
+ - Updated dependencies [4ae0ac4]
519
+ - Updated dependencies [696e3c1]
520
+ - Updated dependencies [4bf612c]
521
+ - Updated dependencies [cb82705]
522
+ - Updated dependencies [f572849]
523
+ - Updated dependencies [444457c]
524
+ - Updated dependencies [022e4c3]
525
+ - Updated dependencies [009e25d]
526
+ - Updated dependencies [726b89c]
527
+ - @object-ui/types@17.2.0
528
+
529
+ ## 17.1.0
530
+
531
+ ### Minor Changes
532
+
533
+ - 62311b6: feat(core): inventory `ActionDef`'s keys and warn on the ones nothing reads — objectstack#4075 step 1
534
+
535
+ `ActionDef` ends with `[key: string]: any`, so it accepts any key of any type.
536
+ Deleting `ActionDef.execute` produced **zero** compile errors even though the
537
+ field had just been removed (objectui#2990), and stale metadata still authoring
538
+ `execute: 'markDone'` type-checks today. The same deletion against
539
+ `@object-ui/types`' `ActionSchema` — which has no index signature — correctly
540
+ produced `TS2353` at the authoring site. One of the two readers can catch a
541
+ retired key; the other is structurally incapable, which is how a typo (`targt`)
542
+ and a tombstoned key both reach a runner that then silently does nothing.
543
+
544
+ This is the non-breaking first step of the staged narrowing: it makes the key set
545
+ **visible** and warns on anything outside it, without changing a single type.
546
+
547
+ New exports from `@object-ui/core`:
548
+
549
+ - `ACTION_DEF_KEYS`, `SPEC_ACTION_KEYS`, `NAVIGATION_ALIAS_KEYS`,
550
+ `RETIRED_ACTION_KEYS`, `KNOWN_ACTION_KEYS` — the inventory.
551
+ - `classifyActionKeys(action)` — splits an action's own keys into `unknown` and
552
+ `retired`.
553
+ - `warnOnUnknownActionKeys(action)` — dev-mode only, warn-once. Called by
554
+ `ActionRunner.execute`, so no consumer wiring is needed.
555
+
556
+ A retired key gets a louder, more specific warning than an unknown one: an
557
+ unknown key is probably a typo, a retired key is metadata that used to work.
558
+ `execute` is not simply gone from the spec — it is a live **tombstone**, still
559
+ present in `ActionSchema` so the parser can reject it by name with the rename
560
+ prescription.
561
+
562
+ Nothing is rejected and no types changed, so existing metadata behaves exactly as
563
+ before. Promoting the legitimate keys to explicit optional fields, then removing
564
+ the index signature so `tsc` catches both typos and retired keys, are steps 2 and
565
+ 3 of objectstack#4075.
566
+
567
+ - 9e7349e: **`target` is the only action handler slot — the `execute` alias is gone from the renderer (framework#3856).**
568
+
569
+ `ActionRunner.executeScript` read `action.target || action.execute`. That fallback
570
+ is unreachable against `@objectstack/spec` 17: `execute` is now a tombstoned key
571
+ (framework#3855) that the parser **rejects** with the rename prescription, so no
572
+ parsed action can carry it and the `||` could only ever yield `target`. Verified
573
+ against 17.0.0-rc.0 — an action declaring `execute` fails `ActionSchema.safeParse`,
574
+ and a `target` action's parsed output has no `execute` key at all.
575
+
576
+ Deleted rather than left as harmless residue: two handler slots is what let one
577
+ action run one script server-side and a different one client-side (framework#3713,
578
+ where this renderer preferred the alias while the spec transform preferred
579
+ `target`). A dead slot still reads as a live contract to the next maintainer.
580
+
581
+ `execute` is also **removed from the types**, which is the part that had never
582
+ landed. framework#3856 predicted a compile error here; there wasn't one, because
583
+ neither reader was typed against the spec's `z.infer`:
584
+
585
+ - `@object-ui/types` `ActionSchema` hand-declared `execute?: string`. Removed, so
586
+ `execute: '…'` now fails `tsc` at the authoring site (TS2353).
587
+ - `@object-ui/core` `ActionDef` hand-declared it too. Removed — but `ActionDef`
588
+ carries a `[key: string]: any` index signature, so stale hand-authored metadata
589
+ that never passed through the parser still compiles. For that path
590
+ `executeScript` now returns the rename prescription instead of a bare
591
+ "No script provided", matching the spec tombstone's rule that removing an
592
+ authorable key must be audible: silently binding no handler is the
593
+ "Mark Done does nothing" shape (framework#2169).
594
+
595
+ The four action renderers (`action:button`, `action:icon`, `action:menu`,
596
+ `action:group`) no longer forward `execute` into the runner, and Studio's
597
+ `ActionPreview` no longer falls back to it — previewing an alias-only draft as
598
+ "bound" contradicted the parse that rejects it on save.
599
+
600
+ Requires `@objectstack/spec` 17. Metadata still on the alias is rewritten by
601
+ `os migrate meta --from 16`.
602
+
603
+ - 6ae818e: feat(core): one column identity per column — `field` stamped at ingestion (#3104)
604
+
605
+ A column's field identity was resolved twice, with two different precedences
606
+ over the same `schema.columns` array, and the two halves disagreed:
607
+
608
+ - **request path** — `ListView`'s `$expand` and `$select` builders, and
609
+ `ObjectGrid.getSelectFields`, read `f?.field` and only `f?.field`.
610
+ - **render path** — the FLS gate, the hidden-field filter, `fieldOrder`, both
611
+ export branches and the hide-fields popover read
612
+ `f.name || f.fieldName || f.field` — name FIRST.
613
+
614
+ So `{ field: 'account', name: 'account_name' }` fetched `account` while the
615
+ renderer keyed off `account_name`, and `{ name: 'account' }` rendered a column
616
+ the request dropped entirely — neither `$select` nor `$expand` carried it. That
617
+ is the mechanism behind the "relation column shows a bare id / column is empty
618
+ / sort does nothing / export is missing a column" defect class.
619
+
620
+ Per AGENTS.md #0.1 the fix is not another `?? name` at the read sites. Legacy
621
+ acceptance moves to the one boundary that already folds this view's vocabulary,
622
+ `normalizeListViewSchema`, which now also canonicalizes each column's identity.
623
+
624
+ New in `@object-ui/core`:
625
+
626
+ - `columnIdentity(entry)` — the single reader. Resolves `field` → `name` →
627
+ `fieldName`, canonical-first, so it agrees with `buildExpandFields` instead
628
+ of racing it. Handles bare-string columns.
629
+ - `normalizeColumnIdentity(entry)` / `normalizeColumnIdentities(columns)` — the
630
+ fold. Stamps `field`; a legacy key that is **already present** is mirrored
631
+ onto the same identity so name-first readers resolve what the request asked
632
+ for; a legacy key that is **absent is never invented**, and an
633
+ already-canonical column is returned by reference.
634
+ - `hasConflictingColumnIdentity(entry)` — true when a column's keys disagree.
635
+ - `CANONICAL_COLUMN_IDENTITY_KEY`, `LEGACY_COLUMN_IDENTITY_KEYS`,
636
+ `TABLE_ADAPTER_COLUMN_KEY`.
637
+
638
+ The fold **mirrors** rather than deleting the legacy key, unlike the other
639
+ folds in `normalizeListViewSchema`. Deleting would work inside this repo (every
640
+ name-first read falls through to `field`), but `columns` entries cross the
641
+ package boundary into host renderers and dropping `name` from under them is a
642
+ breaking change with no inventory. Deletion is a later call, once the in-repo
643
+ consumers read `columnIdentity()`.
644
+
645
+ Behaviour is unchanged for any column carrying a single identity key — every
646
+ read site resolves the same string it did before. The entries whose resolution
647
+ moves are exactly the ones where two sites already disagreed.
648
+
649
+ `accessorKey` is deliberately untouched: it is TanStack Table's own column key
650
+ (`TableColumn.accessorKey`), not ObjectStack metadata identity, and folding
651
+ across that boundary would fossilize the merge.
652
+
653
+ - 746dd00: feat(sdui): curate the page:\*, element:\* and action:\* families into the public contract
654
+
655
+ The AI-authoring vocabulary and the Studio page designer disagreed by thirteen
656
+ blocks: `PUBLIC_BLOCKS` carried one `page:` tag and one `element:` tag while
657
+ the designer palette — and @objectstack/spec's page schema — offered the whole
658
+ families. A block a human can drag in Studio was invisible to a model writing
659
+ the same page, which is the objectui#3006 state at 10× the scale.
660
+
661
+ Fifteen tags join the contract (36 → 42 → **57**), every one shipping a
662
+ renderer with declared inputs (objectui#3065):
663
+
664
+ - `page:` — tabs, card, accordion, section, footer, sidebar
665
+ - `element:` — text, number, button, definition-list, repeater
666
+ - `action:` — button, group, menu, icon
667
+
668
+ Five stay out, each with its reason recorded and guarded: `action:bar`
669
+ (`record:quick_actions` covers the record action strip; the spec blesses the
670
+ other four), `element:image` (duplicates the curated `image` — one spelling
671
+ per concept), and `element:record_picker` / `element:text_input` /
672
+ `element:metadata_viewer` (mirroring the Studio palette's own exclusions, so
673
+ the two vocabularies stay out for the same reasons rather than by
674
+ coincidence).
675
+
676
+ The console's reverse-coverage guard now sweeps all four semantic namespaces
677
+ instead of `record:` alone — checking only the namespace you just fixed is
678
+ exactly how the last 22 doubled keys went unnoticed (objectui#3037). A new
679
+ prop-less allowlist (`element:divider`, `page:section`, `page:footer`,
680
+ `page:sidebar`) keeps "declares no inputs" a pinned decision in both
681
+ directions: those four must stay at zero, everything else curated must declare
682
+ a surface.
683
+
684
+ - 38ca8be: refactor(fields): `requiredWhen` is the only required-predicate slot — drop the retired `conditionalRequired` alias
685
+
686
+ `@objectstack/spec` 17 (objectstack#3855) **retired** `Field.conditionalRequired`,
687
+ the long-deprecated alias of `requiredWhen`. ObjectUI carried a back-compat read
688
+ for it in seven places; all of them are removed.
689
+
690
+ The removal is safe because the spec did not merely _stop emitting_ the key — it
691
+ made authoring it **fail loudly**. `retiredKey()` declares the key as
692
+ `z.never()`, so:
693
+
694
+ - `z.input` types it as `never` — writing it is a `tsc` error at the authoring site;
695
+ - the parse **rejects** it (verified against `17.0.0-rc.0`), at both `FieldSchema`
696
+ and `ObjectSchema`, with the prescription as the message:
697
+
698
+ > `conditionalRequired` was removed in @objectstack/spec 17 (#3855) — use
699
+ > `requiredWhen`. Rename the key; the value (a CEL predicate) is unchanged.
700
+ > Run `os migrate meta --from 16` to rewrite it automatically.
701
+
702
+ So spec-parsed metadata cannot carry the key — an object declaring it fails to
703
+ load rather than loading with the rule silently dropped. Keeping a renderer-side
704
+ `requiredWhen ?? conditionalRequired` would have re-created exactly the second
705
+ de-facto contract the tombstone exists to prevent: the key would have kept
706
+ working in the UI while being rejected everywhere else, hiding the producer's bug
707
+ (AGENTS.md #0.1). "Backend-agnostic" (#1) does not argue for keeping it either —
708
+ `conditionalRequired` is an ObjectStack-spec-ism, so the only producers that ever
709
+ emit it are ObjectStack producers on ≤16, and the spec ships them a converter.
710
+
711
+ Removed from:
712
+
713
+ | package | site |
714
+ | :----------------------- | :-------------------------------------------------------------------------------------------------------- |
715
+ | `@object-ui/types` | the `conditionalRequired?:` member on `FormField` |
716
+ | `@object-ui/core` | the `??` fallback + rules-param member in `resolveFieldRuleState` |
717
+ | `@object-ui/components` | three pass-throughs in the form renderer |
718
+ | `@object-ui/plugin-form` | `ObjectForm`, `ModalForm`, `sectionFields`, `deriveMasterDetail` (×2) |
719
+ | `@object-ui/app-shell` | the field inspector's legacy read/auto-migrate, and the key's entry in `clientValidation`'s CEL lint list |
720
+
721
+ **Studio authors lose nothing.** The object designer's draft validation parses
722
+ against the spec's own `ObjectSchema`, so a draft carrying the key now surfaces
723
+ the tombstone's rename prescription under the same `fields.<name>.conditionalRequired`
724
+ path the CEL lint used to report — a better message than the inspector's silent
725
+ auto-migration, and one the server agrees with. That behavior is pinned by a test.
726
+
727
+ **Migrating:** rename the key to `requiredWhen` (the CEL value is unchanged), or
728
+ run `os migrate meta --from 16`.
729
+
730
+ - 02aef0c: fix(sdui): a `kind:'html'` page can use lazily-registered blocks, and recovers when one registers late
731
+
732
+ objectui#2953 had a twin one tier over, unreported. The whitelist a
733
+ `kind:'html'` page's source compiles against was built from `getAllTypes()` +
734
+ `getConfig()` — both loaded-only — so any block registered via `registerLazy()`
735
+ was rejected as _"not an allowed component"_.
736
+
737
+ The blast radius is worse than the react tier's. There, a missing block cost one
738
+ identifier; here a compile diagnostic fails the **whole page**, so a single
739
+ `<object-kanban>` replaced the entire page with `HTML page failed to compile (2)`.
740
+ And it never recovered: `layoutElement` was memoised on `[schema, pageType]` with
741
+ no registry signal, so the cached error panel outlived the plugin actually
742
+ landing — permanently broken for the session.
743
+
744
+ `ComponentRegistry` gains three lazy-aware reads:
745
+
746
+ - `getKnownTypes()` — loaded registrations **plus** pending lazy stubs, deduped.
747
+ The set a whitelist or manifest should be built from. `getAllTypes()` keeps its
748
+ loaded-only meaning ("what can render right now") and now says so.
749
+ - `getMeta(type, namespace?)` — metadata from the loaded registration, else from
750
+ a pending stub. `getConfig()` stays loaded-only, since callers read
751
+ `.component` off it.
752
+ - `getVersion()` — monotonic counter of changes to the known set, bumped on
753
+ register / unregister / registerLazy. A cache key that a type _count_ cannot
754
+ substitute for: one registration plus one unregistration leaves the count
755
+ untouched while the set changed.
756
+
757
+ `getJsxManifest()` builds from those, and `PageRenderer` subscribes to the
758
+ registry so a page that could not compile retries when the registry grows.
759
+
760
+ A stub carries no `inputs` yet, so its props surface as `unknown-prop` warnings
761
+ rather than errors — the page compiles and renders, and the inner
762
+ `SchemaRenderer` triggers the loader and swaps in the real block. Authoring-time
763
+ prop validation is unaffected: `sdui.manifest.json` is generated with every
764
+ plugin eagerly loaded, and asserts as much.
765
+
766
+ - c4db402: refactor(views): ListView's `aria` and `sharing` are the spec sub-shapes (#2890 scope A step 5)
767
+
768
+ Last rename batch in the ListView vocabulary migration.
769
+
770
+ **`aria`** is now the spec's `AriaPropsSchema`: `label` → `ariaLabel`,
771
+ `describedBy` → `ariaDescribedBy`, folded at the ListView boundary like every
772
+ other legacy key. Two things fall out of adopting the spec shape:
773
+
774
+ - `role` becomes authorable. The list region hardcoded `role="region"`; it now
775
+ reads `aria.role` and falls back to `region`.
776
+ - `aria.live` stays as a documented local extension — it has no spec
777
+ counterpart, and dropping it would silently disable a shipped capability.
778
+ Promote it rather than growing that extension.
779
+
780
+ **`sharing`** is now the spec's `ViewSharingSchema` (`{ type, lockedBy }`),
781
+ imported by reference — the local four-key object is gone. The legacy pair folds
782
+ in: `visibility` collapses onto the two ownership kinds the spec models (only
783
+ `private` is `personal`; `team` / `organization` / `public` are all
784
+ `collaborative`), and a bare `enabled: true` maps to `personal`, which is the
785
+ badge the user already saw (the old title fell back to `'private'`).
786
+
787
+ _Visible change_: the share badge's tooltip shows the spec ownership type, so a
788
+ view authored with `visibility: 'team'` reads "Sharing: collaborative" instead
789
+ of "Sharing: team". The four-value audience has no spec home and nothing but
790
+ that tooltip consumed it; keeping a second audience enum alive would re-open the
791
+ fork this issue closes.
792
+
793
+ Also fixes the **spec bridge**, which was doing the opposite of its job: given a
794
+ spec-shaped `sharing`, `transformListView` _downgraded_ it — inventing a legacy
795
+ `visibility` audience and an `enabled` flag that the renderer then had to fold
796
+ back. Both sides speak `ViewSharing` now, so it passes through.
797
+
798
+ `conditionalFormatting` and `exportOptions` are deliberately **not** folded.
799
+ Both objectui shapes are supersets carrying capability the spec cannot express —
800
+ the `{ field, operator, value }` rule form, and `maxRecords` / `includeHeaders`
801
+ / `fileNamePrefix`. Folding them onto the narrower spec shapes would delete
802
+ working features; they want promotion upstream, not a rename.
803
+
804
+ - 5319bf1: feat(views): the list toolbar speaks one vocabulary — `userActions` (#2890 scope A step 3)
805
+
806
+ The seven bare `show*` toolbar flags fold into the spec's `userActions`, and the
807
+ renderer reads nothing else. `showDescription` folds into
808
+ `appearance.showDescription` at the same boundary.
809
+
810
+ | legacy | canonical |
811
+ | :-------------------------------------------------------- | :-------------------------------------------------------- |
812
+ | `showSearch` / `showSort` / `showFilters` / `showDensity` | `userActions.search` / `.sort` / `.filter` / `.rowHeight` |
813
+ | `showGroup` / `showHideFields` / `showColor` | `userActions.group` / `.hideFields` / `.rowColor` |
814
+ | `showDescription` | `appearance.showDescription` |
815
+
816
+ **The last three are new keys, and they close a capability hole rather than just
817
+ renaming one.** `@objectstack/spec`'s `UserActionsConfigSchema` documents itself
818
+ as "which interactive actions are available to users in the view toolbar — each
819
+ boolean toggles the corresponding toolbar element on/off", and already carries
820
+ `rowHeight` (objectui's `showDensity` under its spec name). Grouping, column
821
+ visibility and row coloring are the same kind of toggle: the spec models all
822
+ three as _configuration_ (`grouping`, `hiddenFields`, `rowColor`) but has no
823
+ "may the user change it" switch for any of them.
824
+
825
+ The consequence was visible in the product. With no `userActions` key to read,
826
+ the two list surfaces **hardcoded opposite policies**: `InterfaceListPage` (the
827
+ author-curated interface page) pinned all three OFF, `ObjectDataPage` pinned two
828
+ ON — and an interface-page author could not turn grouping back on for end users
829
+ at all. Both surfaces now express their policy as `userActions` defaults, which
830
+ an author can override.
831
+
832
+ Until the keys land in `@objectstack/spec`, `@object-ui/types` carries them as a
833
+ documented `.extend()` on `UserActionsConfigSchema` (the same shape
834
+ `ListColumnSchema` uses while waiting on objectstack#3761); it collapses into a
835
+ plain re-export once they do. Note the spec schema is not `.strict()`, so before
836
+ this an author writing `userActions: { group: false }` had it **silently
837
+ stripped** — valid on parse, no effect at render.
838
+
839
+ Defaults are unchanged and deliberately asymmetric, matching what these flags
840
+ have always done: `search` / `sort` / `filter` / `rowHeight` / `group` are on
841
+ unless turned off; `hideFields` / `rowColor` are off unless turned on. Making
842
+ them uniform would grow two buttons on every existing view, so it is left as its
843
+ own product decision rather than smuggled into a vocabulary migration.
844
+
845
+ Also drops a dead relay in app-shell's `ObjectView`, which forwarded
846
+ `showDescription` onto the node although `ListView` has only ever read
847
+ `appearance.showDescription`.
848
+
849
+ - f59f2c1: refactor(actions): `navigation` becomes a named alias of the spec's `url`, sharing one navigator (#2944)
850
+
851
+ The last open item of #2944: `ActionRunner` dispatched a seventh action type,
852
+ `navigation`, that `@objectstack/spec`'s `ActionType` does not contain. The issue
853
+ asked for a decision — promote it upstream or delete the case. Neither, as stated.
854
+
855
+ - **Promoting it is wrong.** The spec already has `url` for "go to a location",
856
+ with `openIn` for the new-tab/same-tab choice. A seventh type would put a
857
+ second spec name on one operation, which is the exact failure the #2901 audit
858
+ is named after: _a second definition of the vocabulary exists, and the renderer
859
+ is faithful to the wrong one_.
860
+ - **Deleting it is worse, because it is silent.** `{ type: 'navigation', to: … }`
861
+ is authored today (`element:button` CTAs). Without the case the action falls
862
+ through to `executeActionSchema`, which returns `{ success: true }` — a green
863
+ toast that navigates nowhere. That is #2960's trap.
864
+
865
+ So it stays, but stops being dialect. `ObjectUiLocalActionType` /
866
+ `OBJECTUI_LOCAL_ACTION_TYPES` in `@object-ui/types` declare it as objectui's own
867
+ alias of `url` — the same treatment #2985 gave `PageVisualizationAlias` — and the
868
+ runner routes both names through one navigator.
869
+
870
+ **The alias had already drifted, which is the point.** `executeNavigation` was
871
+ quietly the weaker of the two implementations: no `${param.X}` / `${ctx.X}`
872
+ interpolation, `openIn` ignored, and no `/api/…` full-page short-circuit (the
873
+ redirect-dance case `url` exists to handle). An author who wrote
874
+ `{ type: 'navigation', to: '/x?p=${param.p}' }` shipped the literal `${param.p}`,
875
+ while the identical `url` action resolved it. Both names now behave identically;
876
+ `url` in turn gains `replace` pass-through, the one modifier only the alias had.
877
+
878
+ Additive only. `replace` is omitted from the `NavigationHandler` options object
879
+ when unset, so hosts see the option shape they already saw.
880
+
881
+ The new guard is structural rather than another assertion. The runner's built-in
882
+ dispatch is a table typed `Record<RunnableActionType, …>` instead of a `switch`,
883
+ so an `ActionType` the spec **adds** stops compiling until an executor exists for
884
+ it — the Tier-2 "validates at save, renders nothing at run time" failure (#2942)
885
+ becomes a build error for actions. `spec-derived-unions.test.ts` additionally
886
+ asserts `navigation` is _absent_ from the spec enum, so the day it is adopted
887
+ upstream, the test fails and names the alias to retire.
888
+
889
+ - ce08d55: chore(deps): upgrade `@objectstack/*` to 17.0.0-rc.0, and let the spec take back what it now owns
890
+
891
+ `spec` / `client` / `formula` / `lint` move from `^16.x` to `^17.0.0-rc.0`. Two
892
+ groups of v17 changes reach this repo, and they pull in opposite directions —
893
+ the spec pruned surface objectui re-exported, and adopted surface objectui had
894
+ been carrying locally.
895
+
896
+ **The spec pruned dead Theme config (objectstack#3494), so the re-exports went
897
+ with it.** `ThemeSchema` dropped `spacing`, `breakpoints`, `logo`, `density`,
898
+ `wcagContrast`, `rtl`, `touchTarget` and `keyboardNavigation` — authorable but
899
+ never enforced, so authoring them was already a silent no-op. `@object-ui/types`
900
+ re-exported those sub-schemas _by reference_ (issue #2231), so they could not
901
+ survive the prune without becoming hand-written mirrors — exactly the second
902
+ de-facto contract AGENTS.md #0.1 forbids. Removed from the public surface:
903
+
904
+ - Types: `Spacing`, `Breakpoints`, `DensityMode`, `WcagContrastLevel`,
905
+ `ThemeLogo`, and the deprecated `SpacingScale` alias
906
+ - Schemas: `SpacingSchema`, `SpacingScaleSchema`, `BreakpointsSchema`,
907
+ `ThemeLogoSchema`, and the `SpacingSchemaType` / `BreakpointsSchemaType` helpers
908
+ - `Theme.spacing`, `Theme.breakpoints` and `Theme.logo`
909
+
910
+ `mergeThemes` no longer merges the three dropped keys. `generateThemeVars` is
911
+ unaffected — it never emitted them, which is why the liveness audit called them
912
+ dead. The one real consumer was `ThemeProvider`, which set the favicon from
913
+ `theme.logo.favicon`; that path is gone, because v17 strips the key at parse and
914
+ it could never arrive again. The live favicon is unaffected: it comes from
915
+ operator branding (`getFaviconUrl()`), applied in the console's `index.html`,
916
+ `main.tsx`, and on route change.
917
+
918
+ Nothing else read the pruned types. In particular the list-density feature is
919
+ untouched — `useDensityMode` and `rowHeightToDensityMode` use `@object-ui/core`'s
920
+ own local `DensityMode`, which never came from the spec.
921
+
922
+ **The spec adopted objectui's ListColumn extensions (objectui#2231), so the
923
+ extension collapsed.** `ListColumnSchema` used to `.extend()` the spec with two
924
+ fields, each carrying a note to promote it upstream rather than grow the
925
+ extension; v17 did exactly that. `summary` is now the spec's
926
+ `union([ColumnSummarySchema, ColumnSummaryConfigSchema])` — the same enum ∪
927
+ `{ type, field }` form `useColumnSummary` reads — and `prefix` is the spec's
928
+ `ColumnPrefixSchema`. `ListColumnSchema` is now a plain by-reference re-export.
929
+ One behavior change rides along: `prefix.type` defaults to `'text'` on parse
930
+ instead of staying `undefined`, so the cell renderer always gets a value.
931
+
932
+ **Node 22 is now the floor.** Every `@objectstack` package declares
933
+ `engines.node: ">=22.0.0"` (objectstack#3825; Node 20 reached EOL 2026-04-30).
934
+ This repo claimed `>=20` and ran CI on Node 20.x, so it promised — and validated
935
+ — a runtime its own core dependency does not support. `engines.node` is now
936
+ `>=22`, CI runs Node 22.x, and the CI/deployment docs say so.
937
+
938
+ The major stays 17: per AGENTS.md the major tracks `@objectstack`'s major, which
939
+ is also 17, and that convention deliberately outranks semver purity — so the
940
+ removals above ship as a minor rather than desyncing the two.
941
+
942
+ - 390c071: feat(record): declare inputs for the seven configurable record:\* blocks, and curate six
943
+
944
+ Seven `record:*` blocks shipped with renderers that read props but declared no
945
+ `inputs`. That combination is the worst of both: the renderer honours
946
+ `limit`, `severity`, `location` …, while every authoring surface — the designer
947
+ panel, the AI vocabulary, the generated manifest — reports the block takes no
948
+ configuration. objectui#3013 recorded them as deliberately uncurated for
949
+ exactly that reason.
950
+
951
+ The declarations mirror what each renderer actually reads:
952
+
953
+ | block | inputs |
954
+ | -------------------------------------- | ---------------------------------------------------------------------------- |
955
+ | `record:activity` | 11 — from `RecordActivityComponentProps` |
956
+ | `record:chatter` / `record:discussion` | 5 — from `RecordChatterComponentProps` |
957
+ | `record:alert` | 8 — severity, title, body, visible, icon, action, dismissible, dismissKey |
958
+ | `record:quick_actions` | 7 — actionNames, requiredPermissions, location, align, inline, variant, size |
959
+ | `record:history` | 3 — limit, emptyText, unknownUserText |
960
+ | `record:reference_rail` | 1 — hideEmpty |
961
+
962
+ `inputs` describe what an AUTHOR writes, which is a subset of what the renderer
963
+ reads. `entries`, `loading` and resolved `actions` are injected by the host
964
+ shell off RecordContext; declaring them would invite a model to hand-write the
965
+ data the page is supposed to fetch. `aria` is omitted for the reason it is
966
+ omitted on `record:details` — an accessibility escape hatch, not a layout
967
+ choice. `location` takes its enum from the spec's `ACTION_LOCATIONS` rather
968
+ than restating it, per objectui#3019.
969
+
970
+ Six of the seven are now in `PUBLIC_BLOCKS`: configurable and absent from the
971
+ contract is the state objectui#3006 was about. The contract goes 36 → 42 tags,
972
+ all resolving.
973
+
974
+ `record:chatter` stays out — it is the same renderer as `record:discussion`
975
+ under a Salesforce-familiar name, kept for schemas already in the wild. Two
976
+ spellings of one block is ambiguity an authoring model cannot resolve, so the
977
+ vocabulary carries the spec's name. A test compares the two input lists, so the
978
+ day they diverge the exclusion stops being justified and fails.
979
+
980
+ A companion assertion requires every curated `record:*` tag to declare inputs.
981
+ A curated tag with none reads as "takes no configuration" when the renderer in
982
+ fact reads props — the same gap objectui#3006 opened, pointed the other way.
983
+
984
+ - 912496d: feat(types,core): the `*Validation` rule types derive from spec 17, and the engine agrees with the server — objectstack#4115
985
+
986
+ The five spec-named rule variants in `data-protocol.ts` were hand-written
987
+ interfaces, each labelled `(ObjectStack Spec v2.0.1)` while the installed spec
988
+ was `17.0.0-rc.0`. Nothing bound them to the spec, so fifteen majors of drift
989
+ accumulated with `tsc` silent throughout and the comment still vouching for it.
990
+ They are now `z.input` derivations of `ScriptValidationSchema` /
991
+ `StateMachineValidationSchema` / `CrossFieldValidationSchema` /
992
+ `ConditionalValidationSchema` / `FormatValidationSchema`, and canonicity is
993
+ carried by that binding plus a parity gate rather than by a comment (#3017).
994
+
995
+ `z.input`, not `z.infer`, because objectui consumes **authored** metadata as it
996
+ arrives over `/meta` — before the spec applies its defaults and canonicalizes
997
+ expressions. That is the shape actually in the JSON.
998
+
999
+ **Breaking, in the shape of the rule types** (minor per this repo's version
1000
+ policy — see AGENTS.md §9):
1001
+
1002
+ | | was | is |
1003
+ | --------------------------------- | ------------------------------------------- | ------------------------------------------------------- |
1004
+ | `ConditionalValidation` | `condition` + `rules[]` | `when` + `then` / `otherwise` |
1005
+ | `FormatValidation` | `pattern` + `flags`, 8 named formats | `regex`, the 4 formats the server implements |
1006
+ | `Script`/`CrossField` `condition` | `string` | `string \| { dialect, source }` |
1007
+ | `StateMachineValidation` | — | gains `initialStates` (objectstack#3165) |
1008
+ | `BaseValidation` | no `priority`, `events` included `'delete'` | gains `priority`; `'delete'` retired (objectstack#3184) |
1009
+
1010
+ `UniquenessValidation` / `AsyncValidation` / `RangeValidation` are now
1011
+ `@deprecated`. They have no spec counterpart — the spec removed the first two
1012
+ deliberately (uniqueness → a unique index, since SELECT-then-INSERT is racy;
1013
+ async → the form layer) — and the spec's `ValidationRuleSchema` rejects all
1014
+ three, so no rule in those shapes can ride in `ObjectSchema.validations`.
1015
+
1016
+ **`ObjectValidationEngine` now agrees with `objectql`'s rule-validator.** It is a
1017
+ client PRE-CHECK of rules the server enforces, so every disagreement cost the
1018
+ user something real. Fixed:
1019
+
1020
+ - **Polarity was inverted.** The server violates a rule when the predicate is
1021
+ TRUE; the engine violated it when the predicate was FALSE. Every
1022
+ spec-authored `script` / `cross_field` rule produced the opposite verdict.
1023
+ - **Envelope conditions were a silent no-op.** `{ dialect, source }` reached
1024
+ `expression.trim()`, threw, was caught, and read as "passes".
1025
+ - **`conditional` was a silent no-op**, reading `rule.condition` / `rule.rules`
1026
+ where the spec says `when` / `then`; `otherwise` was never evaluated at all.
1027
+ - **`format` produced FALSE REJECTIONS** — it read `rule.pattern`, and
1028
+ `undefined.test(...)` threw into a catch that reported a violation, blocking
1029
+ writes the server accepts.
1030
+ - **An absent `active` disabled the rule** and an absent `events` threw; both
1031
+ arrive absent from `/meta` because the spec defaults them at parse time.
1032
+ - `priority` now orders execution; `initialStates` is enforced on insert;
1033
+ `format`/`state_machine` only fire when the write touches the field; a broken
1034
+ predicate or an uncompilable `regex` fails OPEN with a warning; and a rule type
1035
+ the engine cannot evaluate (the spec's `json_schema`) warns instead of
1036
+ reporting the record as valid.
1037
+
1038
+ The default `SimpleExpressionEvaluator` is not CEL and never was; it now binds
1039
+ both the spec's `record.x` scope and objectui's historical bare `x`, and
1040
+ documents that richer predicates need a CEL-backed evaluator. `validateRecord`'s
1041
+ `event` parameter no longer accepts `'delete'`.
1042
+
1043
+ Gates: `packages/types/src/__tests__/validation-rule-spec-parity.test.ts` (key
1044
+ sets, wire shapes, the pinned `then`/`otherwise` divergence with an inverted pin
1045
+ that fails when objectstack#4171 is fixed upstream) and the rewritten engine
1046
+ suite. objectstack#4115's ledger drops 120 → 115.
1047
+
1048
+ ### Patch Changes
1049
+
1050
+ - b41f401: **Authoring types are input types (framework#4074 steps 2–3): `ActionParam` takes the spec's declaration forms, `ListViewSchema` stops promising parse-output defaults, and `FormField.dependsOn` matches its runtime reader.**
1051
+
1052
+ Three public types said something different from what the platform accepts. All
1053
+ three divergences were found by making `packages/types`' tests compile (#3009)
1054
+ and then resolving the declared `p1-spec-alignment.test.ts` debt site-by-site
1055
+ instead of papering over it.
1056
+
1057
+ **`ActionParam` is now the authoring shape, aligned with the spec's input.**
1058
+ `name` / `label` / `type` become optional and `field` / `objectOverride` appear:
1059
+ the spec's primary way to declare a param — a bare field reference that inherits
1060
+ label/type/validation/options from an object field — was unrepresentable while
1061
+ all three were required. The _resolved_ shape the dialog consumes (after
1062
+ app-shell's `resolveActionParams()` inlines the reference) remains
1063
+ `@object-ui/core`'s `ActionParamDef`, with all three required. Authoring and
1064
+ resolved are different types on purpose. `label` and option labels take the
1065
+ spec's `I18nLabel` by import — which the new compile-time guard promptly
1066
+ revealed to be aliased to plain `string` in the current spec (the per-locale
1067
+ record is the separate `I18nObject`), so this is not a behavioural widening
1068
+ today; importing the alias means objectui tracks any future widening
1069
+ automatically.
1070
+
1071
+ **Breaking:** code destructuring `param.name` / `param.label` / `param.type` as
1072
+ guaranteed must now handle the field-backed form (or consume the resolved
1073
+ `ActionParamDef` instead, which is what dialog-side code should be doing).
1074
+
1075
+ **`ListViewInferred` is `z.input`, not `z.infer`.** The spec sub-schemas that
1076
+ flow into the list-view surface (`userActions`, `tabs` → `ViewTab`, `sharing`)
1077
+ carry `.default()`s, so the inferred output type made fields like
1078
+ `userActions.refresh` or a tab's `pinned`/`visible` _required_ — but nothing on
1079
+ the render path ever runs `.parse()`: `normalizeListViewSchema` deliberately
1080
+ applies no defaults ("an absent flag stays absent", its own suite). The output
1081
+ type therefore rejected valid authored metadata (`userActions: { sort: true }`)
1082
+ while promising renderers defaults that never arrive. Typing the surface as
1083
+ input matches both the author and the runtime object. Code that _trusted_ those
1084
+ phantom defaults now gets an optionality error — which is a latent bug surfacing,
1085
+ not a regression: the value really could be absent.
1086
+
1087
+ **`FormField.dependsOn` is `DependsOnInput`.** The runtime reader
1088
+ (`resolveCascadingOptions`) has always accepted a bare name, a list of names, or
1089
+ lookup-parameter entries `{ field, param }` — its parameter type says so. The
1090
+ public property said `string`, so array-authored metadata type-errored while
1091
+ working, and the form renderer read the key through `(f as any).dependsOn` to
1092
+ get past its own type. The shape now lives in `@object-ui/types` (single source
1093
+ of truth next to `FormField`), `@object-ui/core` imports and re-exports it, and
1094
+ the two `as any` reads in the components form renderer are typed.
1095
+
1096
+ **The `p1-spec-alignment.test.ts` exclusion is gone.** Its 14 errors resolved:
1097
+ the two "sharing in ObjectUI format" tests and the legacy-ARIA-spelling fixture
1098
+ are deleted/rewritten — those dialects are _normalizer input_, folded by
1099
+ `normalizeListViewSchema` and asserted branch-by-branch in core's
1100
+ `normalize-list-view.test.ts`, the seam where the fold actually runs; asserting
1101
+ them on the canonical type only ever "passed" because nothing compiled the file.
1102
+ One fixture claimed a shape no surface ever admitted (an ObjectQL triplet as a
1103
+ spec `ViewTab.filter`) and was corrected to the rule-object form. Every test
1104
+ file in `@object-ui/types` is now compiled, with no exclusions.
1105
+
1106
+ Discrimination-checked: reverting `ListViewInferred` to `z.infer`, `dependsOn`
1107
+ to `string`, or `ActionParam.name` to required each produces the expected
1108
+ compile error in the now-compiled test files (`TS2739` / `TS2322` / `TS2741`);
1109
+ restored, all projects are clean.
1110
+
1111
+ - 95b7214: fix(list,grid,detail,tree,core): every column resolver reads one key (#3104 PR2)
1112
+
1113
+ PR1 (#3119) put a canonicalizing fold at ListView's ingestion boundary. This
1114
+ converges the 22 read sites themselves onto `columnIdentity()` from
1115
+ `@object-ui/core`, so a surface that is NOT downstream of that fold resolves
1116
+ the same identity anyway.
1117
+
1118
+ That distinction is the user-visible part. A standalone `object-grid` node —
1119
+ authored directly on a page, with no `list-view` above it — never passed
1120
+ through `normalizeListViewSchema`. Its `getSelectFields` read `c.field` alone
1121
+ while the `ensureId` probe one line above read `f?.name || f?.field`, so a
1122
+ legacy `{ name: 'account' }` column reached `$select` as a literal `undefined`
1123
+ hole: the server never returned the field and every cell in that column came
1124
+ back empty. Same for `ObjectTree`, `RelatedList` and the `record:details` /
1125
+ `record:related_list` renderers.
1126
+
1127
+ Converged:
1128
+
1129
+ | Surface | Was | Now |
1130
+ | ---------------------------------------- | ---------------------------------------------- | ----------------------------------- |
1131
+ | `ListView` ×9 + its 2 request builders | `name \|\| fieldName \|\| field` vs `f?.field` | `columnIdentity()` |
1132
+ | `RelatedList` ×8 | `accessorKey \|\| field \|\| name` | `accessorKey \|\| columnIdentity()` |
1133
+ | `ObjectGrid` | name-first probe vs `c.field` projection | `columnIdentity()` |
1134
+ | `ObjectTree` | `name \|\| fieldName \|\| field \|\| key` | `columnIdentity() \|\| key` |
1135
+ | `buildExpandFields` | `field ?? name ?? fieldName` | `columnIdentity()` |
1136
+ | `record-details` / `record-related-list` | `field \|\| name (\|\| key)` | `columnIdentity() (\|\| key)` |
1137
+
1138
+ `accessorKey` keeps its precedence in `RelatedList` — it is TanStack Table's
1139
+ column key, not ObjectStack metadata identity, and only the `field || name`
1140
+ tail was converged. `key` stays a tail fallback in `ObjectTree` and
1141
+ `record-related-list` for the same reason: it is a generic entry key.
1142
+
1143
+ Two incidental fixes that TypeScript surfaced once the resolver stopped
1144
+ returning `any`: ListView's filter-field options and its hide-fields popover
1145
+ both built entries keyed `undefined` for a column with no resolvable identity.
1146
+ Those entries could never match a column; they are now dropped.
1147
+
1148
+ **Inventory re-triage.** PR1 recorded 24 family members. Two were mis-classified
1149
+ and are reclassified here rather than converged — reading what they actually
1150
+ feed shows they are not column reads at all:
1151
+
1152
+ - `ViewPreview.tsx` adapts a ViewItem **form** section to what `object-form`
1153
+ selects by (`field` → `name`) — the #3090 two-layer join.
1154
+ - `SchemaForm.tsx` renders an arbitrary metadata **array** into a popover
1155
+ summary and guesses at a display key; the entries are validations, actions,
1156
+ or whatever the JSON schema declares.
1157
+
1158
+ So the family was 22, and it is now **0**. The ratchet asserts that, asserts
1159
+ each converged surface actually routes through the shared reader (a surface
1160
+ that dropped identity resolution instead of converging it goes red), and pins
1161
+ `accessorKey`'s precedence in `RelatedList`.
1162
+
1163
+ - 7d9734d: feat(core): say which column identity key won, out loud (#3104 PR3)
1164
+
1165
+ Closes the battle opened in #3104. PR1 (#3119) put the canonicalizing fold at
1166
+ ingestion; PR2 (#3122) converged all 22 read sites onto `columnIdentity()`.
1167
+ This is the audible half.
1168
+
1169
+ A column carrying two identity keys that **disagree** — `{ field: 'account',
1170
+ name: 'account_name' }` — now logs a one-time dev-mode warning naming which key
1171
+ won and what to change:
1172
+
1173
+ ```
1174
+ [ObjectUI] Column carries two identities: `field: 'account'` and
1175
+ `name: 'account_name'`. `field` wins — it is the only key `ListColumnSchema`
1176
+ declares — and `name` has been rewritten to match, so the rendered column and
1177
+ the requested field agree. Fix the producer: drop `name` and author `field`
1178
+ only. (objectui#3104)
1179
+ ```
1180
+
1181
+ The fold making the two halves agree is what stops the bug, but silently
1182
+ rewriting `name` to match `field` also hides that the producer is emitting a
1183
+ contradiction. The renderer recovering is not the same as the metadata being
1184
+ right, so the recovery says so.
1185
+
1186
+ Deliberately narrow:
1187
+
1188
+ - **Only contradictions.** A legacy-only column (`{ name: 'stage' }`) is legacy,
1189
+ not conflicting — it is stamped without noise.
1190
+ - **Warn once per (identity, conflicting spelling).** Columns are re-normalized
1191
+ on every render; a warning that floods the console is a warning that gets
1192
+ muted. Keyed by the pair rather than the identity alone, so a column carrying
1193
+ two different stale spellings reports both — the author needs to fix every
1194
+ producer, not just the first one seen.
1195
+ - **Silent under `NODE_ENV=production`**, and the fold still runs there.
1196
+
1197
+ `resetColumnIdentityWarnings()` is exported for tests.
1198
+
1199
+ **No lint rule, and that is a measured decision.** #3104 asked for
1200
+ `no-restricted-syntax` on `.field ?? .name` to be evaluated on its
1201
+ false-positive rate first. With the family at zero, all 12 remaining scanner
1202
+ hits are legitimate — a syntactic rule cannot tell a two-layer join from a dual
1203
+ read, because the distinction is what the keys mean in that layer, not how the
1204
+ expression is spelled. Adopting it would mean 12 inline disables on correct
1205
+ code, which trains the next author to reach for the disable. The ratchet carries
1206
+ a `verdict` and a `why` per site instead, so a new hit gets triaged rather than
1207
+ silenced. The evaluation is written into the ratchet's header.
1208
+
1209
+ **Ledger item resolved with no change needed.** #3104 flagged `ListColumn` for
1210
+ disposition under objectstack#4115 (spec-named symbols must be imports, not
1211
+ declarations). `ListColumnSchema` is already a by-reference re-export of
1212
+ `@objectstack/spec/ui`, and `spec-subschema-parity.test.ts` already pins it by
1213
+ reference identity — the only check that distinguishes a re-export from a
1214
+ faithful fork. Already compliant; nothing to do.
1215
+
1216
+ - aebfa4f: chore(core): deprecate `ObjectValidationEngine` — rule enforcement stays single-implementation on the server (#3110)
1217
+
1218
+ `ObjectValidationEngine` / `defaultObjectValidationEngine` / `validateRecord` are
1219
+ now `@deprecated`. **Nothing is removed and no behaviour changes** — existing
1220
+ callers keep working exactly as they did after #3103.
1221
+
1222
+ **Why.** Object-level validation rules are _enforcement_, and enforcement is
1223
+ single-implementation on the server (`objectql`'s rule-validator). objectui
1224
+ already draws that line for the predicates it _does_ evaluate client-side:
1225
+ `evaluator/fieldRules.ts` handles the presentation predicates (`visibleWhen` /
1226
+ `readonlyWhen` / `requiredWhen`) by delegating to the canonical
1227
+ `ExpressionEngine`, "rather than re-implementing a parallel evaluator"
1228
+ (ADR-0036). This engine was that parallel evaluator, on the enforcement side.
1229
+
1230
+ #3103 converged its semantics onto the server rule-for-rule, with eight
1231
+ mutation-tested gates — and still left a known divergence: the server carries
1232
+ ADR-0113's legacy-violation exemption (reject only when the merged state violates
1233
+ _and_ this write makes it worse), which this engine does not implement. Editing
1234
+ an unrelated field on a legacy row would be blocked here and accepted there. One
1235
+ careful pass still left a gap, which is the argument: mirroring cross-repo
1236
+ behaviour is structurally unreliable, not unreliable-this-time.
1237
+
1238
+ **What to use instead.** Let the write fail and render the server's rejection —
1239
+ it is already structured (`field` / `code` / `message`, plus a label since
1240
+ objectstack#3957). For pre-submit feedback, the answer is a validate-only
1241
+ (dry-run) write on the server: identical UX, zero parity risk, and it covers the
1242
+ two rule kinds a client can never check — `unique` (needs the database) and
1243
+ `json_schema` (ajv lives server-side).
1244
+
1245
+ **The decision is a mechanism, not a comment.** What #3103 removed was a doc
1246
+ comment claiming spec canonicity that had been false for fifteen majors;
1247
+ shipping its successor as another comment would repeat the mistake one level up.
1248
+ `validation-engine-stays-unwired.test.ts` scans `packages/*/src` and fails if a
1249
+ production module starts referencing the engine, naming the file and the issue to
1250
+ reverse first. Barrels still re-export it — publishing a deprecated API is not
1251
+ wiring — and host applications are free to keep importing it.
1252
+
1253
+ The five spec-derived rule TYPES in `@object-ui/types` are unaffected: they are
1254
+ the anchor for objectstack#4115's ledger and are independent of whether objectui
1255
+ ships an engine.
1256
+
1257
+ - c4d7b20: fix(view,list,core): a view's filter no longer disappears, or arrives as a predicate on columns that don't exist
1258
+
1259
+ Sweeping the other `$filter` producers after #3078 turned up two live defects in
1260
+ `ObjectView`, which fetches its own data for calendar / kanban / gallery /
1261
+ timeline (grid delegates to `ObjectGrid`).
1262
+
1263
+ **1. An object filter was dropped, and only for non-grid views.**
1264
+ `table.defaultFilters` is declared `Record<string, any>`, and the merge tested
1265
+ `baseFilter.length > 0` — `undefined > 0` for an object. So the filter vanished
1266
+ and the view returned **every record**. `ObjectGrid` assigns the same value
1267
+ straight to `params.$filter`, so one view definition filtered correctly as a
1268
+ grid and returned everything as a calendar.
1269
+
1270
+ **2. Rule objects were spread into the `and`, not wrapped.**
1271
+ `['and', ...baseFilter, ...userFilter]` is only correct when the source is an
1272
+ array of AST nodes. `activeView.filter` is a spec `ViewFilterRule[]`, so
1273
+ spreading put bare rule objects where the AST expects nodes:
1274
+
1275
+ ```js
1276
+ isFilterAST([
1277
+ "and",
1278
+ { field: "stage", operator: "eq", value: "won" },
1279
+ ["owner", "=", "me"],
1280
+ ]);
1281
+ // false → 400 since objectstack#4121
1282
+ parseFilterAST(same);
1283
+ // {$and:[{field:'stage',operator:'eq',value:'won'}, {owner:'me'}]}
1284
+ ```
1285
+
1286
+ That second line is a predicate over three columns named `field`, `operator`
1287
+ and `value` — which don't exist.
1288
+
1289
+ > **Correction.** The first version of this note said the spread was "reachable
1290
+ > whenever a view with a filter meets a user filter value". That was wrong for
1291
+ > `ObjectView`: the branch required a non-empty user filter, and nothing ever
1292
+ > wrote the state it was built from, so it could never run. The shape is
1293
+ > genuinely broken — a live server answers it with a 400 — and the adapter-level
1294
+ > defence added alongside is still warranted for any producer that emits it, but
1295
+ > **this particular site was dead code, not a live defect.** Defect 1 above was
1296
+ > live: it sat on the always-taken path. The dead machinery behind the wrong
1297
+ > claim is removed in a follow-up.
1298
+
1299
+ New in `@object-ui/core`: `toFilterNode` normalizes one source (rule array / AST
1300
+ / MongoDB object) and `mergeFilterNodes` combines sources as siblings under one
1301
+ `and`. `ObjectView` and `ListView.buildEffectiveFilter` both use them, so the
1302
+ three filter shapes are reconciled in one place instead of by hand at each
1303
+ renderer.
1304
+
1305
+ `ObjectStackAdapter` also now translates a bare rule object sitting directly
1306
+ under a logical node — the chokepoint defence for any producer still emitting
1307
+ the spread shape. Only rule-_shaped_ objects are touched; a child with no
1308
+ `field` is a genuine MongoDB condition and passes through untouched.
1309
+
1310
+ **Correcting a comment shipped in #3078.** `buildEffectiveFilter` documented the
1311
+ dropped-object case as unreachable, "nothing in this repo produces one for a
1312
+ list view". That was wrong: `ObjectView` passes `mergedFilters` straight into
1313
+ that schema's `filter`, and its last fallback is `table.defaultFilters`. The
1314
+ case is now handled rather than explained away.
1315
+
1316
+ Verified with 19 tests across the four packages; reverting each source file
1317
+ fails the ones that cover it. Emitted filters are asserted against the spec's
1318
+ own `isFilterAST` / `parseFilterAST`, including an executable pin on what the
1319
+ old spread shape produced.
1320
+
1321
+ - 6f29aa5: fix(grid): a legacy string row action runs instead of green-toasting a no-op — objectui#2960
1322
+
1323
+ A list view declaring `rowActions: ['convert_lead']` rendered a menu item that
1324
+ performed **zero network requests** and reported success. Where the object also
1325
+ declared the same action with `locations: ['list_item']`, the row menu showed a
1326
+ working entry and a dead duplicate of it side by side.
1327
+
1328
+ **The name never became an action.** `ObjectGrid` dispatched the legacy form as
1329
+ `{ type: <action name>, params: { record } }` — the action _name_ landing in the
1330
+ runner's `type` slot, never resolved against the object's action defs. It
1331
+ matches no built-in type and (absent a handler registered under that exact name)
1332
+ no handler either, so it fell through to `ActionRunner.executeActionSchema`,
1333
+ which returned `{success: true, reload: true, close: true}` for a schema with
1334
+ nothing in it. `handlePostExecution` then fired the green "Action completed
1335
+ successfully" toast.
1336
+
1337
+ Two changes, either of which would have surfaced the bug:
1338
+
1339
+ **① `ObjectGrid` resolves legacy names against `objectDef.actions`.** A name
1340
+ that matches a declared action is promoted to that def and dispatched through
1341
+ the same path as a `list_item` action — so it actually runs, and it picks up the
1342
+ def's label, `visible`/`disabled` predicates, param dialog and capability gate,
1343
+ none of which the string form could carry. A name that matches an action already
1344
+ rendered as a def is dropped, which is what removes the dead twin. Names that
1345
+ resolve to nothing are still dispatched by name, since a consumer may have
1346
+ registered a runner handler under exactly that name.
1347
+
1348
+ **② `ActionRunner`'s empty-schema fallthrough fails loudly.** It no longer
1349
+ reports success for an action it never ran: a dispatch with no registered
1350
+ handler and no `api`/`endpoint`/`navigate`/`redirect`/`onClick` returns a
1351
+ failure naming the action. Schema-only shapes that _do_ declare something — a
1352
+ bare `redirect`, an explicit `reload`/`close` — run exactly as before.
1353
+
1354
+ - ad0183a: fix(data-objectstack,core): an object filter no longer depends on whether the query expands a lookup
1355
+
1356
+ #3072 single-sourced the ARRAY branch of the adapter's two `find()` routes. The
1357
+ object branch was left as it was: `convertQueryParams` converted a MongoDB-style
1358
+ filter to AST while `translateFilterToAST` returned it verbatim — so the same
1359
+ `$filter` went out in two formats, decided by whether the query happened to
1360
+ expand a lookup.
1361
+
1362
+ Measured across 21 operator shapes, **four diverged**. Most of the gap turned
1363
+ out to be harmless — `{$and: […]}` survives the plain route as a
1364
+ `['$and','=',[…]]` comparison that `parseFilterAST` reads back as a real `$and`,
1365
+ and `$exists` vs `$null` is a difference the server treats identically. Two were
1366
+ not harmless:
1367
+
1368
+ - **The unknown-operator guard only ran on one route.** `convertFiltersToAST`
1369
+ throws on an unrecognised operator, with a comment saying it does so "to avoid
1370
+ silent failure" — but the expanded route never called it, so a typo'd operator
1371
+ threw on a plain read and shipped silently whenever a lookup was expanded.
1372
+ - **`$regex` was silently rewritten to `contains`.** `$regex: 'a.c'` matches
1373
+ "abc"; `contains 'a.c'` matches only those three literal characters. That is a
1374
+ _different question_, not a weaker version of the same one, and neither result
1375
+ looks wrong on screen. The rewrite sat behind a `console.warn`, which is not
1376
+ an error channel in a deployed app — and the function's own unknown-operator
1377
+ message never listed `$regex` among the supported set. The spec has no
1378
+ `$regex` (`FILTER_OPERATORS`, `data/filter.zod.ts`), so there is nothing to
1379
+ translate it into: it is now refused, the same treatment the neighbouring
1380
+ unknown operator already got. Nothing in the repo depended on the conversion.
1381
+
1382
+ Both refusals now throw `FilterOperatorError`, carrying `code: 'INVALID_FILTER'`
1383
+ / `httpStatus: 400`. The pre-existing unknown-operator throw was a bare `Error`,
1384
+ which `classifyLoadError` classifies as a network fault — so a malformed filter
1385
+ told the user to check their connection (#3066), the one thing it definitely
1386
+ was not.
1387
+
1388
+ - aa1240a: fix(sdui): lazily-registered public blocks reach a `kind:'react'` page's scope, and ReactRunner keeps the errors it catches
1389
+
1390
+ Two defects in the trusted `kind:'react'` page tier.
1391
+
1392
+ **objectui#2953 — the contract skipped lazy blocks.** `getPublicConfigs()`
1393
+ resolved every curated `PUBLIC_BLOCKS` tag through `getConfig()`, which reads
1394
+ loaded registrations only, so a block registered with `registerLazy()` was
1395
+ absent from the contract until its plugin chunk happened to be imported. In
1396
+ `apps/console` that silently dropped `object-kanban`, `object-calendar`,
1397
+ `object-gantt`, `object-timeline`, `object-map` and `markdown` from every react
1398
+ page's scope — writing `<ObjectKanban/>` threw `ReferenceError` even though the
1399
+ tag is a first-class contract member, and whether it threw depended on load
1400
+ order. `getPublicConfigs()` now resolves pending lazy stubs too, returning them
1401
+ with `lazy: true` and no `component` (new `PublicComponentConfig` type); the
1402
+ injected wrapper renders through `SchemaRenderer`, which triggers the loader and
1403
+ shows its placeholder. `getConfig()` stays loaded-only by design.
1404
+
1405
+ **objectui#2954 — ReactRunner discarded its own error state.**
1406
+ `getDerivedStateFromProps` re-transpiled and re-evaluated the page source on
1407
+ every render and unconditionally set `error: null`. React runs it before the
1408
+ re-render that follows `getDerivedStateFromError`, so the boundary threw away
1409
+ the error it had just caught, rebuilt an identical throwing element, and the
1410
+ throw escaped past its own `fallback` to the renderer's generic panel; `onError`
1411
+ was gated on state that had already been cleared and never fired for a
1412
+ compile-time error at all; and each compile minted a fresh page function — a new
1413
+ element type — that remounted the subtree and wiped the page's `useState`. The
1414
+ transpile+eval is now memoised on `(code, scope)`, errors persist until the
1415
+ inputs actually change, and `onError` reports each error exactly once.
1416
+
1417
+ - d10f526: fix(sdui): the curated contract lists `record:line_items`, the tag that actually resolves
1418
+
1419
+ `PUBLIC_BLOCKS` carried `line_items` — the bare tag. `@object-ui/plugin-form`
1420
+ registers the block as `record:line_items` with `skipFallback: true`, which
1421
+ exists precisely so the bare name is _not_ claimed, so that key never existed
1422
+ and the curated entry could never resolve. Its four siblings in the list are all
1423
+ `record:`-prefixed, and plugin-form's own comment says "Register
1424
+ record:line_items"; the bare spelling was a slip.
1425
+
1426
+ The effect was a block that has shipped all along — a full renderer, a label,
1427
+ five declared `inputs` — being absent from the public contract, from the JSX
1428
+ type surface, from the generated manifest, and from every `kind:'react'` page's
1429
+ scope. It read as an unimplemented aspirational entry, which is how it was
1430
+ recorded when objectui#2979 added the contract-coverage guard.
1431
+
1432
+ With the tag corrected the contract has no gaps left: all 36 curated tags
1433
+ resolve in the console, `record:line_items` among them with its full `inputs`.
1434
+ The guard's known-unimplemented list is now empty and stays asserted, so the
1435
+ next entry that cannot resolve surfaces instead of being explained away.
1436
+
1437
+ - 2d5d594: fix(list,detail): sorting a lookup column no longer orders by an invisible key — #3096
1438
+
1439
+ A relational column (`lookup` / `master_detail` / `user` / `tree`) never holds
1440
+ the string its cell shows: it holds the `$expand`-ed record, or a raw foreign-key
1441
+ id whose label was resolved separately. Every sort path took that raw value as
1442
+ its key, so the column of names came back in an order with no relation to the
1443
+ names — sorting looked broken, with nothing saying the key was something else.
1444
+
1445
+ The two halves are fixed differently, because they can order by different things:
1446
+
1447
+ - **Client-side sorts** (grid column headers, any `data-table`, a non-windowed
1448
+ related list) now key off the label the cell renders, via the new
1449
+ `getSortValue` / `compareSortValues` in `@object-ui/core` — which resolves an
1450
+ expanded record through `getRecordDisplayName` (ADR-0079), so the sort key and
1451
+ the lookup cell agree on which field names a record. This replaces two broken
1452
+ comparators: `a[col] < b[col]` is always false between two objects (the
1453
+ comparator collapsed to a constant and permuted the rows), and
1454
+ `String(a[col])` is `"[object Object]"` (every row compared equal, so the sort
1455
+ silently did nothing).
1456
+ - **Server `$orderby` sorts** cannot be fixed here — the key is the stored id by
1457
+ construction, and `objectstack#4256` settled that no relation join is coming.
1458
+ So those entry points stop offering the illusion: the ListView toolbar sort
1459
+ picker withholds relational fields and explains why (pointing at a formula
1460
+ field as the supported way to sort by a related name), and a windowed related
1461
+ list renders no sort button for them.
1462
+
1463
+ A relational field the view's CURRENT sort already uses stays listed, labelled
1464
+ `(by ID)`, so view metadata authored or saved with such a sort round-trips
1465
+ instead of rendering a blank row and losing the sort on the next edit.
1466
+
1467
+ - 7f23cd0: fix(form): a numeric/boolean select option survives selection with its type intact — #3090
1468
+
1469
+ `SelectOptionSchema.value` has accepted `string | number | boolean` for as
1470
+ long as it has existed, but the Radix controls underneath speak strings:
1471
+ picking `{ value: 2 }` silently submitted `"2"` — a wrong-typed write into a
1472
+ number field that nothing on the client ever reported. (Display half-worked:
1473
+ a numeric default matched its numeric item; only SELECTION morphed the type.)
1474
+
1475
+ The renderers now stringify on the way into the control and map the selection
1476
+ back to the AUTHORED option value on the way out (`matchOptionValue`), across
1477
+ the in-form select, the standalone `type: 'select'` component, and the
1478
+ standalone `type: 'radio-group'` component. The TS types stop lying to match:
1479
+ `SelectOption.value` / `RadioOption.value` and the corresponding
1480
+ `value`/`defaultValue`/`onChange` channels widen to what the zod schemas
1481
+ always accepted — a call site treating `option.value` as `string` is now a
1482
+ compile error pointing at a real latent crash, not a false comfort.
1483
+
1484
+ The ripple the widening named, handled at each boundary: `@object-ui/core`'s
1485
+ `OptionLike.value` widens (the option engines compare by identity, so values
1486
+ flow opaquely; the option-lint's CEL-literal domain stringifies at its
1487
+ boundary), and the multi-value field widgets (checkboxes / multiselect /
1488
+ radio) stringify at theirs — multi-value fields store string arrays.
1489
+
1490
+ Round-trip pinned by real Radix interactions in jsdom: the in-form select
1491
+ submits `2` (number), the standalone select hands its handler `false`
1492
+ (boolean).
1493
+
1494
+ - Updated dependencies [9e7349e]
1495
+ - Updated dependencies [8864971]
1496
+ - Updated dependencies [b41f401]
1497
+ - Updated dependencies [19e9fa0]
1498
+ - Updated dependencies [38ca8be]
1499
+ - Updated dependencies [4952edf]
1500
+ - Updated dependencies [7f0252e]
1501
+ - Updated dependencies [7639a61]
1502
+ - Updated dependencies [94e63ef]
1503
+ - Updated dependencies [c4db402]
1504
+ - Updated dependencies [5319bf1]
1505
+ - Updated dependencies [49e5671]
1506
+ - Updated dependencies [b5b97e2]
1507
+ - Updated dependencies [f59f2c1]
1508
+ - Updated dependencies [4874117]
1509
+ - Updated dependencies [ce08d55]
1510
+ - Updated dependencies [2374a49]
1511
+ - Updated dependencies [ea7f477]
1512
+ - Updated dependencies [7f23cd0]
1513
+ - Updated dependencies [24e0e0a]
1514
+ - Updated dependencies [3a6cf24]
1515
+ - Updated dependencies [aa35561]
1516
+ - Updated dependencies [03bd53b]
1517
+ - Updated dependencies [3c1f321]
1518
+ - Updated dependencies [a045a32]
1519
+ - Updated dependencies [912496d]
1520
+ - Updated dependencies [9867281]
1521
+ - @object-ui/types@17.1.0
1522
+
1523
+ ## 17.0.0
1524
+
1525
+ ### Minor Changes
1526
+
1527
+ - f9bbddb: feat: gate detail/form edit & delete on the server's effective operation set (#3546)
1528
+
1529
+ PR-4 (#3391) wired the **list/toolbar** surface (ObjectView Import, ListView /
1530
+ ObjectGrid Export) to the server-resolved effective API operation set
1531
+ (`/me/permissions` `apiOperations`, intersected via
1532
+ `resolveCrudAffordances(obj, effectiveApiOperations?)`). The **detail / form**
1533
+ surfaces still gated edit/delete on the bucket + `userActions` alone. This
1534
+ extends the same intersection to them, so the record page and its forms never
1535
+ offer an operation the server would 405.
1536
+
1537
+ - **core** `isObjectInlineEditable(obj, effectiveApiOperations?)` gains the same
1538
+ optional second argument as `resolveCrudAffordances` — inline-edit is now
1539
+ additionally ANDed with the server allowing `update`.
1540
+ - **app-shell** `RecordDetailView` threads the object's effective operations into
1541
+ the synthesized Edit/Delete header actions and the record-body inline-edit
1542
+ gate (`canEdit`); `RelatedRecordActionsBridge` intersects each **child**
1543
+ object's Create/Edit/Delete handlers with that child's own effective set.
1544
+ - **plugin-detail** `record:details` ANDs its inline-edit affordance with the
1545
+ object's effective `update`.
1546
+ - **plugin-form** `ObjectForm`'s blanket managed-object field lock also engages
1547
+ when the server denies `update` (edit mode) / `create` (create mode).
1548
+
1549
+ Backward-compatible: a missing effective set (unrestricted object, older
1550
+ backend, or no `PermissionProvider`) leaves the resolved affordance untouched —
1551
+ the bucket/`userActions` decision wins, exactly as today. Layers on top of the
1552
+ existing per-object `check('edit')` / `check('delete')` permission gates
1553
+ (intersection, never union).
1554
+
1555
+ - 2735de6: feat: render the server's effective API operation set (#3391 PR-4)
1556
+
1557
+ The frontend now consumes the per-object **effective API operation set** the
1558
+ server resolves (from `/me/permissions` `apiOperations`, framework #3391) —
1559
+ never the raw `apiMethods` — so Import/Export/New/Edit/Delete buttons match what
1560
+ the server will actually admit, and a 405 import refusal shows a dedicated
1561
+ message instead of silently falling back.
1562
+
1563
+ - **core** `resolveCrudAffordances(obj, effectiveApiOperations?)` — new optional
1564
+ second argument intersects each affordance bit with its API operation
1565
+ (create/import→create/import, edit→update, delete→delete, exportCsv→export).
1566
+ Omitting it (old backend / no effective set) leaves affordances unchanged.
1567
+ - **permissions** — `/me/permissions` response carries per-object
1568
+ `apiOperations`; `PermissionContextValue.getObjectApiOperations(object)`
1569
+ exposes it (undefined when absent → callers keep current behavior); `check()`
1570
+ maps `import→allowCreate`, `export→allowRead`.
1571
+ - **app-shell** `ObjectView` intersects its toolbar affordances with the object's
1572
+ effective operations (Import); the platform-admin identity-import bypass is
1573
+ unaffected.
1574
+ - **plugin-list** `ListView` / **plugin-grid** `ObjectGrid` gate the Export
1575
+ button (and export handler) on effective `export`; `plugin-grid` gains the
1576
+ `@object-ui/permissions` workspace dependency.
1577
+ - **plugin-grid** `ImportWizard` — a 405 / `OBJECT_API_METHOD_NOT_ALLOWED`
1578
+ import refusal is detected by a new `isImportNotAllowed` predicate at every
1579
+ catch site (async, sync, dry-run) and STOPS with a dedicated
1580
+ `grid.import.notAllowed` message (10 locales + fallback dict) — it never falls
1581
+ back to the sync/legacy path (which 405s too), distinct from the 404
1582
+ route-absent fallback.
1583
+
1584
+ Backward-compatible: a missing effective set (unrestricted object, older
1585
+ backend, or no permission provider) preserves the current default-allow
1586
+ behavior everywhere.
1587
+
1588
+ - cd09a7b: refactor(views): ListView reads the spec-canonical `columns`, with legacy `fields` folded in one normalizer (#2890 scope A step 1)
1589
+
1590
+ `ListViewSchema` has been derived from `@objectstack/spec/ui` since #2231, but
1591
+ the renderer still spoke objectui's own vocabulary for the same concepts. First
1592
+ rename closed: **`fields` → `columns`**.
1593
+
1594
+ Legacy acceptance does not disappear — stored view metadata in user databases
1595
+ carries `fields` — but it now lives in exactly one place instead of being
1596
+ re-implemented per read-site:
1597
+
1598
+ - **New `normalizeListViewSchema` (`@object-ui/core`)** folds `fields` into
1599
+ `columns` (canonical wins when both are present) and drops the legacy key, so
1600
+ a read-site that was missed fails loudly instead of quietly taking the legacy
1601
+ path. It also absorbs the `viewType` renderability default ListView applied
1602
+ inline. Non-mutating, idempotent, and returns its input by reference when
1603
+ there is nothing to fold, so ListView's downstream memos keep a stable
1604
+ dependency identity.
1605
+ - **`ListView` normalizes once at the component boundary**, before anything
1606
+ reads the schema. This is what guarantees the fold runs: nothing on the render
1607
+ path parses view metadata through zod (the zod schemas serve the CLI
1608
+ validator, the VS Code extension and tests), so a `z.preprocess` on
1609
+ `ListViewSchema` — spec-side or local — would never execute.
1610
+ - **Producers emit `columns`**: `ObjectView`'s `renderListView` payload,
1611
+ `ObjectDataPage`, `InterfaceListPage` and the `list-view` registry defaults
1612
+ had been _downgrading_ already-canonical `columns` config back to `fields`.
1613
+
1614
+ Two latent inconsistencies go away with it: the filter builder's
1615
+ objectDef-not-loaded fallback now resolves `ListColumn.field` (it read only
1616
+ `name`/`fieldName`, so object-form columns produced unnamed filter entries), and
1617
+ the column list no longer depends on which of the two keys a host happened to
1618
+ emit.
1619
+
1620
+ `fields` stays declared on `ListViewSchema` and in the drift guard's sanctioned
1621
+ set — it is still valid input, and `@objectstack/spec`'s `react-blocks.ts`
1622
+ sanctions it as the React-tier `<ListView fields>` prop — but it is input-only.
1623
+
1624
+ - f1abf0e: fix(views): ListView reads the spec-canonical `filter`, so a view's base filter reaches every visualization (#2890 scope A step 4)
1625
+
1626
+ Third rename in the ListView vocabulary migration: **`filters` → `filter`**. Unlike
1627
+ the first two this closes a live bug, because the fork was asymmetric.
1628
+
1629
+ `ListView` was the **only** surface in the repo reading `filters`. Every child
1630
+ view — `ObjectGrid`, `ObjectGallery`, `ObjectKanban`, `ObjectCalendar`,
1631
+ `ObjectGantt`, `ObjectMap`, `ObjectTree`, `ObjectChart` — reads `filter`, and
1632
+ `ListView` handed them `filters`. Wherever a child fetches its own rows instead
1633
+ of receiving `ListView`'s, the view's base filter was silently dropped:
1634
+
1635
+ - **a `chart` list view aggregated the whole object.** The chart branch built an
1636
+ `object-chart` node with `filters:`; `ObjectChart` reads `schema.filter` and
1637
+ never read `filters`, so a chart view with a base filter charted unfiltered
1638
+ totals.
1639
+ - the same applied to any of the other view components rendered standalone from
1640
+ a list-view-shaped config.
1641
+
1642
+ Conversely, a **spec-authored** list view — one carrying `filter`, which is what
1643
+ the spec says and what `runtime-metadata-persistence` and "Save as view" already
1644
+ persist — rendered **unfiltered** in `ListView`, because nothing read that key.
1645
+
1646
+ The fold is a key rename only. Both keys carry an ObjectQL FilterNode array
1647
+ everywhere in objectui; every consumer passes the value straight to `$filter`.
1648
+ (The spec types `filter` as `ViewFilterRule[]` — `{field, operator, value}`
1649
+ objects — so objectui's field is typed from the spec but used as something else.
1650
+ That mismatch is real and left alone here: converting formats inside a
1651
+ vocabulary fold would change what reaches the data source.)
1652
+
1653
+ Also collapses a duplicated computation in `app-shell`'s `ObjectView`, which
1654
+ computed the same effective filter **twice** — once as `filter` for the child
1655
+ views, once as `filters` for `ListView` — with the two copies subtly different
1656
+ (only one fell back to `listSchema.filter`; only the other ran token
1657
+ substitution over the URL filters). There is now one computation, keeping both
1658
+ behaviors.
1659
+
1660
+ `filters` stays declared on `ListViewSchema` and in the drift guard's sanctioned
1661
+ set — stored views carry it and it is still valid input — but it is input-only.
1662
+
1663
+ - f05b84e: refactor(views): ListView resolves density from the spec-canonical `rowHeight` (#2890 scope A step 2)
1664
+
1665
+ Second rename in the ListView vocabulary migration: **`densityMode` → `rowHeight`**,
1666
+ folded in the same `normalizeListViewSchema` that step 1 introduced.
1667
+
1668
+ Unlike `fields`/`columns` this is not a pure alias — the two vocabularies are
1669
+ different sizes. The spec has five row heights (`compact`/`short`/`medium`/
1670
+ `tall`/`extra_tall`); ListView's toolbar offers three densities
1671
+ (`compact`/`comfortable`/`spacious`). Both directions now live in one place as
1672
+ `DENSITY_MODE_TO_ROW_HEIGHT` / `ROW_HEIGHT_TO_DENSITY_MODE`, chosen so a fold
1673
+ followed by a read is a round trip (`spacious` → `tall` → `spacious`), with the
1674
+ narrowing collapse (`short` → `compact`, `extra_tall` → `spacious`) stated once
1675
+ instead of being re-derived per call site.
1676
+
1677
+ Two behavior fixes fall out of it:
1678
+
1679
+ - **Precedence is no longer inverted.** `ListView` read `densityMode` _first_, so
1680
+ a view carrying both keys rendered the legacy value — backwards from every
1681
+ other legacy/canonical pair in the schema. The canonical key now wins.
1682
+ - **The toolbar stops re-seeding the legacy key.** `ObjectView`'s
1683
+ `onDensityChange` persisted `densityMode` into stored view metadata on every
1684
+ density toggle, so the legacy vocabulary kept regrowing underneath the
1685
+ migration. It persists `rowHeight` now.
1686
+
1687
+ `densityMode` stays declared on `ListViewSchema` and in the drift guard's
1688
+ sanctioned set — stored views carry it and it is still valid input — but it is
1689
+ input-only.
1690
+
1691
+ ### Patch Changes
1692
+
1693
+ - 7b35e4b: fix(dashboard,charts): resolve `{current_user_id}` in widget filters (framework #3574)
1694
+
1695
+ A dashboard widget filtered on `{current_user_id}` rendered `0`. The token
1696
+ reached SQL as a literal, matched no row, and nothing was logged on the client
1697
+ or the server — a silent zero that reads as "you have no work" rather than
1698
+ "this filter did not resolve". The same token in a list-view filter resolved
1699
+ correctly, so a user-scoped list and a user-scoped widget over the same data
1700
+ disagreed.
1701
+
1702
+ There was no shared resolver. Three ad-hoc implementations had grown up
1703
+ independently — `ObjectView` for list views, `ObjectDataPage` for URL filter
1704
+ triples, `NavigationRenderer` for hrefs — and each understood only the filter
1705
+ shape its own surface used. `ObjectView`'s opened with
1706
+ `if (!Array.isArray(filter)) return filter`, so it could not have been reused
1707
+ by dashboard widgets even in principle: widget filters are MongoDB-style
1708
+ objects. Widgets therefore got no resolution at all — `DatasetWidget` called
1709
+ `resolveDateMacros` and nothing else, which is why `{today}` worked in a widget
1710
+ and `{current_user_id}` silently did not.
1711
+
1712
+ - **`@object-ui/core`** — new `utils/filter-tokens.ts` with
1713
+ `resolveContextTokens` and `resolveFilterPlaceholders`. The latter expands
1714
+ _every_ placeholder vocabulary in one call and is what surfaces should use;
1715
+ resolving only some of them is the whole defect. The walk handles arrays and
1716
+ plain objects uniformly, so one resolver covers both platform filter shapes.
1717
+ - **`@object-ui/react`** — new `FilterScopeProvider` / `useFilterScope`. The
1718
+ renderer packages deliberately do not depend on `@object-ui/auth`, so the
1719
+ shell supplies the session values. This is a separate context from
1720
+ `PredicateScopeContext`, which is the expression evaluation scope and carries
1721
+ no organization.
1722
+ - **`@object-ui/plugin-dashboard` / `@object-ui/plugin-charts`** — all six
1723
+ widgets that previously resolved date macros only now resolve both
1724
+ vocabularies: `DatasetWidget`, `ObjectMetricWidget`, `ObjectDataTable`,
1725
+ `ObjectPivotTable`, and `ObjectChart` (dataset-bound and inline paths). The
1726
+ chart's `compareTo` comparison filter gets the session pass too — otherwise
1727
+ the overlay series silently ignored the owner clause the primary series
1728
+ honoured.
1729
+ - **`@object-ui/app-shell`** — `ObjectView`'s local `substituteFilterTokens`
1730
+ and `ObjectDataPage`'s inline `=== '{current_user_id}'` ternary now delegate
1731
+ to the shared resolver, so both also gain `{current_org_id}` and date macros.
1732
+ Two of the three ad-hoc implementations are gone rather than joined by a
1733
+ fourth.
1734
+
1735
+ An unresolvable token is left intact rather than dropped: leaving it yields an
1736
+ empty result, whereas dropping the clause would _widen_ the result set and show
1737
+ a signed-out viewer everyone's data. It is no longer silent — the resolver
1738
+ warns, naming the token, and suggests the intended spelling for known
1739
+ near-misses (`{current_user}`, `{user_id}`, `{organization_id}`). Authoring-time
1740
+ enforcement lands separately as `filter-token-unknown` in `@objectstack/lint`.
1741
+
1742
+ - e16ed2d: fix(dashboard,charts): send widget `dateGranularity`/`sortBy`/`limit` to the query, and give funnels a real stage order (framework#3588)
1743
+
1744
+ `DatasetWidget` never read `widget.options`. Four keys an author writes there
1745
+ change the query the server compiles, so a widget declaring
1746
+ `options: { dateGranularity: 'month' }` grouped by the raw timestamp and drew
1747
+ one bar per record, and `sortBy`/`sortOrder` produced no ordering at all.
1748
+
1749
+ - `DatasetWidget` lowers `options.dateGranularity`, `options.sortBy` +
1750
+ `options.sortOrder`, and `options.limit` into the `DatasetSelection` it posts.
1751
+ A `sortBy` naming something the widget does not project is dropped rather than
1752
+ sent, so a stale sort key left by an edit degrades to "unordered" instead of
1753
+ failing the widget against the server's stricter validation. These keys also
1754
+ join the refetch signature, so editing one in the designer refetches instead
1755
+ of re-rendering the previous grid.
1756
+ - Funnel stages follow a **declared order**. `AdvancedChartImpl` sorted funnel
1757
+ segments by value descending, unconditionally — which overrode any server
1758
+ ordering and rendered a sales pipeline as a tidy narrowing shape whatever the
1759
+ stages' real sequence, hiding the very anomaly (a bulge at Proposal) the chart
1760
+ exists to show. It now honours a `categoryOrder`, which `DatasetWidget`
1761
+ derives from the dimension field's picklist option order — the pipeline order
1762
+ an author already declared on the object — or from an explicit
1763
+ `options.stageOrder`. With no declared order the value-descending default is
1764
+ unchanged, and a category missing from the order is kept (after the declared
1765
+ ones), never dropped.
1766
+ - New `@object-ui/core` helpers `buildCategoryOrder` / `buildCategoryRank`,
1767
+ keyed by both the stored value and the display label like the existing
1768
+ `buildOptionColorMap`, so ordering works whether or not the server resolved
1769
+ the dimension's labels.
1770
+
1771
+ Requires the framework-side fix in objectstack#3588 for the selection keys to
1772
+ take effect server-side.
1773
+
1774
+ - 53642d4: fix(core,fields): a string `$orderby` is a clause, not a character array — and localize the sharing-rule widgets (objectstack#3821)
1775
+
1776
+ **The recipient picker listed nothing, ever.** `QueryParams['$orderby']` was
1777
+ typed as `Record | string[] | SortObject[]`, so `queryParamsToRecord` sent any
1778
+ non-array value through `Object.entries`. Handed the clause string `'name asc'`
1779
+ — which callers do build by hand — it walked the string index by index and
1780
+ emitted `$orderby=0 n,1 a,2 m,3 e,4 ,5 a,6 s,7 c`. The server sorted by columns
1781
+ that don't exist and every row was filtered out, so
1782
+ `sys_sharing_rule.recipient_id` rendered "No matches" for every recipient type
1783
+ and no sharing rule could be created from the Console. `ObjectGrid` builds the
1784
+ same shape from a schema-level `sort` in three places, so grids with a string
1785
+ sort silently showed an empty table.
1786
+
1787
+ A string `$orderby` is now passed through verbatim (the server's OData
1788
+ normalizer has always parsed `'name asc'`), and the type admits `string`.
1789
+ `RecipientPickerField` additionally switched to the structured
1790
+ `{ name: 'asc' }` form so it can't regress this way against any data source.
1791
+
1792
+ **The three sharing-rule authoring widgets never had translations.**
1793
+ `ObjectRefField`, `RecipientPickerField` and `FilterConditionField` hardcoded
1794
+ their English copy — a Chinese Console showed "Select an object", "Select a
1795
+ user", "Search…", "No matches", "Edit as JSON". They now go through
1796
+ `useFieldTranslation` like every other widget, with keys added under `fields.*`
1797
+ in all ten locales.
1798
+
1799
+ The recipient placeholder was the interesting one: it read
1800
+ `` `Select a ${recipientType.replace(/_/g,' ')}` ``, interpolating the enum
1801
+ value into an English sentence — a shape no locale can translate. It is now a
1802
+ per-type key (`fields.recipient.selectUser`, `…selectBusinessUnit`, …), so
1803
+ "选择业务单元" and "Select a business unit" no longer have to share a structure.
1804
+
1805
+ **Editing a rule silently dropped its recipient.** The picker resets the stored
1806
+ id when `recipient_type` changes, because an id valid for a user is meaningless
1807
+ for a team. It treated the edit form's `'' → 'user'` hydration as such a change:
1808
+ opening any saved rule blanked the recipient, and saving persisted the blank.
1809
+ Only a non-empty predecessor now counts as a type switch.
1810
+
1811
+ **Building a filter submitted the surrounding form.** None of `FilterBuilder`'s
1812
+ controls declared `type="button"`, and a bare `<button>` inside a `<form>`
1813
+ defaults to `type="submit"`. Adding, removing or clearing a condition therefore
1814
+ submitted the sharing-rule dialog — firing validation mid-edit, and on an
1815
+ already-valid form saving the record before the admin was done.
1816
+
1817
+ **A rejected write showed the user raw server diagnostics.** The form rendered
1818
+ `error.message` verbatim, so a sharing / RLS denial reached the dialog and the
1819
+ toast as `FORBIDDEN: insufficient privileges to update showcase_private_note
1820
+ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and the
1821
+ record id to whoever hit it. Permission failures now render localized copy
1822
+ (`form.noPermissionToSave`, added in all ten locales), with the server text kept
1823
+ on the console for debugging; other failures still show the server's message,
1824
+ which is the useful part, and fall back to `form.submitFailed` when there is
1825
+ none — replacing the previously hardcoded English "An error occurred during
1826
+ submission".
1827
+
1828
+ **The detail header offered "Edit" on records the user may only read.** Object
1829
+ permissions can't express "this one record is read-only" — a read-only sharing
1830
+ grant sits inside an object the user may otherwise edit — so the header showed
1831
+ the primary Edit CTA, opened the form, and let the user retype a field before
1832
+ the server rejected the save. `DetailView` now gates Edit / Delete on the
1833
+ object-level check AND on the explain engine's record-grained verdict
1834
+ (`POST /api/v1/security/explain` with a `recordId`, ADR-0090 D6 / ADR-0095 C2 —
1835
+ the same pipeline the enforcement middleware runs, so button and server cannot
1836
+ disagree). Explaining oneself needs no special permission. The probe is one
1837
+ cached request per record, skipped entirely when the object-level check already
1838
+ says no, and **fails open** on every uncertainty — an unanswered hint must never
1839
+ be the reason a permitted user cannot act; the server stays the authority
1840
+ (ADR-0057 D10).
1841
+
1842
+ **A long option rendered straight past the combobox border.** `Combobox`'s
1843
+ trigger pinned itself to the component's `w-[200px]` default while the fields
1844
+ around it ran the full form column, and the selected label was a bare text child
1845
+ of a flex button — flex items need `truncate` AND `min-w-0` to clip, and it had
1846
+ neither. So "成员 (showcase_project_membership)" in the object picker overflowed
1847
+ the control and collided with the field beside it. The label now truncates, the
1848
+ trigger can shrink, the dropdown matches the trigger's width instead of a
1849
+ hardcoded 200px (a widened combobox used to clip its own options), and the two
1850
+ sharing-rule pickers ask for `w-full` so they line up with every other input.
1851
+
1852
+ Hardens `evaluatePermission` while there: a role config carrying only
1853
+ `fieldPermissions` (no `actions`) made `check()` throw a TypeError that
1854
+ propagated out of the render. A permission check must not be able to crash a
1855
+ view.
1856
+
1857
+ Browser-verified against the framework showcase Console in Chinese: object /
1858
+ criteria / recipient copy is fully localized, the recipient dropdown lists real
1859
+ users, business units and positions, a saved rule reopens with its recipient and
1860
+ criteria intact, editing the filter no longer submits, and a rule created
1861
+ end-to-end stores a real record id rather than free text. The criteria authored
1862
+ in the builder is honored by the evaluator: `{"pinned":true}` on an owner-private
1863
+ object granted the recipient exactly the matching records and nothing else.
1864
+
1865
+ - Updated dependencies [1767124]
1866
+ - Updated dependencies [8ecf5a6]
1867
+ - Updated dependencies [dfd3705]
1868
+ - Updated dependencies [6dee2cb]
1869
+ - Updated dependencies [c7cff19]
1870
+ - Updated dependencies [cd09a7b]
1871
+ - Updated dependencies [f1abf0e]
1872
+ - Updated dependencies [f05b84e]
1873
+ - Updated dependencies [662bdf9]
1874
+ - Updated dependencies [059a052]
1875
+ - Updated dependencies [53642d4]
1876
+ - Updated dependencies [8aae006]
1877
+ - Updated dependencies [d147a13]
1878
+ - @object-ui/types@17.0.0
1879
+
3
1880
  ## 16.1.0
4
1881
 
5
1882
  ### Minor Changes
@@ -59,7 +1936,7 @@
59
1936
 
60
1937
  Together with the `^15.1.1` alignment (#2662), a renderer CEL predicate now
61
1938
  reaches the identical verdict as the server — including the framework's
62
- `dateField == today()` equality fix (objectstack-ai/framework#3205) once it
1939
+ `dateField == today()` equality fix (objectstack-ai/objectstack#3205) once it
63
1940
  lands in a published 15.x. The broader home-grown-vs-canonical divergence
64
1941
  motivation is #2661.
65
1942
 
@@ -148,7 +2025,7 @@
148
2025
  and removes both bounds together. A new `drillUrlFilters` module owns the
149
2026
  write/read serialization so both sides can't drift (round-trip tested).
150
2027
 
151
- Companion to the framework analytics change (objectstack-ai/framework#3256).
2028
+ Companion to the framework analytics change (objectstack-ai/objectstack#3256).
152
2029
 
153
2030
  ### Patch Changes
154
2031
 
@@ -163,7 +2040,7 @@
163
2040
  (`visibleWhen`/`readonlyWhen`/`requiredWhen` via `fieldRules.ts`, which delegates
164
2041
  to `@objectstack/formula`'s `ExpressionEngine`) now tracks the 15.x engine — and
165
2042
  will pick up the framework's `dateField == today()` equality fix
166
- (objectstack-ai/framework#3205) automatically at the next 15.x release via the
2043
+ (objectstack-ai/objectstack#3205) automatically at the next 15.x release via the
167
2044
  caret range. Renderer/action `visible`/`disabled` predicates are unaffected (they
168
2045
  use the home-grown JS evaluator — tracked separately in #2661).
169
2046
 
@@ -736,7 +2613,7 @@
736
2613
  (plugin-charts) and `DatasetWidget` (plugin-dashboard) both use it, so
737
2614
  multi-dimension charts render consistently as grouped/coloured bars.
738
2615
 
739
- Refs objectstack-ai/objectui#1759, objectstack-ai/framework#1890
2616
+ Refs objectstack-ai/objectui#1759, objectstack-ai/objectstack#1890
740
2617
 
741
2618
  - 7c239fd: Add `ComponentRegistry.unregister(type, namespace?)` — the inverse of
742
2619
  `register()`. Clears the namespaced key and the bare-name fallback (when it