@object-ui/react 17.6.0 → 17.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +3766 -0
  2. package/README.md +294 -38
  3. package/dist/SchemaRenderer.d.ts +38 -7
  4. package/dist/SchemaRenderer.js +1218 -87
  5. package/dist/context/ActionContext.js +40 -1
  6. package/dist/context/DiscussionContext.d.ts +12 -0
  7. package/dist/context/DiscussionContext.js +2 -0
  8. package/dist/context/DrillNavigationContext.d.ts +13 -0
  9. package/dist/context/InlineEditContext.d.ts +81 -1
  10. package/dist/context/InlineEditContext.js +8 -2
  11. package/dist/context/RecordContext.d.ts +48 -5
  12. package/dist/context/RecordContext.js +10 -1
  13. package/dist/context/RelatedRecordActionsContext.d.ts +5 -1
  14. package/dist/context/SchemaRendererContext.d.ts +54 -2
  15. package/dist/context/SchemaRendererContext.js +38 -5
  16. package/dist/context/ThemeContext.js +1 -1
  17. package/dist/context/actionRunnerTranslator.d.ts +27 -0
  18. package/dist/context/actionRunnerTranslator.js +37 -0
  19. package/dist/element-data-source/ElementDataSourceGate.d.ts +88 -1
  20. package/dist/element-data-source/ElementDataSourceGate.js +326 -13
  21. package/dist/hooks/index.d.ts +3 -1
  22. package/dist/hooks/index.js +10 -1
  23. package/dist/hooks/useActionEngine.js +46 -3
  24. package/dist/hooks/useActionRunner.js +4 -0
  25. package/dist/hooks/useActionTextLocalizer.d.ts +21 -8
  26. package/dist/hooks/useActionTextLocalizer.js +33 -8
  27. package/dist/hooks/useConfigBagEvaluator.d.ts +30 -0
  28. package/dist/hooks/useConfigBagEvaluator.js +39 -0
  29. package/dist/hooks/useDensityMode.d.ts +6 -2
  30. package/dist/hooks/useDensityMode.js +6 -2
  31. package/dist/hooks/useDiscovery.d.ts +1 -10
  32. package/dist/hooks/useETagCache.js +25 -4
  33. package/dist/hooks/useElementDataSource.d.ts +15 -2
  34. package/dist/hooks/useElementDataSource.js +24 -7
  35. package/dist/hooks/useExpression.d.ts +52 -59
  36. package/dist/hooks/useExpression.js +58 -90
  37. package/dist/hooks/useFilterScope.d.ts +22 -2
  38. package/dist/hooks/useFilterScope.js +29 -4
  39. package/dist/hooks/useGlobalUndo.d.ts +10 -0
  40. package/dist/hooks/useGlobalUndo.js +112 -46
  41. package/dist/hooks/useNavigationOverlay.d.ts +76 -11
  42. package/dist/hooks/useNavigationOverlay.js +119 -9
  43. package/dist/hooks/useOffline.js +65 -11
  44. package/dist/hooks/usePageAssignment.d.ts +8 -5
  45. package/dist/hooks/usePageAssignment.js +37 -16
  46. package/dist/hooks/useRecordSearch.d.ts +34 -1
  47. package/dist/hooks/useRecordSearch.js +42 -7
  48. package/dist/hooks/useResolvedFilter.d.ts +46 -0
  49. package/dist/hooks/useResolvedFilter.js +130 -0
  50. package/dist/hooks/useSchemaPersistence.d.ts +7 -0
  51. package/dist/hooks/useSchemaPersistence.js +144 -5
  52. package/dist/hooks/useSettledSchema.d.ts +123 -0
  53. package/dist/hooks/useSettledSchema.js +186 -0
  54. package/dist/hooks/useViewData.d.ts +7 -2
  55. package/dist/index.d.ts +8 -1
  56. package/dist/index.js +45 -1
  57. package/dist/schema-input.d.ts +38 -8
  58. package/dist/schema-input.js +39 -7
  59. package/dist/utils/configBag.d.ts +68 -0
  60. package/dist/utils/configBag.js +70 -0
  61. package/dist/utils/error-message.d.ts +1 -1
  62. package/dist/utils/error-message.js +1 -1
  63. package/dist/utils/inlineAria.d.ts +61 -0
  64. package/dist/utils/inlineAria.js +70 -0
  65. package/dist/utils/nonGridRowCeiling.d.ts +44 -0
  66. package/dist/utils/nonGridRowCeiling.js +78 -0
  67. package/dist/utils/paramsBag.d.ts +28 -0
  68. package/dist/utils/paramsBag.js +146 -0
  69. package/dist/utils/propsBagDiagnostic.d.ts +196 -0
  70. package/dist/utils/propsBagDiagnostic.js +421 -0
  71. package/dist/utils/refusedDataPropDiagnostic.d.ts +44 -0
  72. package/dist/utils/refusedDataPropDiagnostic.js +94 -0
  73. package/dist/utils/unevaluatedExpression.d.ts +42 -1
  74. package/dist/utils/unevaluatedExpression.js +74 -11
  75. package/dist/utils/visibilityDiagnostic.d.ts +271 -19
  76. package/dist/utils/visibilityDiagnostic.js +311 -44
  77. package/package.json +10 -10
  78. package/dist/hooks/useClientNotifications.d.ts +0 -47
  79. package/dist/hooks/useClientNotifications.js +0 -162
  80. package/dist/spec-bridge/SpecBridge.d.ts +0 -28
  81. package/dist/spec-bridge/SpecBridge.js +0 -56
  82. package/dist/spec-bridge/bridges/form-view.d.ts +0 -158
  83. package/dist/spec-bridge/bridges/form-view.js +0 -170
  84. package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
  85. package/dist/spec-bridge/bridges/list-view.js +0 -268
  86. package/dist/spec-bridge/index.d.ts +0 -11
  87. package/dist/spec-bridge/index.js +0 -10
  88. package/dist/spec-bridge/types.d.ts +0 -55
  89. package/dist/spec-bridge/types.js +0 -8
package/CHANGELOG.md CHANGED
@@ -1,5 +1,3771 @@
1
1
  # @object-ui/react
2
2
 
3
+ ## 17.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 97abedc: A `dataSource` binding's own `limit` that the contract refuses is now treated as **not
8
+ authored**: the row cap falls through to the named saved view's usable cap, and only when
9
+ that is absent too to the consumer's own default (objectui#10016).
10
+
11
+ **Behaviour change.** `composeElementDataSource` resolved the cap as
12
+ `config.limit ?? savedViewLimit(view)`. objectui#9928 put a positivity check on the view's
13
+ operand only, so a binding `limit` of `0`, `-10`, `25.5` or a non-number went through
14
+ unchecked:
15
+
16
+ - through `ViewDataProvider.resolveElementDataSource` it reached `DataFetcher.fetchRecords`
17
+ verbatim, and nothing said so;
18
+ - through `ElementDataSourceGate` it was written over everything, a usable component cap
19
+ included, and the consuming block then dropped it and drew its own default, so the read
20
+ went wider than either the view or the component asked for.
21
+
22
+ Both operands now pass the same check, and a refused cap from either is not authored. The
23
+ chain is: a usable binding cap, else a usable view cap, else none. This is the rule
24
+ objectui#10009 set one layer up (a value the contract refuses is not authored, so the other
25
+ source wins), applied to the two operands of one resolver. On the renderer path the view's
26
+ cap that takes the binding's place is a baseline like any other view-sourced value, so a
27
+ usable component cap still wins over it. A usable binding `limit` still beats both, exactly
28
+ as before.
29
+
30
+ **Diagnostics.** `elementDataSourceRefusedLimitMessage` takes two optional trailing
31
+ parameters, the binding and the operand (`'view'`, the default, or `'binding'`). The binding
32
+ operand has its own sentence, naming the binding, so a binding refusal and a view refusal
33
+ are told apart when both fire. `ViewDataProvider` and `ElementDataSourceGate` report it once
34
+ per declaration on the existing `console.warn` channel; the gate reports it only for a block
35
+ that reads a row cap. Called with three arguments the builder answers exactly as before.
36
+
37
+ **Fixed with it.** `ViewDataProvider` reported a saved view's refused cap even when the
38
+ binding's own usable `limit` was the cap actually used, and that warning's claim that the
39
+ fetch falls back to a default was false. The view's refusal is now reported only when the
40
+ binding's `limit` is absent or refused. The builder applies that condition itself, so both
41
+ callers share one copy of it.
42
+ - 2b5f509: fix(react): a data object in a node's `properties` / `props` bag reaches the renderer whole, even when it carries a `source` field
43
+
44
+ `SchemaRenderer` evaluates each value of a node's config bag (`properties`, and its legacy alias `props`) one by one, and handed every value to `ExpressionEvaluator.evaluate`. That call unwraps ANY object with a string `source` to that bare string before it does anything else. So an authored data object that merely had a field called `source` was silently replaced by that one string: an `action:button` with `properties.bodyExtra: { source: 'web', campaign: 'spring' }` sent `"web"` to the action runner instead of the object, with no error and no warning. The same object written at node level, which no loop visits, always arrived intact.
45
+
46
+ This was never one key's problem. Every object-valued input on those two bags took the same path, including values keyed by field name: form `defaultValues` / `initialValues`, a declarative `patch`, filter `values` and a detail view's `data`, where a field named `source` is ordinary.
47
+
48
+ On a key outside the predicate chain, an object now reaches `evaluate` only when it is an Expression envelope in `@objectstack/spec`'s sense, meaning it carries a string `dialect` (the spec's `ExpressionSchema` requires one). Every other object is handed to the renderer exactly as authored.
49
+
50
+ **Behaviour change, stated plainly.** The only value whose meaning changes is an object on a non-predicate config-bag key that has a string `source` and no string `dialect`. It used to collapse to its evaluated `source`; it now arrives as the object. Everything else evaluates as before:
51
+
52
+ - strings are still interpolated;
53
+ - a `{ dialect: 'template', source: '${…}' }` envelope still resolves to its value;
54
+ - the predicate keys (`visible`, `visibleWhen`, `visibleOn`, `visibility`, `hidden`, `hiddenOn`, `disabled`, `disabledOn`, `enabled`) are unchanged: a CEL envelope is still preserved for the canonical engine (objectui#9100 / #9107), and a dialect-less `{ source }` on them is still evaluated, the form `ExpressionWire` declares for `visible` / `hidden` / `disabled`;
55
+ - `params` is still walked leaf by leaf (objectui#7867).
56
+
57
+ Declared as a minor (Clause-②): nothing in the published API is added or removed, but what an authored value evaluates to changes for one shape. `ExpressionEvaluator.evaluate`'s signature admits a dialect-less `{ source }`, and these loops handed it every value except a `params` bag and a CEL envelope on a predicate key. So `{ source: '${…}' }` on a config-bag key outside the predicate chain and `params` now arrives as the object, uninterpolated, and the unevaluated-expression diagnostic does not report the `${…}` inside it. To keep such a value evaluated, write the bare string `'${…}'` or `{ dialect: 'template', source: '${…}' }`; both still interpolate on that path. The spec's envelope always carries `dialect`, and `@object-ui/core`'s `isRuntimeDefault` already reads a dialect-less `{ source }` as a literal value.
58
+ - 6cf5999: fix(components): an action container's members evaluate `properties.params` the way a top-level `action:button` does
59
+
60
+ A member of `action:bar`, `action:group` or `action:menu` never passes through
61
+ `SchemaRenderer`, so the evaluation that resolves `"recordId": "${record.id}"`
62
+ in a node's `properties.params` did not run for it. On a record page an
63
+ `action:bar` member drawn inline handed its `navigate_edit` handler the raw
64
+ `${record.id}` text, and a member that the bar placed in its overflow menu, or
65
+ an `action:group` / `action:menu` item, did not forward its `properties.params`
66
+ at all.
67
+
68
+ Each container now evaluates its members' `properties` with the evaluator and
69
+ scope `SchemaRenderer` uses: `action:bar` where it composes an inline member,
70
+ and `action:group` / `action:menu` when they run an item. A container member's
71
+ static values therefore reach the handler as a top-level `action:button`'s do,
72
+ with one difference: the development-build unevaluated-expression diagnostic
73
+ runs in the `SchemaRenderer` memo, so it does not report an unresolvable
74
+ template on a container member. An item that carries an `ActionParam[]` input
75
+ list forwards both, as `action:button` does. A node-level `params` object is
76
+ still not read as values (objectui#10289). The one exception is unchanged from
77
+ objectui#10462: an `action:group` / `action:menu` item of `type: 'api'` still
78
+ forwards it as the request payload (objectstack#5777 window), unless the item
79
+ also carries a `properties.params` bag, which now takes precedence.
80
+
81
+ `@object-ui/react` exports `useConfigBagEvaluator()` for this. It returns a
82
+ function that applies the `SchemaRenderer` memo's `properties` evaluation (the
83
+ same per-key rule and the same `record` / `current_user` / `page` scope) to a
84
+ config bag rendered without `SchemaRenderer`. It is not a second template
85
+ engine. Its plain half, `evaluateConfigBagInScope()`, is exported beside
86
+ `SchemaRenderer`. Both are built from the module-scope scope builder and
87
+ per-key rule that the memo itself calls.
88
+
89
+ Declared as a minor (Clause-②, widening): `@object-ui/react` gains two public
90
+ exports, `useConfigBagEvaluator` and `evaluateConfigBagInScope`. Nothing is
91
+ removed or renamed.
92
+ - f9c06ef: Every data node that sends its own authored `filter` into a query now resolves the spec's context tokens first (objectui#10666). With `filter: [['owner', '=', '{current_user_id}']]` these nodes used to send the literal token; they now send the signed-in user's id, and `{current_org_id}` resolves to the active organization:
93
+
94
+ - `object-calendar`, `object-map` and `object-tree`, on both the object query and the inline (`provider: 'value'`) query;
95
+ - `object-gantt`, `object-kanban` and `object-timeline`;
96
+ - `record:related_list` and `record:line_items`, where the node's own filter is combined with the parent-record condition;
97
+ - the `element:repeater`, `element:number` (both the `aggregate` filter and the `find` fallback) and `element:record_picker` page elements.
98
+
99
+ `list-view`, `object-grid` and `object-gallery` already did this, and are unchanged.
100
+
101
+ **New in `@object-ui/react`: `useResolvedFilter(filter, scope)`.** Pass it a node's authored filter and `useFilterScope()`. It resolves the filter through `@object-ui/core`'s shared `resolveFilterPlaceholders` and holds the result: re-rendering with an equal filter (even one rebuilt inline on every render) hands back the same value, so a fetch effect keyed on it does not run again, while a structurally different filter or a change of signed-in user or organization resolves again. A filter with no token to resolve is handed back as the value you passed in. The hold is the one `list-view` and `object-gallery` used privately; `plugin-list` now imports it from here, and no second copy remains in that package.
102
+
103
+ What changes for an app:
104
+
105
+ - A filter with no placeholders reaches the query as before, unchanged.
106
+ - Nodes that re-queried whenever the host rebuilt an equal filter inline (`object-calendar`, `object-gantt`, `object-kanban`, `object-map`, `object-tree`) no longer issue those redundant queries.
107
+ - Date macros such as `{today}` in these nodes' own filters are now resolved in the browser's local time, as they already are for `list-view`, `object-grid` and `object-view`. Before, they reached the server as literals, and the server resolved them in the tenant's time zone.
108
+
109
+ `@object-ui/types`: the `filter` docblocks on `ObjectMapSchema`, `ObjectTreeSchema`, `ObjectGanttSchema`, `ObjectCalendarSchema`, `ObjectKanbanSchema`, `ObjectChartSchema` and `ObjectGallerySchema` (and the matching zod descriptions) no longer say the filter is forwarded verbatim; they say its context tokens are resolved first. No type changes.
110
+ - a4b017e: `element:text`, `element:button`, `element:image` and `element:number` now render the accessible name an author declares in their `aria` prop (objectui#11051).
111
+
112
+ **What an author sees change.** These four elements declare the spec's `AriaPropsSchema` as their `aria` prop. The renderer used to put `aria-` in front of each key as written, so `aria: { ariaLabel: 'Order total' }` reached the DOM as `aria-arialabel="Order total"`. No assistive technology reads that attribute, so the element had no accessible name. A locale map (`{ en: 'Order total', 'zh-CN': '订单合计' }`) was written as `[object Object]`. Now:
113
+
114
+ - `ariaLabel` renders `aria-label`. A locale map gives the entry for the display locale (`useDisplayLocale()`), resolved by the spec's own `resolveI18nLabel`.
115
+ - `ariaDescribedBy` renders `aria-describedby` (it rendered `aria-ariadescribedby`).
116
+ - `role` renders `role`, as before.
117
+
118
+ **New export: `resolveInlineAriaProps(aria, locale)` from `@object-ui/react`.** A pure function, not a hook: it maps the spec's nested `aria` bag to those three attributes, and returns only the ones that have a value. It is exported so the renderers in `@object-ui/components` share one mapping. `SchemaRenderer`'s own reader for the FLAT node keys (`ariaLabel` in the keyed `{ key, defaultValue }` form) is unchanged: objectui#4580 Q2-B keeps the two vocabularies apart, and each resolver returns nothing useful for the other's shape.
119
+
120
+ **What is no longer read.** The old helper also passed any raw `aria-*` key straight through, and turned any other key into `aria-KEY`. So an `aria: { label: 'x' }` rendered `aria-label="x"` by accident. The spec refuses both shapes when it parses the document (`unrecognized_keys`), and these four elements now ignore them. When this change was made, a `git grep` found no example app, doc or skill in this repository or in the ObjectStack framework that puts an `aria` bag on these four elements, so no shipped document depended on those shapes. If you wrote one, use `aria: { ariaLabel: 'x' }`.
121
+ - 1263e40: fix(app-shell,plugin-detail): the record feed says "no permission" when its read is refused, instead of showing an empty list
122
+
123
+ This widens two published surfaces: `@object-ui/plugin-detail` gains the export `isRefusedFeedRead`, and `@object-ui/react`'s `DiscussionContextValue` gains the optional members `activityDenied` and `commentsDenied` (as do `RecordActivityTimelineProps` and `RecordChatterPanelProps`).
124
+
125
+ When the server refused a record's activity read (401, 403, or a permission
126
+ envelope), the record page's discussion panel and the `record:activity` block
127
+ rendered the empty state: "No comments yet" or "No activity recorded". A member
128
+ who was not allowed to see the activity could not tell that apart from a record
129
+ with no history. Every failed feed read ended in a catch that treated it as an
130
+ empty answer.
131
+
132
+ Each feed read now keeps its verdict. A refused read shows a localized
133
+ no-permission state in place of the empty state: a lock and "You don't have
134
+ permission to view activity on this record." When the other read did answer, its
135
+ rows stay on screen and the notice above them names the half that was withheld.
136
+ A refused `sys_comment` read shows "You don't have permission to view comments on
137
+ this record." in the same way.
138
+
139
+ - **What counts as refused** is one verdict, `isRefusedFeedRead`, exported from
140
+ `@object-ui/plugin-detail`. Both surfaces use it. It is the `forbidden` and
141
+ `unauthorized` kinds of the existing `classifyLoadError` read classifier, and
142
+ it reads no status of its own.
143
+ - **Unchanged:** a read that answers with zero rows, a 404 (`sys_activity` on a
144
+ deployment without the audit plugin), and any other failure such as a 500 keep
145
+ the empty state. The panel has no error state of its own.
146
+ - **Not retried:** a refusal is recorded and shown; the read is not re-issued.
147
+ - `DiscussionContext` (`@object-ui/react`) gains two optional members,
148
+ `activityDenied` and `commentsDenied`, which the host that owns the fetch
149
+ sets. `RecordActivityTimeline` and `RecordChatterPanel` take the same two
150
+ optional props.
151
+ - Two new `detail.*` strings, `activityAccessDenied` and `commentsAccessDenied`,
152
+ are translated in all ten locale packs.
153
+ - 8bab157: fix: an action gated on `current_user.can(object, verb)` stays hidden until the permissions payload has loaded on every action `visible` surface, a `page:header` action gated through `disabled` stays disabled, and `record:quick_actions` can answer `current_user` at all
154
+
155
+ An action whose `visible` asks `current_user.can(...)` has no answer until the signed-in
156
+ user's permissions have loaded. On `action:group` (both display modes, and the group's own
157
+ `visible`), `action:icon` and a related list's toolbar, that unanswerable gate used to
158
+ SHOW the action, so a button meant for permission holders flashed up for everyone while
159
+ the page loaded, and stayed up where the permissions never load (the `/forms/:name` route,
160
+ a standalone embed). These renderers now treat a `visible` predicate that cannot be
161
+ evaluated the way `action:button` and `action:menu` already did: the action is hidden and
162
+ the console names it once. This is the rule for the key, not a special case for `can`: a
163
+ `visible` that faults for any other reason (a misspelled root such as `nope.x == 1`) is
164
+ hidden too, where it used to be shown.
165
+
166
+ On `page:header`, a `disabled` predicate that cannot be evaluated now renders the button
167
+ DISABLED instead of enabled, so `disabled: "!current_user.can('account', 'delete')"`
168
+ shows a greyed-out button until the answer arrives. This also applies to a `disabled`
169
+ predicate that faults for another reason. A header action's `visible` and `hidden`
170
+ predicates keep their fail directions.
171
+
172
+ `record:quick_actions` filters its actions against the action runner's context, which
173
+ held the host's `user` but never `current_user`, so `current_user.can(...)` and every
174
+ other `current_user.*` gate there hid the action for everyone, the users who hold the
175
+ permission included. `useActionEngine` (`@object-ui/react`) now binds the signed-in user
176
+ from the surrounding `ExpressionProvider` as `current_user`, so a quick action is shown
177
+ to users who hold the permission and hidden from those who don't, and while the
178
+ permissions are loading.
179
+ - 18d1a0a: fix: a row-menu action gated through `disabled` stays disabled until the permissions payload has loaded, in a grid and in a related list's table, and an `<ActionProvider>` answers `current_user` in the runner's own gates
180
+
181
+ A custom row action whose `disabled` asks `current_user.can(...)` has no answer until the
182
+ signed-in user's permissions have loaded. In the row menu of a grid (`RowActionMenu`) and
183
+ of a data table (the related list's rows), that unanswerable gate used to leave the item
184
+ ENABLED, so `disabled: "!current_user.can('account', 'delete')"` offered the action to
185
+ everyone while the page loaded, and kept offering it where the permissions never load (the
186
+ `/forms/:name` route, a standalone embed). Both menus now render a `disabled` predicate that
187
+ cannot be evaluated as DISABLED, the way `page:header` already does, and the console names
188
+ it once. This is the rule for the key, not a special case for `can`: a `disabled` that
189
+ faults for any other reason (a misspelled root such as `nope.x == 1`) is disabled too,
190
+ where it used to be enabled. An absent, empty or blank `disabled` is still not disabled.
191
+ The built-in Edit and Delete keep their fail-soft `disabledWhen`.
192
+
193
+ `<ActionProvider>` (`@object-ui/react`) now binds the signed-in user from the surrounding
194
+ `ExpressionProvider` as `current_user` on its runner. The runner checks an action's
195
+ `disabled` again when the action runs, and that check used to fault on `current_user` unless
196
+ the page also mounted `record:quick_actions`, `record:alert` or a dashboard, so a user who
197
+ held the permission clicked an enabled header action and was told "Action is disabled".
198
+ The check now answers `current_user.can(...)` under any provider mounted inside the
199
+ predicate scope. On the runner's gates `user.can(...)`, `ctx.user.can(...)` and
200
+ `os.user.can(...)` still fault, because there `user` is the host's own user object; write
201
+ `current_user.can(...)`.
202
+ - 2124d04: `useNavigationOverlay` hands an authored `page` click with no `onNavigate` to the record navigator the host publishes (objectui#11293).
203
+
204
+ `page` is the spec's default navigation `mode`, so `navigation: { mode: 'page' }` and every block written without `mode` (for example `{ size: 'lg' }`) resolve to it. The hook's `page` branch called `onNavigate` and nothing else, so a block a host rendered without wiring one — a standalone `object-kanban`, `object-calendar` or `object-grid` placed on a page — opened nothing on click. With no `onNavigate` supplied, the branch now calls `openRecord(objectName, recordId)` from `RelatedRecordActionsContext`, the seam the grid's link column and the lookup cells already read; the hook still builds no record URL for `page`.
205
+
206
+ What does not change: an `onRowClick` still takes the click first, a supplied `onNavigate` still wins (the grid's explicit wiring and the console's list views behave as before), an absent `navigation` stays the host's call, and modifier clicks and `new_window` are untouched. Under a host that publishes no record navigator the click still opens nothing, because there is no record page to open.
207
+ - c476be0: The action success toast is composed from the action's `outcomeMessages`, then its `successMessage`, then the runner's default text. A `message` in the server's answer is no longer shown (objectui#11344).
208
+
209
+ `@objectstack/spec` 17.6.0 declares `ActionSchema.outcomeMessages`, the success copy for each handler outcome. Its keys are the snake_case `outcome` values that a `type: 'api'` or `type: 'script'` handler returns in its success payload. This follows ruling A on objectstack-ai/cloud#2315: the server returns facts, and the console writes the sentence in the user's locale.
210
+
211
+ - `@object-ui/core`: the `ActionRunner` success toast now picks its text in this order:
212
+ 1. the `outcomeMessages` entry named by the top-level `outcome` of the handler's answer;
213
+ 2. `successMessage`;
214
+ 3. the runner's default text, in the language of the translator set with `setTranslator`.
215
+
216
+ Rungs 1 and 2 fill in `${result.*}` tokens from the handler's answer. That is the same scope `onSuccess.navigate` reads, and the values go in as text, not percent-encoded. **Behaviour change:** `result.data.message` used to take precedence over both. The runner no longer reads it at all. To show the server's sentence, write `${result.message}` as the copy. `ActionDef` now declares `outcomeMessages`.
217
+ - `@object-ui/react`: `useActionTextLocalizer` resolves each `outcomeMessages` entry that a named action declares. It looks in `_actions.NAME.outcomeMessages.OUTCOME`, then `globalActions.NAME.outcomeMessages.OUTCOME`, then falls back to the authored text. On a named action, an inline `I18nLabel` map on an entry, or on `successMessage`, is collapsed to the active language first. `${result.*}` tokens pass through unchanged, and the runner fills them in after the action runs.
218
+ - `@object-ui/i18n`: `useObjectLabel()` adds `actionOutcome(objectName, actionName, outcome, fallback)`, the resolver for that address.
219
+ - `@object-ui/components`: `action:button`, `action:icon`, `action:group` and `action:menu` now forward `outcomeMessages` to the runner. Before, a registered action rendered through `action:bar` lost the map one hop before the toast. The `action:button` and `action:icon` registrations do not publish it as an input, because their own spec rows do not declare it at 17.6.0.
220
+ - `@object-ui/types`: `UIActionSchema` declares `outcomeMessages`, derived from the spec.
221
+
222
+ `@object-ui/core` and `@object-ui/types` raise their `@objectstack/spec` floor from `^17.5.0` to `^17.6.0`, because their published types now read `ActionSchema.outcomeMessages`, a member the spec first declares in 17.6.0.
223
+ - 304f611: The spec's page blocks, the `element:text_input` / `element:record_picker`
224
+ rows and a stored page document under its page kind have a TypeScript
225
+ authoring type, and `SchemaRenderer`'s `schema` prop accepts them
226
+ (objectui#11364).
227
+
228
+ `@object-ui/types` exports, each derived by reference and none restated:
229
+
230
+ - `PublicBlockNode`: one member per arm of the zod face's
231
+ `PublicBlockComponentSchema` (`element:text`, `page:tabs`, `action:button`
232
+ and the rest), each the arm's own input. Its `properties` bag is the block's
233
+ `ComponentPropsMap` row in `@objectstack/spec`. `PublicBlockNodeOf<'…'>`
234
+ picks one member.
235
+ - `ElementTextInputNode` and `ElementRecordPickerNode`: the spec rows of the
236
+ two blocks the zod face does not arm yet, each built the way an arm is
237
+ built: the zod `BaseSchema` shape extended with the `type` literal, the node
238
+ envelope (`responsiveStyles`, the spec's `ResponsiveStylesSchema`),
239
+ `properties` as the row, and the `body` / `children` refusals every arm
240
+ carries. The record picker also declares the node-level `dataSource` its
241
+ renderer reads.
242
+ - `PageDocumentNode`: the spec's `PageSchema` input with `type` (the page
243
+ kind) required, the shape `PageView` hands to `SchemaRenderer`.
244
+ - `AuthoringNode`: the union of all of them.
245
+
246
+ `@object-ui/react`'s `SchemaRendererProps.schema` is now
247
+ `BaseSchema | AuthoringNode | string | null | undefined`, and
248
+ `toRenderableSchema`, which returns `SchemaRendererProps['schema']` by
249
+ reference, widens its return type with it. Every value the old union accepted
250
+ is still accepted. Reading is a different matter: on a union, a key that one
251
+ closed member does not declare is TS2339, whatever `BaseSchema`'s index
252
+ signature allows. No caller in this repository reads a key off the prop's type
253
+ or off the bridge's return (the `Type Check` job re-derives that); code that
254
+ does needs to narrow first. Once objectui#8347 removes the index signature,
255
+ a key misspelled inside one of these nodes' bags is refused, and the spec's
256
+ spelling compiles. None of these types carries an index signature, and the
257
+ prop gains none.
258
+
259
+ ⚠️ **Dated note, 2026-10-02 — the prop takes the declared-node union — objectui#11466.** At this change `SchemaRendererProps.schema` was `BaseSchema | AuthoringNode | string | null | undefined`, and every value the old union accepted was still accepted; now, later in this same release, it is `DeclaredNode | string | null | undefined`. `DeclaredNode` (`@object-ui/types`) is the union, keyed by `type`, of the component schemas `AnySchema` declares (without its `BaseSchema` arm and without the app-level document `AppComponentSchema`), `AuthoringNode`, and the types an application declares in `CustomNodeRegistry`, so a value typed `BaseSchema` or a `type` nothing declares is refused. `PageDocumentNode` admits every page kind but the interface-mode `list`. The rest of this entry is kept as the reading of this change.
260
+ - 83e3f83: A node slot and `SchemaRenderer`'s `schema` prop take the union of the declared node types, `DeclaredNode` (objectui#11466).
261
+
262
+ **BREAKING (TypeScript authoring face only), shipped as `minor` per this repository's version policy.** `SchemaNode` was `BaseSchema | string | number | boolean | null | undefined`, and the prop was `BaseSchema | AuthoringNode | string | null | undefined`. Both object members are now `DeclaredNode`, a new export of `@object-ui/types`: the discriminated union, keyed by the literal `type`, of
263
+
264
+ - every component schema `AnySchema` declares, without its `BaseSchema` arm (whose `type` is `string`) and without `AppComponentSchema` (the app-level document, which `AppSchemaRenderer` reads structurally and `ComponentRegistry` never dispatches: as a node, `type: 'app'` is the stored page of that kind);
265
+ - every spec-declared `AuthoringNode`;
266
+ - every type an application declares in the new `CustomNodeRegistry` interface.
267
+
268
+ There is no `type: string` arm and no index signature. What moves:
269
+
270
+ - A node whose `type` no declaration names is refused, nested or at the prop: `{ type: 'card', children: [{ type: 'txt' }] }` no longer compiles. So is a value typed `BaseSchema`, which names no declared type.
271
+ - An inline child is checked against its own type's arm wherever it is nested, so a misspelled key on a node type without an index signature (the `AuthoringNode`s, a closed `CustomNodeRegistry` entry) is refused at the slot. Node types that extend `BaseSchema` keep its index signature until objectui#8347 removes it.
272
+ - A node's REQUIRED keys are required (a stored `home` page document needs its `label`, a `data-table` its `columns`), because no `BaseSchema` arm accepts the same literal structurally any more.
273
+ - `app` and `list` are one arm each: `PageDocumentNode` admits every page kind but the interface-mode `list`, which `PageView` renders through `InterfaceListPage` and never hands to `SchemaRenderer`. Narrowing a `DeclaredNode` on `type === 'app'` gives `PageDocumentNode`, and on `'list'` gives `ListSchema`. `SchemaByType` reads `AnySchema` and does not move.
274
+ - `@object-ui/core`'s schema builder takes `DeclaredNode` where it took `BaseSchema`: `.child()` / `.children()` on the grid and flex builders and the card builder's `.content()`.
275
+ - `toRenderableSchema` (`@object-ui/react`) follows `SchemaNode` and the prop by reference.
276
+
277
+ What does NOT move: every zod face (`AnyComponentSchema`, `SchemaNodeSchema`, the strict authoring face) and every runtime path. `zod/base.zod.ts` writes the node slot's zod type out through type aliases so the declared-node union can name the zod-derived `AuthoringNode`s without a circular reference; the schema objects are unchanged. `FlexBlockNode` is now an interface for the same reason, with the same members. `@object-ui/components`' `kind: 'html'` page crosses its runtime-parsed tree into `DeclaredNode` at one documented boundary, after `validateTree` reports no error; it renders exactly what it rendered.
278
+
279
+ **Migration.** Annotate a node with its declared type, or with `DeclaredNode` where any node goes. Declare each type you register with `ComponentRegistry` in `CustomNodeRegistry`:
280
+
281
+ ```ts
282
+ import type { BaseSchema } from '@object-ui/types';
283
+
284
+ interface MyWidgetSchema extends BaseSchema {
285
+ type: 'my-widget';
286
+ customProp?: string;
287
+ }
288
+
289
+ declare module '@object-ui/types' {
290
+ interface CustomNodeRegistry {
291
+ 'my-widget': MyWidgetSchema;
292
+ }
293
+ }
294
+ ```
295
+
296
+ An entry joins the union under its KEY (the arm is the entry intersected with `{ type: KEY }`), so it never adds a `type: string` arm. A value built at runtime whose `type` the compiler cannot know is narrowed to a declared type, or crosses at one validated boundary, as the html-tier page does.
297
+ - 1b1d772: Expression-bindable text keys: `statistic.value`, `card.title`, `button.label`
298
+ and their siblings now evaluate `${...}` on the node (objectui#4795 Direction 1,
299
+ maintainer ruling 2026-08-25).
300
+
301
+ **What changes for you.** Four text keys — `title`, `label`, `value`,
302
+ `description` — can now carry an expression written directly on the component
303
+ node, on the component types that declare them:
304
+
305
+ | Component | Bindable node keys |
306
+ |---|---|
307
+ | `statistic` | `label`, `value`, `description` |
308
+ | `card` | `title`, `description` |
309
+ | `button` | `label` |
310
+
311
+ ```json
312
+ { "type": "statistic", "label": "Active users", "value": "${data.metrics.active}" }
313
+ ```
314
+
315
+ That node used to render the literal text `${data.metrics.active}`. A dashboard
316
+ `statistic` previously had no way at all to bind a dynamic number — the
317
+ documented workaround (moving the key under `props`) evaluated the value and
318
+ then discarded it, painting a blank card instead. Both shapes are fixed by the
319
+ same change: the value is evaluated once, at the single place that produces
320
+ evaluated schema, and lands where the renderers already read.
321
+
322
+ **No component behaviour changed.** `statistic.tsx`, `card.tsx` and `button.tsx`
323
+ are untouched — they always read these keys off the node; nothing was writing an
324
+ evaluated value there.
325
+
326
+ **Scope, and how it grows.** The list is closed and lives in
327
+ `@objectstack/spec` (`EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT`); the renderer
328
+ reads that declaration rather than keeping a copy. On any other component type
329
+ these four keys are still read raw, so an expression reaches the screen as
330
+ literal text — notably `text`, whose `value` is read but has no declaration.
331
+ Adding a type or a key is a change to the spec, never something the renderer
332
+ infers.
333
+
334
+ **Nothing is newly rejected.** This release only widens what evaluates; no
335
+ metadata that used to render now fails to. The build-time rejection of `${...}`
336
+ in undeclared keys — the second half of the same ruling — is not in this release
337
+ and is still open.
338
+
339
+ Published authoring guidance updated to match: `skills/objectui/rules/protocol.md`
340
+ (new "Bindable Text Keys" rule), plus the `page-builder`, `schema-expressions`
341
+ and `data-integration` guides, which taught the now-retired "never evaluated"
342
+ statement and its host-pre-resolution workaround.
343
+ - 30c73cd: A `disabled` / `disabledOn` predicate that cannot be evaluated is now reported on the console, in development **and** in production — and the message says what this gate's fail-soft default actually did.
344
+
345
+ `SchemaRenderer` routes six visibility legs (`visibleWhen`, `visible`, `visibleOn`, `visibility`, `hidden`, `hiddenOn`) through one reporter, and called `evaluateCondition` **bare** on the two enablement legs — the only uninstrumented predicate pair in the file. A faulting `disabled` predicate had therefore never been reported in any build, in any dialect that does not report on its own.
346
+
347
+ It is also the pair whose fail-soft answer **bites**. `evaluateCondition` answers an unevaluable predicate with `true`; on the negated visibility legs that means SHOWN, here it means GREYED OUT. So the user got a control they could see and could not use, and the author got nothing to grep for.
348
+
349
+ - **Wiring only, one engine call.** Both legs pass `EvaluationOptions.onFault` (objectui#6038's seam), which hands back the fault the evaluator has already caught. No `throwOnError`, no second evaluation, no `__DEV__` split — dev and production print identical bytes.
350
+ - **No verdict moves.** The fail-soft `true` is preserved deliberately: a faulting `disabled` predicate still disables, exactly as before. Flipping that is a shipped-behaviour change and is not part of this.
351
+ - **Its own copy, not the visibility reporter's.** The shipped line says the safe default meant the gate "did NOT bite", which is written about a gate that shows the node. This gate's line says the opposite, because the opposite is true: `[ObjectUI] An enablement predicate could not be evaluated`, then the node, the key, the source, the engine's reason, and that the node renders disabled — on screen, greyed out — with nothing else on screen to say a predicate failed. One reporter, one dedupe, one severity; a second message.
352
+ - **Rate-limited exactly as the visibility gate is**, per `(node type, key, predicate source)`: two hundred rows of one broken predicate print one line, a second distinct source still prints, and the same source authored on `disabled` and on `visibleWhen` prints two — the gates cannot silence each other.
353
+ - 830ed58: The record page's approval band offers its **Recall** button to the approval's submitter
354
+ only.
355
+
356
+ Field report on `@objectstack/*@17.2.0`: user A submits a record into a 4-level approval;
357
+ user B — not the submitter, read access, not an admin — opens the record and the band still
358
+ lights a clickable recall button. The click cannot succeed. The recall endpoint authorizes
359
+ on submitter identity and refuses everyone else, so the only outcome available to that
360
+ button was a failure toast. Record state was never at risk; this was purely a
361
+ writability-feedback mismatch, the same family as objectui#3794.
362
+
363
+ The button's only gate was `dataSource.cancelPendingApproval` — "can this adapter recall at
364
+ all" — which is a question about the DataSource, not about the viewer. Identity now joins
365
+ it, threaded the way every other signal on that band already travels: the HOST resolves it
366
+ and passes it through `InlineEditProvider`, so the renderer stays DataSource-agnostic and
367
+ never re-derives who submitted what.
368
+
369
+ - `@object-ui/react` — `InlineEditProvider` accepts `approvalIsSubmitter`, surfaced on
370
+ `InlineEditContextValue`. Additive and optional; no existing prop changes.
371
+ - `@object-ui/plugin-detail` — the band's recall button is withdrawn when that signal is a
372
+ resolved `false`.
373
+ - `@object-ui/app-shell` — `RecordDetailView` resolves the verdict from its existing
374
+ approvals read and threads it.
375
+
376
+ **The signal is tri-state, and the third state is the load-bearing one.** `true` offers
377
+ recall, `false` withdraws it, and **`undefined` — a host that resolves no approval identity
378
+ — renders exactly as it did before this release.** Omission preserving prior behaviour
379
+ mirrors how `approvalPending` falls back to `locked`. Defaulting the unknown case to "hide"
380
+ would have traded a cosmetic defect for a functional loss: every host whose band runs off
381
+ the record's `approval_status` mirror alone would silently lose its submitter's only way to
382
+ unlock their own record.
383
+
384
+ **Withdrawn rather than disabled-with-reason.** The card offered either. For a
385
+ non-submitter this control is never actionable on any pending record, so a permanently
386
+ disabled button is standing clutter rather than a lesson; and the two sibling submitter
387
+ levers already hide — the approvals panel's Remind button, and the declared
388
+ `approval_recall` action's `visible` predicate. The band, its quorum tally and the
389
+ approvals timeline still tell a non-submitter exactly what state the record is in. Only the
390
+ lever they can never pull is gone.
391
+
392
+ **This changes no permission.** Nothing about what the server allows moves, `canEdit` and
393
+ the approval lock are untouched, and nothing downstream reads `approvalIsSubmitter` as an
394
+ authorization verdict — the recall endpoint remains the sole authority, and it refused
395
+ these callers before this change and refuses them after. There is deliberately **no admin
396
+ carve-out** (the reporter ruled that case out, cf. objectstack#9464).
397
+
398
+ The derivation itself is now one function, `isSubmitterOf` — server-resolved
399
+ `viewer.is_submitter` first (framework#3310), an id comparison as the fallback for backends
400
+ that predate it, joined with `??` so a server that resolved `false` is believed rather than
401
+ re-litigated client-side. The approvals panel's Remind gate, which already carried that
402
+ expression inline and whose behaviour is unchanged, now reads the same answer: two copies
403
+ would have been two definitions of who submitted.
404
+
405
+ The **untranslated refusal text** the reporter also saw ("No pending approval request found
406
+ for this record", concatenated after a localized prefix) is a separate defect and is not
407
+ addressed here; it is tracked on objectstack#11993.
408
+ - c71e14d: **The unresolvable-visibility-predicate report now names the roots of the tier the
409
+ predicate was actually evaluated against** (objectui#6487). An app-shell author
410
+ whose nav, area or field `visible` faulted was told to check `record` and
411
+ `page.<var>` — two roots that tier does not bind at all.
412
+
413
+ `formatUnresolvableVisibilityMessage` and `reportUnresolvableVisibilityPredicate`
414
+ (both exported from `@object-ui/react`) take a new **optional sixth argument**, a
415
+ `PredicateScopeTier` — also exported — selecting the closing advice paragraph.
416
+ Everything above that paragraph is unchanged on every surface, and so is every
417
+ verdict: this is diagnostics copy only.
418
+
419
+ **The published signature grew; nothing existing breaks.** The argument defaults
420
+ to `'page-component'`, so a five-argument call keeps printing the bytes it
421
+ printed before. All three in-repo call sites pass their tier explicitly rather
422
+ than lean on that default.
423
+
424
+ ⚠️ **Dated note, 2026-09-29 — the call-site count above has since moved —
425
+ objectui#6445.** "All three in-repo call sites" above held when this change landed
426
+ (`c71e14d1c`), counting gates: `reportUnresolvableVisibilityPredicate` was called from
427
+ `SchemaRenderer`'s node visibility gate (twice), the `page:tabs` item gate and
428
+ `ExpressionProvider`'s chrome gate, four calls in three files. Later in this same release
429
+ objectui#6445 (PR objectui#6510) added a fourth gate, `SchemaRenderer`'s `disabled` /
430
+ `disabledOn` enablement gate. Re-measured for objectui#10979 on `main` at `2eaf5be27`,
431
+ the reporter was called five times outside its own module, from four gates in the same
432
+ three files, and all five calls passed their tier explicitly, so none leaned on the
433
+ default. `.changeset/6445-disabled-gate-fault-diagnostic.md` (PR objectui#6510) states
434
+ what ships; the text above is kept as the reading of this change.
435
+
436
+ Each tier's root set was derived from the code that builds the bag, not from the
437
+ prose that described it:
438
+
439
+ - **`'page-component'`** — `SchemaRenderer`'s node gate and `page:tabs` item
440
+ predicates. Both bind `record`, `current_user` and `page.<var>` (the roots
441
+ `@objectstack/spec`'s `ui/page.zod.ts` declares for the tier). Its paragraph is
442
+ byte-for-byte what it was.
443
+ - **`'app-shell'`** — the chrome gate `ExpressionProvider.evaluateVisibility`
444
+ runs, wired onto this reporter by objectui#6443. Its evaluator is built from
445
+ `{ current_user, user, ctx: { user }, os: { user }, data, features }`, so
446
+ the line now names `current_user` with its three ADR-0068 alias spellings and
447
+ `features` — the deployment-flag root that provider documents for exactly
448
+ this kind of predicate — and states outright that `record` and `page.<var>`
449
+ do not exist there. (That bag and that paragraph also carried an `app` root
450
+ when this entry was first written; objectui#8155 removed it from both before
451
+ either shipped, so the released message names five roots, not six.)
452
+
453
+ **Why not generalise the copy instead.** Dropping the concrete root names would
454
+ have made one paragraph true everywhere at the cost of making it useful nowhere:
455
+ an author who mistyped a root needs to know which roots exist *at their tier*,
456
+ which is the whole reason the paragraph is read.
457
+
458
+ `data` is bound at the app-shell tier but is deliberately not advertised there —
459
+ every mount of `ExpressionProvider` in this repo passes `data={{}}` or omits it,
460
+ so naming it would point an author at a root that answers nothing.
461
+ - 748494b: The unresolvable-predicate diagnostic tells `hidden` / `hiddenOn` authors what actually
462
+ happened to their node (objectui#6503). Those two legs are the only ones in
463
+ `SchemaRenderer`'s visibility chain whose verdict is NOT negated: `evaluateCondition`
464
+ answers an unevaluable predicate with `true` on every path, the four negated legs turn that
465
+ into SHOWN, and these two return it as-is, so the same `true` sets `_hidden` and the
466
+ component returns `null`. Both were handed the consequence paragraph written for the negated
467
+ legs — "the gate did NOT bite - a predicate that cannot be evaluated reads on screen exactly
468
+ like one that said yes" — which is the opposite of what an author whose block VANISHED is
469
+ looking at. That line exists to name their predicate, and instead it sent them to hunt a
470
+ rendering bug that does not exist.
471
+
472
+ The two legs now print their own paragraph: the safe default is the one that BITES here, the
473
+ node was REMOVED and is not on the page at all, an absent node is indistinguishable from
474
+ metadata that meant to hide it, and nothing is wrong with the renderer.
475
+
476
+ Copy only — no verdict moved. The node still vanishes, which is the shipped fail-soft the
477
+ neighbouring family (objectui#3862 / #3955 / #6443 / #6487 / #6445) preserved deliberately,
478
+ and every case in the new suite pins the verdict beside the sentence.
479
+
480
+ `PredicateGateKind` — re-exported from `@object-ui/react`'s entry — gains a third member,
481
+ `'concealment'`, joining `'visibility'` and `'enablement'`. The opening line is deliberately
482
+ UNCHANGED (`UNRESOLVABLE_VISIBILITY_PREFIX`): these are visibility predicates, and an app or
483
+ a test filtering the console by that constant must go on catching them. Consumers that
484
+ switch EXHAUSTIVELY over the union, or key a `Record` by it, gain a third case to answer;
485
+ no runtime signature moved and every value accepted before still is.
486
+ - dc4365c: Retire the discovery-wire preview mode — the console no longer turns
487
+ authentication off because a server said `mode: 'preview'` (objectui#6654).
488
+
489
+ `@objectstack/spec` retired the `RuntimeMode` value `'preview'` and the whole
490
+ `PreviewModeConfig` block (objectstack `0c2334f6c`). This console still read that
491
+ surface back off the runtime discovery payload, which is a different layer from
492
+ the retired compile-time type — so the consumption could not simply be assumed
493
+ dead, and its removal was ruled deliberately (2026-08-29).
494
+
495
+ - `ConditionalAuthWrapper` (`@object-ui/app-shell`) drops the branch gated on
496
+ `discovery.mode === 'preview'`. That branch called `setAuthEnabled(false)` and
497
+ simulated an identity out of `discovery.previewMode`, every field behind a
498
+ default. Auth availability is now decided **only** by the ADR-0076 D12 service
499
+ reading (`isServiceUsable(discovery.services.auth)`), exactly as for any other
500
+ mode.
501
+ - `DiscoveryInfo` (`@object-ui/react`) drops the `previewMode` block and stops
502
+ documenting `'preview'` as a runtime mode; the package README's discovery
503
+ section is updated to match.
504
+
505
+ **Accepted failure direction:** a deployment that still emits `mode: 'preview'`
506
+ or a `previewMode` block now falls back to the ordinary auth reading — it
507
+ requires login. That is loud, diagnosable and more secure than keeping a dormant
508
+ auth-off path keyed on a spelling the platform no longer produces.
509
+
510
+ **Not affected:** `AuthProvider`'s `previewMode` prop, `useAuth().previewMode`
511
+ and `PreviewBanner` in `@object-ui/auth` are a separate published capability
512
+ with a different producer (a host passing the prop). Only the discovery-wire
513
+ producer of that prop is retired; hosts that pass it explicitly are unchanged.
514
+ - e321d52: `useSchemaPersistence` refuses to save a schema carrying function-valued keys
515
+ (objectui#6658).
516
+
517
+ The designer save door handed the schema straight to `JSON.stringify`, which
518
+ does not preserve a function value and says nothing about it: an object key is
519
+ **dropped**, an array element is **coerced to `null`**. No throw, no warning.
520
+ `save()`
521
+ resolved with the id, `error` stayed null and `lastSavedAt` advanced, so every
522
+ observable signal reported success while the stored schema had quietly lost the
523
+ handler. The failure ordering was the bad one: loss at save, symptom at render
524
+ or click, arbitrarily later, in a different component, with no link back.
525
+
526
+ Per the 2026-08-29 maintainer ruling on objectui#6658, the door now refuses such
527
+ a save instead of performing a lossy one:
528
+
529
+ - `save()` walks the schema for function-valued keys **anywhere** in it —
530
+ nested objects and array elements included — before serialization.
531
+ - On a hit it sets `error`, returns `null`, leaves `lastSavedAt` and `isDirty`
532
+ untouched, and never reaches the adapter. The message names the exact
533
+ offending key paths (`columns[2].cell`, `toolbar.actions.onExport`) and both
534
+ escapes: strip the callables before saving, or use the declarative form.
535
+ - The guard sits at the **hook layer**, not inside the default localStorage
536
+ adapter, so a host-injected or REST adapter is covered too — the documented
537
+ REST adapter has the identical `JSON.stringify` shape, so a host following
538
+ the docs inherited the behaviour rather than escaping it.
539
+
540
+ True lowering (objectstack's server-side `lowerCallables`) is recorded as
541
+ unavailable at a browser door: functions cannot round-trip storage without a
542
+ code registry, so refusal is the honest shape.
543
+
544
+ Behaviour change for hosts that previously saved callable-bearing schemas —
545
+ those saves were already losing the callables and now fail loudly instead.
546
+ Fully declarative schemas are unaffected and store byte-identically to before.
547
+ No published type or signature changed.
548
+ - 3beef6d: The spec's `dataSource` element binding is now DECLARED by the blocks that read
549
+ it, so the html tier stops reporting the one working saved-view spelling as
550
+ `unknown-prop` (objectui#6678).
551
+
552
+ `PageComponentSchema.dataSource` — `{ object, view, filter, sort, limit }` — is
553
+ the one spelling that resolves a saved view for an object-bound block. It works,
554
+ and it drew the identical `unknown-prop` warning as the two spellings that do
555
+ nothing (`viewName`, `view`), because `validateTree` looks a prop up in the
556
+ block's declared `inputs` and no registration declared this key. On the tier
557
+ built to accept AI-authored pages, where the diagnostic IS the contract, the
558
+ only signal pointed away from the key that works.
559
+
560
+ Adopting the maintainer ruling of 2026-08-29 — option B **in the injection
561
+ form**:
562
+
563
+ - `ELEMENT_DATA_SOURCE_INPUT` is the single declaration, in `@object-ui/core`
564
+ beside the binding's own semantics; `Registry.register` emits it for any
565
+ registration whose renderer passed through the new `elementDataSourceBlock()`
566
+ seam. One mechanism, one copy — not a hand-kept declaration per block, which is
567
+ the shape that drifts and that a new block forgets. The seam lives in
568
+ `@object-ui/core` and is re-exported by `@object-ui/react` beside
569
+ `ElementDataSourceGate` for discoverability; call sites take the core import,
570
+ because a registration runs at module scope and this repo's suites partially
571
+ mock `@object-ui/react`.
572
+ - Seventeen renderers, in thirteen files across twelve packages, reach the seam
573
+ and now publish the key to the save gate, the parser whitelist, the generated
574
+ JSX authoring types and the block list. The card named nine blocks; the tree
575
+ also has `plugin-grid`, `plugin-timeline`, two further `plugin-form` blocks and
576
+ `element:record_picker` — nothing was hand-listed, so the mechanism covered
577
+ them. `element:record_picker` consumes the gate's HOOK and status panels rather
578
+ than the wrapper tag (its object lives under `properties`), and was found by a
579
+ render probe rather than by reading sources.
580
+ - `dataSource` on a block that does NOT read it (`flex`, `card`) still reports
581
+ `unknown-prop`. Adding the key to `sdui-parser`'s `BASE_PROPS` was refused for
582
+ exactly this reason — that set mirrors `BaseSchema`, and silencing the key
583
+ everywhere would make the diagnostic lie in the other direction.
584
+ - New `check:element-data-source-declaration` fails any source that consumes the
585
+ gate without reaching the seam, so a block added tomorrow cannot forget.
586
+
587
+ Behaviour of the binding itself is unchanged — this is a declaration, not a
588
+ resolution change. The saved view still resolves its columns, and an
589
+ unresolvable `view` still fails loudly rather than widening to the object's full
590
+ scope.
591
+
592
+ The spec/registry parity gates (repo-wide and the `record:related_list` per-block
593
+ pin) now derive their accepted set from the WHOLE node contract rather than from
594
+ `ComponentPropsMap[type]` alone. `PageComponentSchema` accepts and keeps
595
+ `dataSource` on a page-component node — it is a node-level key, a sibling of
596
+ `type` and `className`, not a per-block prop — so the gates' previous complaint
597
+ was measurably wrong. Derived from the spec, not exempted, and both still
598
+ discriminate against an invented key.
599
+ - 26896c6: `element:*` renderers stop re-reading a degenerate config bag as its own character indices — the third and last channel of the objectui#6752 / objectui#6760 hazard (objectui#6783).
600
+
601
+ Five modules under `packages/components/src/renderers/basic/` — `elements.tsx`, `data-list.tsx`, `text-input.tsx`, `record-picker.tsx`, `metadata-viewer.tsx` — each carried a copy of the same reader, `{ ...(schema?.props ?? {}), ...(schema?.properties ?? {}) }`. `??` only replaces `null`/`undefined`, so a non-object bag went into the object spread and came back out as indexed keys: for `properties: 'not-a-bag'`, the config bag a renderer received was `{ '0': 'n', '1': 'o', … '8': 'g' }` — nine keys nobody authored. The five copies are now one `readProps` (`renderers/basic/readProps.ts`) that asks `isConfigBag`, and a degenerate bag on either side contributes no keys.
602
+
603
+ `@object-ui/react` exports `isConfigBag` from its package entry. That is the API addition here, and it is the reason the fix is not a sixth spelling of the predicate: objectui#6761 converged six occurrences of "is this a real config bag?" behind one definition in `packages/react/src/utils/configBag.ts` and pinned it, but the pin scans `packages/react/src` only — a copy written one package over would be invisible to it. `@object-ui/components` already depends on `@object-ui/react` (all five modules import from it today), so the reachable answer was to publish the definition rather than retell it. Same reason the node-gate predicate reporter is exported at that entry (objectui#6038): one definition, read by every package that asks.
604
+
605
+ **What this does not change, measured rather than predicted.** No rendered output moves on today's tree. All five renderers read named keys off this bag, and the single onward spread — `metadata-viewer`'s `<StateMachineView {...props} />` — hands it to components that destructure named fields, so the indexed keys were computed and then dropped. The census behind objectui#6708 found zero authored nodes carrying a degenerate config bag, so this was a latent shape, not a live failure. What the guard buys is what objectui#6752 measured its own guard buys, one channel further down: the authored value's shape is not reinterpreted. objectui#5123's precedence is untouched — `properties` still wins a contested key, and a degenerate bag declares no key for either side to win.
606
+ - b87f15b: `useOffline` auto-syncs mutations queued while already online (objectui#6818).
607
+
608
+ The auto-sync effect was keyed `[isOnline, enabled]` with
609
+ `react-hooks/exhaustive-deps` suppressed, so its `queue.length === 0` guard was
610
+ evaluated against the queue as it stood when `isOnline` or `enabled` last
611
+ changed. `queueMutation` has never been conditional on being offline — it
612
+ accepts entries whenever the hook is enabled — so anything queued while ALREADY
613
+ online found the effect asleep, and nothing re-ran it. Only an explicit `sync()`
614
+ drained those mutations; the hook whose job is auto-sync did nothing for them.
615
+
616
+ The suppression's stated reason ("only trigger on `isOnline` changes, not on
617
+ every queue change") was about TIMER RESTARTS, and it is kept: the effect is
618
+ keyed on the **boolean** `queue.length > 0`, never on `queue` or `queue.length`,
619
+ so queueing a second mutation while the 100ms stabilization timer is already
620
+ armed still does not re-run the effect or restart the timer. What the
621
+ suppression never justified — the early return against a stale snapshot — is
622
+ what changed.
623
+
624
+ `sync` also read `batchSize` through a ref (newest) while reading `queue` from
625
+ its own closure (a snapshot), so the two halves of one call disagreed about how
626
+ current they were, and the auto-sync effect retains such a closure by design.
627
+ The queue now reaches `sync` through the same commit-phase mirror the sync
628
+ config uses, so both halves are the newest committed values. That also takes
629
+ `queue` out of `sync`'s dependency list: `sync` is keyed `[enabled]` and is
630
+ stable across queued mutations, which is what lets the effect name every value
631
+ it reads and drop the `eslint-disable` entirely rather than reword it.
632
+
633
+ **Behaviour change, graded `minor` deliberately.** `useOffline` is published and
634
+ its out-of-repo population is unmeasured; the single in-repo caller
635
+ (`AppHeader`) destructures `isOnline` only and is unaffected. A consumer that
636
+ called `queueMutation` while online and relied on nothing being sent until it
637
+ called `sync()` itself will now see that mutation flushed ~100ms later.
638
+ `sync`'s identity is also more stable than before — it no longer changes on
639
+ every queued mutation — which is safe for effects keyed on it but is a visible
640
+ difference.
641
+
642
+ Not changed here: a `batchSize` smaller than the queue still drains one batch
643
+ and leaves the remainder for the next transition, because whether one auto-sync
644
+ should chain batches until the queue is empty is a separate question about what
645
+ `batchSize` means, not about this guard. Filed as objectui#6857.
646
+ - 03380aa: Inline edit: a rejected save now says WHICH field the server refused, and why.
647
+
648
+ Editing a record in place on a detail page and hitting Save used to surface the
649
+ backend's own string when the write was refused — `VALIDATION_FAILED:
650
+ Validation failed for crm_opportunity` — leaving the user to guess which of the
651
+ fields they had just edited was the problem. The refusal has always been
652
+ field-scoped (`@objectstack/objectql`'s validators throw `VALIDATION_FAILED`
653
+ with `fields[]`, and both the REST layer and the runtime dispatcher pass those
654
+ entries through intact); the inline surface was the last one still dropping
655
+ them. `<InlineEditSaveBar>` now renders one reason per rejected field, named by
656
+ that field's own label — the same treatment record forms have had since #3222.
657
+
658
+ Attribution never guesses. It reads the envelope through
659
+ `@object-ui/react`'s `extractFieldErrors`, the single in-repo normaliser the
660
+ form surface already uses, and an entry with no usable `field` is dropped
661
+ rather than pinned on whichever input is nearby. In the drawer's callback mode,
662
+ where persistence loops `onFieldSave(field, value)` one key at a time, a
663
+ rejection is attributed to the key that was in flight — a fact about the write,
664
+ not an inference. Anything that is not field-scoped (a network failure, a
665
+ permission denial) keeps the cleaned single-line message it had before.
666
+
667
+ `@object-ui/react` gains one additive public API member to carry this, and it
668
+ is the reason that package's entry is `minor` rather than `patch`:
669
+ `InlineEditContextValue` now has **`fieldErrors`** — a nullable map of field
670
+ machine name to the server's reason — alongside a **`setFieldErrors`** setter,
671
+ the exact companions of the `error` / `setError` pair that interface already
672
+ carried. Nothing is removed and nothing changes shape, so every existing host
673
+ and consumer compiles and behaves as before; a host that never reads the new
674
+ member sees no difference. It exists because the save bar and the field rows
675
+ are SIBLINGS under `InlineEditProvider` in both persistence modes, so before
676
+ this there was no channel between the component that receives a refusal and the
677
+ components that render the fields it is about.
678
+
679
+ Also recorded in code, per the maintainer's ruling on objectui#6868: **the
680
+ server is the validation authority on the inline-edit surface.** That was
681
+ previously an absence — `InlineFieldInput` runs no rules and takes no `error`
682
+ prop — and it is now a decision, written into both modules' headers with a
683
+ pointer to the ruling. No client-side rule evaluator was added, and none should
684
+ be: the server is the only rule source, and this surface only presents it.
685
+ - 7c3df8f: A non-grid view's fetch now carries a platform row ceiling, and crossing it is
686
+ never silent (objectui#7210, maintainer ruling a′, 2026-09-02).
687
+
688
+ Before this, `ObjectGantt`, `ObjectCalendar`, `ObjectMap` and `ObjectTree` each
689
+ issued a `find` with **no `$top` at all**, so the request returned the entire
690
+ filtered result set. At the 186 rows the card was filed from that is invisible;
691
+ on an object with 100k scheduled rows it is the whole table into the browser,
692
+ and nothing an author could write — `pagination.pageSize` included — could
693
+ bound a request that never carried a cap to begin with.
694
+
695
+ **What changed.** Those four fetches now ask for `NON_GRID_ROW_CEILING_TOP`
696
+ rows, draw at most `NON_GRID_ROW_CEILING` of them, and when the result set was
697
+ larger they render a footnote naming both numbers, verbatim as it renders:
698
+ *"Showing the first 2000 of 41234 records. Narrow the filter."* Below the
699
+ ceiling nothing changes: the full set draws and no footnote appears.
700
+
701
+ The four view packages take a **minor**, not a patch: a result set above the
702
+ ceiling is no longer drawn in full, which is a behaviour break whatever the
703
+ fixed group does to the released version number.
704
+
705
+ **The ceiling is a platform constant, not an authorable key** — `2000`, exported
706
+ from `@object-ui/react` as `NON_GRID_ROW_CEILING`. An authored `limit` or
707
+ `dataSource: { limit }` still does not reach these queries, by the same ruling;
708
+ three alternatives were rejected with it (a documentation note only — still the
709
+ whole table; truncating at `pageSize` — silent, and a complete schedule capped
710
+ at one page; an authorable `maxRows` — a new permanent key every author sets).
711
+
712
+ **Why 2,000.** One constant for all four, so the binding view sets it. Measured
713
+ in this repo's jsdom lane: gantt, calendar and map hold their DOM flat as rows
714
+ grow (virtualised task list; four events per day cell; auto-clustering above
715
+ 100 markers), while `ObjectTree` flattens every expanded node into the document
716
+ at a linear **5.2 DOM elements per record** with no virtualisation. 2,000 rows
717
+ is where the worst of the four lands at ~10,400 elements — an order of
718
+ magnitude above Lighthouse's "excessive DOM size" warning, and still ~10x the
719
+ real application result set this card came from.
720
+
721
+ New exports on `@object-ui/react`: `NON_GRID_ROW_CEILING`,
722
+ `NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote`
723
+ and the type `NonGridCeilingResult`. Two new `common.*` i18n keys carry the
724
+ footnote copy in all ten packs.
725
+ - cfc9b6d: A filter on a record page can now be scoped to the record the page shows (objectui#7297). Write `{record_id}` as a filter value, for example `{ "assignee": "{record_id}" }` on an `element:number` counting tasks, and on a `type: 'record'` page it resolves to the id of the record in view: on one person's page the number is that person's count, and on the next person's page it is theirs. It works in a component's own `filter` and in its component-level `dataSource.filter`, on every data component that resolves its filter through `@object-ui/react`'s `useFilterScope()`. The token is declared by `@objectstack/spec` 17.5.0 (`RECORD_CONTEXT_TOKENS`, objectstack-ai/objectstack#20003).
726
+
727
+ The id comes from the record page's mounted record context, never from a URL parameter or a page variable. `@object-ui/react`'s `useFilterScope()` adds it to the scope it returns (as the new optional `recordId` member of `@object-ui/core`'s `FilterTokenScope`) whenever a `RecordContextProvider` is mounted above the component; `FilterScopeProvider` is unchanged and still carries only the session values. The held filters in `useResolvedFilter`, `object-grid` and `object-view` resolve again when the record changes, so moving to another record queries again without a remount.
728
+
729
+ Anywhere with no record in context (a list view, a dashboard, a report, or a page that is not a record page) `{record_id}` is refused by name: `resolveContextTokens` reports it through `onUnresolved` (a console warning by default), the way an unresolved `{current_user_id}` is reported, and leaves it as written. It never becomes `null` and its condition is never dropped, so a number can never silently count every record; the ObjectStack server refuses the leftover token by name (`FILTER_TOKEN_UNRESOLVED`). Like the session tokens, `{record_id}` only scopes what a component shows. It is not access control: which rows a user may read is still decided by the server's row-level security.
730
+
731
+ **Correction, 2026-09-30 (objectui#8945).** The example in the first paragraph, `{ "assignee": "{record_id}" }`, is the retired MongoDB-style record form. `@objectstack/spec` 17.5.0 refuses it at an `element:number` `filter`, because that key takes an array and a record is refused by kind. Write the same condition as a `ViewFilterRule` array, which the spec accepts: `[{ "field": "assignee", "operator": "equals", "value": "{record_id}" }]`. A component-level `dataSource.filter` takes the same array. The `{record_id}` token itself, and the rest of this entry, are unaffected.
732
+ - 71a4a53: **BREAKING: `useClientNotifications` is removed from `@object-ui/react`**
733
+
734
+ What left: the `useClientNotifications` hook and its two types,
735
+ `UseClientNotificationsOptions` and `UseClientNotificationsResult`. Code that
736
+ imports any of the three from `@object-ui/react` no longer compiles.
737
+
738
+ Migration: use the shell's shared inbox feed. `@object-ui/app-shell` reads the
739
+ ADR-0030 inbox rows (`sys_inbox_message`, with read-state in
740
+ `sys_notification_receipt`) through one shared feed, and each of its inbox
741
+ surfaces reads that feed: the header bell in `AppHeader`, and a
742
+ `global:notifications` node placed on a page. Mount one of those instead of
743
+ polling `client.notifications.*` yourself. The "Server-side notifications"
744
+ section of the notifications guide describes both.
745
+
746
+ Why: the hook ran a poller of its own (a `setInterval` over
747
+ `client.notifications.list`) and copied the rows into `NotificationProvider`,
748
+ beside the shell's feed rather than through it. Mounted next to the console
749
+ shell, it would have read the same rows on a second schedule and kept a second
750
+ copy of their read-state: the duplicate-scheduler shape that objectui#4197,
751
+ objectui#4225 and objectui#4316 removed. It had no consumers. Before the
752
+ removal, a census found no call site in this repository (objectui `e6203d756`)
753
+ beyond its own file, the package barrel, the notifications guide and CHANGELOG
754
+ entries. It also found none in the sibling `objectstack` (`338feda6`) and
755
+ `hotcrm` (`087b7c5`) repositories. Importers outside those repositories are
756
+ NOT MEASURED. Under the ruling on objectui#7389, a published capability with
757
+ zero consumers retires immediately, with no deprecation window.
758
+
759
+ Unchanged: `NotificationProvider`, `useNotifications` and the rest of the
760
+ notification system in `@object-ui/react`.
761
+
762
+ Marked `minor`, not `major`, per the version-alignment convention in
763
+ AGENTS.md; the break is real and is stated here.
764
+ - 1237ae4: The non-grid row ceiling's mechanism moves to `@object-ui/core`, its probe row
765
+ lives in one query helper, and its footnote takes the result and nothing else
766
+ (objectui#7508, maintainer ruling A′ — follow-up to objectui#7210).
767
+
768
+ **`@object-ui/core` — new exports**, beside the `extractRecords` they wrap:
769
+
770
+ - `NON_GRID_ROW_CEILING` (moved from `@object-ui/react`, value unchanged);
771
+ - `applyNonGridRowCeiling` and the type `NonGridCeilingResult` (moved from
772
+ `@object-ui/react`, behaviour unchanged);
773
+ - `nonGridRowCeilingQuery()` (new) — returns `{ $top }`, the ceiling plus ONE
774
+ probe row. It is the only place that `+ 1` is written; spread it into the
775
+ `find()` query.
776
+
777
+ **`@object-ui/react`**:
778
+
779
+ - still exports `NON_GRID_ROW_CEILING`, `applyNonGridRowCeiling` and the type
780
+ `NonGridCeilingResult`, now re-exported from `@object-ui/core` — the same
781
+ bindings, so no import of these three changes;
782
+ - **removed**: `NON_GRID_ROW_CEILING_TOP`. Replace `$top: NON_GRID_ROW_CEILING_TOP`
783
+ with `...nonGridRowCeilingQuery()` from `@object-ui/core`. This supersedes the
784
+ export list in objectui#7210's entry, which names it;
785
+ - **breaking prop change**: `NonGridRowCeilingNote` takes `result`
786
+ (a `NonGridCeilingResult`) and no other prop. `drawn`, `total`, `truncated`
787
+ and `className` are gone, with no deprecated path. Pass the object
788
+ `applyNonGridRowCeiling` returned: the note prints `result.rows.length` and
789
+ `result.total`, so a footnote whose numbers disagree with the rows drawn can
790
+ no longer be written. It now carries `shrink-0` itself, which the gantt used
791
+ to pass in through `className`.
792
+
793
+ `ObjectCalendar`, `ObjectGantt`, `ObjectMap` and `ObjectTree` take the helpers
794
+ from `@object-ui/core` and hand the note their result; what they fetch, draw and
795
+ print is unchanged.
796
+ - b041b9c: `@object-ui/core` publishes `resolveRecordSourceObjectName`, the ONE reader for "which
797
+ object is this block bound to".
798
+
799
+ Six view plugins each spelled that resolution locally — `ObjectCalendar` twice,
800
+ `ObjectGantt`, `ObjectTree` twice, `ObjectMap`, `ObjectGrid` — and had drifted: three
801
+ wrote `?? schema.objectName`, one `|| ''`, one `: undefined`, one an `'object' in
802
+ dataConfig` test. They now delegate to one function that states the published
803
+ record-source ladder `77cb489b4` declared (`data`, then `staticData`, then `objectName`) once.
804
+
805
+ **No behaviour changes.** Each site's pre-collapse expression is transcribed verbatim
806
+ into `record-source.behaviourNeutrality-7627.test.ts` and asserted equal to its
807
+ post-collapse spelling across the whole contract-valid input matrix — both bindings
808
+ present, data only, `objectName` only, empty `objectName`, empty `data.object`, the
809
+ `api` / `value` / `staticData` / array-shorthand providers, and nothing bound.
810
+
811
+ **Two questions stay two questions.** `normalizeListViewSchema`'s gap-fill (#7477,
812
+ ruling B, landed as `00d2fa682`) is untouched and is NOT re-pointed at the new reader: it answers
813
+ how `objectName` gets POPULATED when absent, where an already-present `objectName` wins.
814
+ The new reader answers which object a block RESOLVES, where the `data` block wins — the
815
+ order declared on both published faces in `@object-ui/types` and pinned by
816
+ `objectql-record-source-refinement-6939.test.ts`. Merging them would silently override
817
+ one standing ruling or the other.
818
+
819
+ **`ObjectGantt`'s `persistLayoutKey` is deliberately excluded** and keeps its inverted
820
+ order, with an in-place comment saying why: its receiver is a localStorage key
821
+ (`gantt-layout:KEY:filters`), not a record source, so re-pointing it would orphan every
822
+ saved layout and filter-chip set of a view carrying both bindings. Two more sites the
823
+ finding listed are not object-name readers at all and were struck: `ObjectGantt`'s
824
+ refresh-handler predicate (`object` OR `api`) and `plugin-dashboard`'s `isObjectProvider`
825
+ type-guard over a widget's `data`.
826
+
827
+ `useSettledSchema`'s doc comment stops prescribing the hand-written ladder at all four
828
+ lines that taught it, so the copies cannot re-seed from the hook that replaced them.
829
+ - 95bf128: feat(react): an action's `params` values are templates, evaluated where `properties` are
830
+
831
+ Every string leaf of a node's `params` bag - the node-level `params` and
832
+ `properties.params` alike (and the legacy `props.params`, which follows its
833
+ canonical bag), at any depth, inside nested objects and arrays - is now
834
+ template-evaluated by `SchemaRenderer`, with the same evaluator and the same
835
+ scope that already evaluates `properties`. A metadata-authored button on a
836
+ record page can therefore name the record it sits on:
837
+
838
+ ```json
839
+ { "type": "action:button", "label": "Edit", "actionType": "navigate_edit",
840
+ "params": { "objectName": "account", "recordId": "${record.id}" } }
841
+ ```
842
+
843
+ Before this, both spellings of `params.recordId` reached the action handler as
844
+ the raw `${record.id}` text: the node-level bag was never evaluated, and
845
+ `properties.params` was one value of a shallow loop, so the template inside it
846
+ was never visited.
847
+
848
+ Behaviour change, stated plainly: a `params` string containing `${...}` is now
849
+ evaluated at render time instead of being passed through verbatim. It reaches
850
+ only values whose templates never worked - no authored `params` bag in this
851
+ repository carries one. A template that still cannot be evaluated (an unbound
852
+ root such as `${nope.id}`) keeps its source text exactly as before, and the
853
+ development-build unevaluated-expression diagnostic now reports it by its path
854
+ (`params.recordId`, `properties.params.target.id`), so a wrong template stays
855
+ loud.
856
+
857
+ What is not walked: an ARRAY `params` (the `ActionParam[]` definition list the
858
+ params dialog renders) is left as authored; keys are never evaluated, only
859
+ values; non-plain objects (`Date`, `Map`, class instances) and functions pass
860
+ through by identity; a cycle in a host-built bag ends the walk. Every other
861
+ nested config value keeps the shallow reading it had.
862
+
863
+ ⚠️ **Dated note, 2026-10-01 — props written flat on the node — objectui#10872 batch 10.** Later in this same release a flat `actionType` on an `action:button` node is refused by name, by `objectui validate` and the strict authoring face, with a message naming `properties.actionType`. The node-level `params` object the example above shows is refused the same way, toward `properties.params`, where objectui#10289's entry moved its values: the runtime no longer reads a node-level `params` object as values. So the example is kept as the reading of this change, and the node is written `{ "type": "action:button", "properties": { "label": "Edit", "actionType": "navigate_edit", "params": { "objectName": "account", "recordId": "${record.id}" } } }` in this release. The template evaluation this entry describes applies to `properties.params` unchanged. The rest of this entry is kept as the reading of this change.
864
+ - edea22a: **BREAKING** — `SchemaRendererProvider`'s `dataSource` prop, and the context
865
+ type every `useSchemaContext()` consumer reads back, are the published
866
+ `DataSource` contract instead of `any`.
867
+
868
+ **FROM** `dataSource={anything}` **TO** `dataSource={adapter}` — a `DataSource`
869
+ from `@object-ui/types`, or `null` / `undefined` when the host has no adapter
870
+ bound.
871
+
872
+ ```ts
873
+ // before — compiled, and failed at runtime on the first find()
874
+ <SchemaRendererProvider dataSource={'not-an-adapter'}>
875
+ // before — compiled, and no reader can do anything with it
876
+ <SchemaRendererProvider dataSource={{}}>
877
+ // after
878
+ <SchemaRendererProvider dataSource={adapter}> // a DataSource
879
+ <SchemaRendererProvider dataSource={undefined}> // "I have no adapter"
880
+ ```
881
+
882
+ Both sites are typed `DataSource | null | undefined` — the spelling
883
+ `useSettledSchema` in this same package already used. The two absences are part
884
+ of the contract, not a weakening of it: a Studio preview, a `kind:'react'` page
885
+ rendered before the host's adapter connects, and a widget probe driving
886
+ `apiFetch` alone all render with nothing bound, and every reader in the tree
887
+ already guards for it. What the union refuses is everything that is not an
888
+ adapter: a string, an empty object, a plain data bag, a partial adapter missing
889
+ a required member.
890
+
891
+ The measured cost, both halves, because the two `any`s have different blast
892
+ radii (measured separately on `origin/main`, whole-repo type-check over the 33
893
+ packages that depend on `@object-ui/react`):
894
+
895
+ - the **context type** — the `any` that reaches every `useSchemaContext()`
896
+ reader — reds **7 diagnostics at 7 sites in 4 packages**, all of them
897
+ production code or a mocked module factory.
898
+ - the **provider prop** — the injection points — reds **52 diagnostics at 27
899
+ sites in 11 packages**, all but one of them test doubles.
900
+
901
+ That ordering is the reverse of the prediction on the card: the context `any`
902
+ was expected to be the expensive one because it infects the whole tree, and it
903
+ is the cheap one, because every reader in the tree already guarded and none of
904
+ them ever reached past `find` / `getObjectSchema`. The prop is the expensive
905
+ one, because the injection points are overwhelmingly test doubles that were
906
+ never complete adapters. The full accounting is on objectui#7912.
907
+
908
+ Two runtime behaviours change, both in the "no adapter" direction and both
909
+ strictly closer to what the surrounding code already intended:
910
+
911
+ - `@object-ui/components`' `kind:'react'` page passed an empty object as its
912
+ "no adapter yet" stand-in. An empty object is TRUTHY, so it walked past every
913
+ `if (!dataSource)` guard written to catch exactly that state and failed later,
914
+ at the call. It now passes the absent adapter itself, so the guard fires where
915
+ it was meant to. The module-constant identity that stand-in existed for is
916
+ preserved: `null` is a primitive, so the provider's memo is unaffected.
917
+ - `@object-ui/plugin-calendar`, `@object-ui/plugin-gantt` and
918
+ `@object-ui/plugin-kanban` collapse a `null` adapter from the context to
919
+ `undefined` before handing it to their widget, whose prop declares the single
920
+ spelling `dataSource?: DataSource`.
921
+
922
+ Nothing else moves at runtime: no value flowing through this key changes, and
923
+ no data path is touched. A TypeScript consumer outside this repo that handed
924
+ this prop something other than an adapter now gets a compile error naming the
925
+ key (TS2322 / TS2739 / TS2740), which is why the FROM/TO is spelled out above.
926
+
927
+ Five `as any` reads of this context in `@object-ui/fields` are gone — they were
928
+ redundant the moment the seam became honest — and `LookupField`'s local
929
+ re-declaration of the imported context as a `Context` of `any`, which laundered
930
+ its `dataSource` read while looking typed, is gone with them. Both directions of
931
+ the contract are pinned against the real compiler in
932
+ `SchemaRendererContext.dataSourceType.pin.test.ts`, and the card's planted
933
+ documentation probe (a bare string in `packages/react/README.md`'s provider
934
+ example) now fails `pnpm check:doc-snippets`, where it used to exit 0 with zero
935
+ diagnostics.
936
+
937
+ objectui#7912.
938
+ - 0ea7054: Remove 37 runtime dependencies that no file in the declaring package consumes, and gate
939
+ the direction so the next one cannot land (objectui#8198).
940
+
941
+ `check:phantom-deps` judges imports that are not declared; nothing judged the reverse,
942
+ so a declaration could outlive its last consumer indefinitely. That is what happened to
943
+ `recharts` in `@object-ui/components` after objectui#7397 deleted its only importer — it
944
+ was removed by hand on objectui#7625, and nothing would have reported the next one. The
945
+ new `pnpm check:unused-deps` asks the reverse question over `dependencies` and
946
+ `optionalDependencies` of every released package.
947
+
948
+ **Potentially breaking, for consumers relying on hoisting.** Nothing these packages ship
949
+ changes: their Vite `external` predicates are path-based and never read `dependencies`,
950
+ so no built artifact moves. What changes is the install graph — a project that imports
951
+ one of the removed packages while depending only on the ObjectUI package that used to
952
+ drag it in will no longer resolve it. Declare it directly; that is the correct
953
+ dependency edge in either case. The removals, by package:
954
+
955
+ - `@object-ui/plugin-designer`: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`, `@object-ui/fields`
956
+ - `@object-ui/plugin-chatbot`: `react-markdown`, `react-syntax-highlighter`, `remark-gfm` (and the orphaned `@types/react-syntax-highlighter`)
957
+ - `@object-ui/plugin-report`: `@object-ui/plugin-grid`, `clsx`, `react-i18next`, `tailwind-merge`
958
+ - `@object-ui/plugin-map`: `@objectstack/spec`, `lucide-react`, `zod`
959
+ - `@object-ui/runner`: `class-variance-authority`, `clsx`, `tailwind-merge`
960
+ - `@object-ui/core`: `lodash`, `zod`
961
+ - `@object-ui/layout`: `clsx`, `tailwind-merge`, and `react-dom` — which it pinned at an exact version in `dependencies` while also declaring it as a peer range, i.e. a library hard-depending on the renderer it asks its host to supply
962
+ - `@object-ui/plugin-dashboard`: `clsx`, `tailwind-merge`, and the same `react-dom` defect
963
+ - `@object-ui/plugin-ai`: `@object-ui/react`, `clsx`, `tailwind-merge`
964
+ - `@object-ui/fields`: `clsx`, `tailwind-merge`
965
+ - `@object-ui/console`: `@object-ui/react-runtime`, `sucrase`
966
+ - `@object-ui/auth`: `@object-ui/types`
967
+ - `@object-ui/plugin-calendar`: `@object-ui/fields`
968
+ - `@object-ui/plugin-editor`, `@object-ui/plugin-markdown`: `@object-ui/react`
969
+ - `@object-ui/react`: `react-hook-form`
970
+
971
+ Every one was verified by a whole-package grep before removal — the name appeared nowhere
972
+ under the package but its own manifest and CHANGELOG — and the whole workspace builds,
973
+ type-checks and tests green afterwards.
974
+ - 4eaa835: `BaseSchema.testId` is now emitted as `data-testid` (objectui#8268, ADR-0049 enforce arm).
975
+
976
+ The declaration promised "Rendered as data-testid attribute" and nothing rendered it. `testId`
977
+ was absent from `SchemaRenderer`'s metadata destructure, so it fell through into the spread and
978
+ reached the DOM under React's fallback spelling for an unrecognised prop — a non-standard
979
+ lowercase `testid` that `screen.getByTestId(...)` and `[data-testid=...]` both miss. The
980
+ validator accepted the key, so an author who followed the documented row got a failing test with
981
+ no diagnostic near the cause.
982
+
983
+ Measured on `93127bd6f` through the real `ui:scroll-area` renderer, node
984
+ `{ id: 'my-scroll', testId: 'my-tid' }`:
985
+
986
+ ```
987
+ before id="my-scroll" testid="my-tid" data-obj-id="my-scroll"
988
+ [data-testid="my-tid"] false · [testid="my-tid"] true
989
+ after id="my-scroll" data-testid="my-tid" data-obj-id="my-scroll"
990
+ [data-testid="my-tid"] true · [testid="my-tid"] false
991
+ ```
992
+
993
+ `id` reaching the DOM as `id` + `data-obj-id` is the contrast leg, green on both runs — the
994
+ strip list and the spread were working as designed; only this key had no mapping.
995
+
996
+ The other direction — retiring the promise — was considered and declined. It is what
997
+ `c1fe272ad` did for `BaseSchema.hidden`, but that key had a working behaviour to describe and
998
+ zero named consumers, and the ruling's decline turned on exactly that. This promise already has
999
+ carriers outside the type declaration: `content/docs/api/schema-reference.md` states it as a
1000
+ table row and authors `testId` in that page's own base-schema example, `@object-ui/cli`'s
1001
+ `OBJECTUI_STRUCTURAL_KEYS` identifies a file as an ObjectUI schema node by this key,
1002
+ at this change `ObjectGridSlotKey` / `ObjectFormSlotKey` pin it, `SchemaBuilder.testId()` writes
1003
+ it, and ADR-0054 C4 — shipped — reads "the renderer emits `data-testid` … derived from metadata".
1004
+
1005
+ ⚠️ **Dated note, 2026-09-29 — `ObjectGridSlotKey` withholds `testId` — objectui#10976.** Later
1006
+ in this same release `ObjectGridSlotKey` stops carrying `testId`: `ObjectView` draws its grid as
1007
+ a component, not as a schema node, so nothing emits the attribute there, and `table.testId` is
1008
+ refused. `ObjectFormSlotKey` still carries it. The rest of this entry is kept as the reading of
1009
+ this change; the objectui#10976 entry states what ships.
1010
+
1011
+ `minor`, not `patch`: nodes that author `testId` change what they emit in both directions, so a
1012
+ selector written against the accidental `[testid=…]` stops matching.
1013
+
1014
+ Nodes that author no `testId` are byte-identical to before. The attribute is spread
1015
+ conditionally rather than passed as `'data-testid': undefined`, because the unconditional form
1016
+ puts the key in every node's props bag and a component that sets its own `data-testid` before
1017
+ spreading what it is handed loses its locator — measured, 9 tests red across 5 files, three of
1018
+ them the byte-for-byte props-bag pins (objectui#6708 / #6752 / #6760).
1019
+
1020
+ `testId` is stripped from the spread but kept in objectui#4795's unevaluated-expression scan
1021
+ set: the strip list is that diagnostic's exclusion list because every other member holds raw
1022
+ predicate source, and this key holds a literal.
1023
+ - 152f0a7: `toRenderableSchema` is behaviour-preserving for the falsy primitives too, so `empty`
1024
+ stops printing a stray `"0"` (objectui#8908).
1025
+
1026
+ `toRenderableSchema` is the repo's permanent bridge from `@object-ui/types`' `SchemaNode`
1027
+ union onto `SchemaRendererProps['schema']`, which deliberately declares no `number` /
1028
+ `boolean` (objectui#4548 ruling Q2). Its docblock promised — and its callers were entitled
1029
+ to rely on — that "the value a caller forwards renders identically whether or not it
1030
+ passes through here". That was false for exactly two of the six `SchemaNode` members.
1031
+
1032
+ The bridge mapped every `number` / `boolean` onto `String(node)`. `String(0)` and
1033
+ `String(false)` are the NON-EMPTY strings `'0'` and `'false'`, which are truthy, so they
1034
+ sailed past `SchemaRenderer`'s own `!evaluatedSchema` leg and rendered as their own text —
1035
+ while the same two values handed to `SchemaRenderer` directly render nothing, pinned since
1036
+ objectui#4548. The mapping is now two-legged and mirrors the renderer's own narrowing
1037
+ order: a truthy `number` / `boolean` still becomes its text form, and a falsy one
1038
+ (`0`, `-0`, `NaN`, `false`) becomes nothing.
1039
+
1040
+ **Behaviour change on a published surface, deliberately.** The reachable, shipped instance
1041
+ is `empty`: its `action` slot gates on nullish rather than truthiness (objectui#7105, so a
1042
+ bare string node renders instead of being dropped), so `0` and `false` went through the
1043
+ bridge. `{ type: 'empty', title: 'T', action: 0 }` rendered `"T0"` and now renders `"T"`,
1044
+ which is what the same node handed straight to `SchemaRenderer` has always rendered.
1045
+ Every other slot routed through the bridge — page regions, header-bar actions, detail tabs
1046
+ and footers, report sections, view configs — gets the same correction for the same two
1047
+ values.
1048
+
1049
+ **No type change, and that is not an accident.** The return type is unchanged:
1050
+ `BaseSchema | string | null | undefined`. Letting `0` / `false` through UNCHANGED would
1051
+ have widened it to admit `number` / `boolean`, which `SchemaRenderer`'s prop refuses by
1052
+ ruling — so it would have been a TS2322 at every call site that forwards the result
1053
+ straight into `<SchemaRenderer schema={…} />`, which is all nineteen of them. The falsy
1054
+ leg returns nothing instead, which keeps the guarantee and the type both. The exact
1055
+ equality of the two is pinned in
1056
+ `packages/react/src/__tests__/SchemaRenderer.propsResolution.test.ts`.
1057
+
1058
+ **Migration.** Nothing has to change, and nothing needs a code edit. A slot that authored
1059
+ `0` or `false` was painting a literal `"0"` / `"false"` where the platform's answer for
1060
+ those values is nothing; it now renders nothing. If some surface was relying on that stray
1061
+ text — authoring `action: 0` to make the digit appear — author the string `'0'` instead,
1062
+ which is a `SchemaNode` and renders as its own text on both paths.
1063
+
1064
+ **Supersedes one sentence in the objectui#8331 entry.** That note said the `data-table`
1065
+ `emptyAction` truthiness leg is kept "precisely so those two never reach the bridge, whose
1066
+ `String` mapping would otherwise turn them into the text `"0"` and `"false"`". The bridge
1067
+ no longer does that. The leg is kept anyway, and objectui#8908 kept it deliberately rather
1068
+ than removing it as newly redundant: it is what makes that slot's answer independent of
1069
+ the bridge, and it is why `emptyAction` was never affected by this defect in the first
1070
+ place.
1071
+
1072
+ ⚠️ **Dated note, 2026-10-01 — the bridge's return type widens with the prop — objectui#11364.** Later in this same release `SchemaRendererProps.schema` becomes `BaseSchema | AuthoringNode | string | null | undefined`. `toRenderableSchema` returns `SchemaRendererProps['schema']` by reference, so its return type widens with it. So "The return type is unchanged: `BaseSchema | string | null | undefined`" no longer describes the release as a whole: the falsy leg still adds no `number` / `boolean`, and the pinned equality with the prop still holds. The rest of this entry is kept as the reading of this change.
1073
+
1074
+ ⚠️ **Dated note, 2026-10-02 — the prop takes the declared-node union — objectui#11466.** At this change, and at objectui#11364's note above, the bridge's return type spelled its object member `BaseSchema` (with `AuthoringNode` beside it after objectui#11364); now, later in this same release, `SchemaRendererProps.schema` is `DeclaredNode | string | null | undefined`, and the return follows it by reference. The falsy leg still adds no `number` / `boolean`, and the pinned equality with the prop still holds. The rest of this entry is kept as the reading of this change.
1075
+ - 689953a: Keep the `{ dialect: 'cel', source }` predicate envelope intact through a node's
1076
+ config bag, so a CEL visibility gate authored the canonical way reaches the CEL
1077
+ engine instead of the legacy JS one (objectui#9100).
1078
+
1079
+ **The defect.** `SchemaRenderer`'s per-value `properties` / `props` evaluation loops
1080
+ handed every bag value to `ExpressionEvaluator.evaluate`, whose first action is to
1081
+ unwrap any `{ source: string }` object down to its bare `source` — the unwrap that
1082
+ makes a `template` envelope interpolate. Applied to a `cel` predicate it was
1083
+ destructive: the envelope is the only thing that routes `evaluateCondition` to the
1084
+ canonical `@objectstack/formula` engine, and those loops run before the hoist, so
1085
+ both consumers — `SchemaRenderer`'s own visibility chain and the renderer's
1086
+ `toPredicateInput` → `useCondition` call one layer down — received a bare string and
1087
+ took the legacy JS path, where a CEL stdlib call such as `has()` is not a function.
1088
+
1089
+ Both polarities then failed silently, in opposite directions. On the four SHOW legs
1090
+ (`visibleWhen` / `visible` / `visibleOn` / `visibility`) the fail-soft `true` is
1091
+ negated, so **a gate authored to hide something rendered it on every row** — measured
1092
+ in a real browser, where a duplicate-lead banner showed on 6 of 6 records whose field
1093
+ was `null`. On the two HIDE legs (`hidden` / `hiddenOn`) the same `true` is returned
1094
+ un-negated, so **the node vanished entirely**. Neither is distinguishable on screen
1095
+ from a predicate that said so on purpose.
1096
+
1097
+ **Behaviour change, deliberately.** A `cel`-envelope predicate on any of those six
1098
+ keys, authored inside `properties` or `props`, now actually decides. Metadata that
1099
+ was relying — knowingly or not — on such a gate never biting will start seeing nodes
1100
+ hidden (SHOW legs) or shown (HIDE legs) as authored. Nothing else moves: a `template`
1101
+ or dialect-less envelope keeps the legacy `${…}` behaviour byte-for-byte, a
1102
+ non-predicate bag key keeps the flattening it has always had, and node-level
1103
+ predicate keys were never affected (they do not pass through these loops, which is
1104
+ why objectui#7530's pin stayed green throughout).
1105
+
1106
+ **`record:alert` declaration.** Its `visible` input was registered as a bare
1107
+ `'string'` while `@objectstack/spec` 17.4.0 declares
1108
+ `boolean | string | { dialect, source }` for the same key and the renderer resolves
1109
+ all three. It is now declared `['boolean', 'string', 'object']`, in the same change
1110
+ that made the envelope arm actually reachable.
1111
+ - 8d3dbb2: fix(react): keep the CEL predicate envelope intact on the ENABLEMENT keys too
1112
+
1113
+ `SchemaRenderer`'s two per-value config-bag loops (`properties` — the spec
1114
+ spelling — and its legacy `props` alias) flattened a `{ dialect: 'cel', source }`
1115
+ predicate envelope on `disabled` / `disabledOn` / `enabled` down to its bare
1116
+ `source` string, exactly as objectui#9100 measured for the visibility chain. The
1117
+ envelope is the only thing that routes `evaluateCondition` to the canonical
1118
+ `@objectstack/formula` engine, so the flattened predicate took the legacy JS
1119
+ path, where a CEL stdlib call such as `has()` is not a function.
1120
+
1121
+ objectui#9104 repaired this for the six visibility legs only. The enablement
1122
+ keys are not visibility legs — they route through the node's enablement gate and
1123
+ through the action renderers' own predicate call — so they were left flattened,
1124
+ and the resulting fail-soft `true` landed in OPPOSITE directions on the two
1125
+ halves of the chain:
1126
+
1127
+ - `disabled` / `disabledOn` are not negated, so a CEL-authored gate greyed its
1128
+ control out on every row and no predicate the author could write re-enabled
1129
+ it — measured through `action:button` and `action:icon`;
1130
+ - the legacy `enabled` alias IS negated by those renderers, so the same `true`
1131
+ arrived as "not disabled" and a control the author had disabled stayed
1132
+ pressable.
1133
+
1134
+ The guard now consults the union of the file's two closed predicate-chain
1135
+ declarations instead of the visibility one alone. `template` and dialect-less
1136
+ envelopes, non-predicate bag keys and predicates nested inside arrays keep their
1137
+ behaviour byte for byte.
1138
+ - 345e24a: **BREAKING** — `RecordContextValue.dataSource` is the DataSource **adapter** it
1139
+ has always held, not a datasource id string.
1140
+
1141
+ **FROM** `dataSource: 'ds_primary'` **TO** `dataSource: myAdapter` (any
1142
+ `DataSource` from `@object-ui/types`).
1143
+
1144
+ ```ts
1145
+ // before — compiled, but no producer ever did this and no reader could use it
1146
+ const ctx: RecordContextValue = { objectName: 'account', recordId: 'r1', dataSource: 'ds_primary' };
1147
+ // after
1148
+ const ctx: RecordContextValue = { objectName: 'account', recordId: 'r1', dataSource: myAdapter };
1149
+ ```
1150
+
1151
+ The member was declared `dataSource?: string` — "an optional datasource id;
1152
+ mirrors the page-level datasource override" — while the one production host
1153
+ that writes it (`@object-ui/app-shell`'s `RecordDetailView`) forwards an adapter
1154
+ OBJECT and every reader calls adapter methods on it. Measured on the base tree:
1155
+ of the provider sites in this repository that write the member, exactly one is
1156
+ non-test and it passes the adapter; the only in-repo writer of a string was a
1157
+ test fixture, which existed because the declaration invited it. So the id was
1158
+ never a contract anyone kept — it was a statement the code contradicted at every
1159
+ site, and the override it described has zero producers. Contract-first
1160
+ (Commandment #0.1): the declaration moves to what the code holds, and no second
1161
+ `dataSourceId` member is minted for a requirement nobody implements.
1162
+
1163
+ What it bought: each reader paid for the wrong declaration with a cast at the
1164
+ point of use, and a cast deletes the compiler's answer to "does this adapter
1165
+ have the method I am about to call". All 11 record-context read sites are now
1166
+ cast-free, across `@object-ui/app-shell`, `@object-ui/components` and
1167
+ `@object-ui/plugin-detail`. The worked instance is objectui#8883's reference
1168
+ rail: it reaches `dataSource.getObjectSchema` — a REQUIRED member of
1169
+ `DataSource` — and now gets that guarantee from the compiler instead of from
1170
+ `(ctx as any)`.
1171
+
1172
+ ⚠️ Nothing to migrate at runtime and no data change: every producer already
1173
+ passed the adapter, so no value moving through this member changes. A
1174
+ TypeScript consumer outside this repo that read the member as `string` is not
1175
+ observable from here and gets a compile error (TS2322) naming the key — which
1176
+ is why the FROM/TO is spelled out above. Re-widening to `string | DataSource`
1177
+ would not be a kindness: a union member without `getObjectSchema` cannot be read
1178
+ without narrowing, so it re-breaks every reader this change repaired. Both
1179
+ directions are pinned against the real compiler in
1180
+ `RecordContext.dataSourceType.pin.test.ts`.
1181
+
1182
+ ⚠️ `SchemaRendererContextValue.dataSource` one context over is a **different**
1183
+ declaration (`any`) and is **not** touched here. Five `as any` reads in
1184
+ `@object-ui/fields` key off that context, not this one; they are unaffected by
1185
+ this change and are reported separately.
1186
+
1187
+ objectui#9197.
1188
+ - 8524372: Unbind the data-source adapter from the expression scope, and point `bind` at the scope
1189
+ channel (objectui#9308, maintainer ruling 2026-09-13, option B).
1190
+
1191
+ **Breaking, deliberately — and `minor` only because this repo's `fixed` group of 40
1192
+ packages may not carry a `major` (AGENTS.md 版本号策略).** Read the migration note below
1193
+ before upgrading if any of your metadata reads `data.*` at the page/component tier.
1194
+
1195
+ **What changed.**
1196
+
1197
+ 1. `SchemaRenderer` no longer writes `data: dataSource` into the evaluator scope. `data`
1198
+ is not a root this tier binds. The roots it supplies are `record` (the row, when a
1199
+ record surface bound one), `page` (page-local variables) and `current_user` — plus
1200
+ every name the host published through `PredicateScopeProvider`.
1201
+ 2. `useDataScope(path)` — what a node's `bind` resolves through — reads the ambient
1202
+ predicate scope (`usePredicateScope()`) instead of walking the injected adapter.
1203
+
1204
+ **Who this breaks, concretely.**
1205
+
1206
+ * **A `${data.…}` expression on a page/component node.** It used to resolve against the
1207
+ host's injected `DataSource` adapter. An adapter answers no `data.*` path, so for every
1208
+ host that injected a real adapter the read was already `undefined` and the predicate was
1209
+ a silent constant — but the constant MOVES: `data` used to be a present key holding an
1210
+ object, so `data.status == 'draft'` evaluated cleanly to `false` and the node was hidden
1211
+ on every row. `data` is now ABSENT, the expression cannot be evaluated at all, and this
1212
+ surface is fail-soft, so the same gate now answers `true` and the node is SHOWN on every
1213
+ row. It is no longer silent: the objectui#5454 reporter names it in the console, in
1214
+ production as well as development. Re-root such a predicate on `record.*` — the row —
1215
+ or publish a `data` of your own through `PredicateScopeProvider`.
1216
+ * **A host that injected a plain data bag as `dataSource`.** That was meaning 2 of the
1217
+ key, and it stops working entirely: `${data.…}` no longer reads it and `bind` no longer
1218
+ walks it. It has been a compile error since objectui#7912 (`dataSource` is typed
1219
+ `DataSource | null | undefined`). Publish those values through `PredicateScopeProvider`
1220
+ instead — the provider, the hook and the app-shell wiring already exist, and this change
1221
+ adds no new published key.
1222
+ * **The nine `bind` readers** (`list`, `tree-view`, `ObjectChart`, `ObjectDataTable`,
1223
+ `ObjectPivotTable`, `ObjectGrid`, `ObjectKanban`, `ObjectGallery`, `ObjectTimeline`) now
1224
+ resolve `bind` against the scope. All nine already carry a fallback
1225
+ (`boundData || schema.items`, `|| schema.nodes || []`, or a fall-through to their own
1226
+ fetch chain), and against a conformant adapter `useDataScope` returned `undefined`
1227
+ before this change — so the fallback is what was running, and a `bind` that resolved
1228
+ nothing before still resolves nothing. What is new is that a `bind` CAN now resolve:
1229
+ a host that publishes rows on its scope will see them used where the fetch used to run.
1230
+
1231
+ **Why.** `@object-ui/app-shell`'s `ExpressionProvider` states the rule this applies: every
1232
+ root bound at a tier must be one the engine accepts AND one that tier can actually answer.
1233
+ objectui#8155 unbound `app` under it and objectui#8166 unbound `data`; ADR-0089 D3 puts
1234
+ `data` at the metadata layer and `record` at the runtime layer, and the engine's
1235
+ per-surface `FIELD_RULE_BOUND_ROOTS` is `['record','previous','parent']`. The renderer tier
1236
+ was the last one still binding a root it could not answer.
1237
+
1238
+ Two side effects worth naming. A host that legitimately published `data` through the
1239
+ documented scope channel used to be silently overwritten by the adapter (the adapter was
1240
+ spread last); it is not any more. And `reportAdapterOnlyDataPredicate` now resolves against
1241
+ the `data` the evaluator actually bound rather than against the adapter — left pointing at
1242
+ the adapter it would have gone silent for precisely the hosts this change breaks.
1243
+
1244
+ The Data Context passages of `content/docs/guide/schema-rendering.md` and
1245
+ `packages/react/README.md` teach the scope channel accordingly.
1246
+ - 72d6587: A record id is a `string` everywhere in the published types, as
1247
+ `@objectstack/spec` has always declared it. Three published declarations that
1248
+ admitted `number` no longer do.
1249
+
1250
+ ⚠️ **BREAKING if your code hands a numeric primary key to any of these three:**
1251
+
1252
+ - **`RecordContextValue.recordId`** (`@object-ui/react`) — the value
1253
+ `useRecordContext()` gives you is now a `string`, never a `number`.
1254
+ - **`DataSource.update`'s `id` parameter** (`@object-ui/types`) — a call that
1255
+ passes a `string | number` is now a type error. Adapters that *implement*
1256
+ `DataSource` are unaffected (see Migration).
1257
+ - **`TransactionOperation.id`** (`@object-ui/core`) — the operation record you
1258
+ hand to `TransactionManager.recordOperation()` must carry a `string` id. If
1259
+ you build that object from a numeric key, convert it where you build it. This
1260
+ type is exported from the package root, so this is a breaking change for
1261
+ `@object-ui/core` consumers in its own right, not just a knock-on.
1262
+
1263
+ Ships as `minor` per the launch-window convention: objectui's
1264
+ `major` is a cross-repo pin to `@objectstack`'s so that "same major means
1265
+ compatible" holds across the two repos
1266
+ (`scripts/check-changeset-no-major.mjs`), and objectui's own breaking changes
1267
+ ship as `minor` with the break named where it lands — this entry is the channel
1268
+ that carries it.
1269
+
1270
+ ## What changed
1271
+
1272
+ - `RecordContextValue.recordId` (`@object-ui/react`) was
1273
+ `string | number | null | undefined`; it is now `string | null | undefined`.
1274
+ - `DataSource.update`'s `id` parameter (`@object-ui/types`) was
1275
+ `string | number`; it is now `string`.
1276
+ - `TransactionOperation.id` (`@object-ui/core`) was `string | number`; it is now
1277
+ `string`. Its sibling `BatchTransactionOperation.id` was already a `string`,
1278
+ so the two operation records finally agree.
1279
+ - `LineItemsPanel` (`@object-ui/plugin-form`) drops the type assertion
1280
+ objectui#9304 left on its parent id. That assertion was the only thing making
1281
+ the context declaration and `buildMasterDetailEditBatch(parentId: string)`
1282
+ meet; the declaration now does it, so the evidence is discharged.
1283
+
1284
+ ## Why the protocol, and not a wider consumer type
1285
+
1286
+ `@objectstack/spec` declares a record id as `z.string()` on every record door —
1287
+ get, update, delete and the batch operation. A consumer type may not be wider
1288
+ than the protocol: a declaration that admits `number` promises callers something
1289
+ the wire never carries, and the promise is kept only by an assertion at the far
1290
+ end, which is what this card was filed about.
1291
+
1292
+ ## Internal consumers repaired at the same time (no public contract moves)
1293
+
1294
+ Narrowing an interface **parameter** never reaches implementors — TypeScript
1295
+ compares method parameters bivariantly, so an adapter that still declares
1296
+ `id: string | number` keeps satisfying `DataSource`. It reaches **callers**. A
1297
+ full local type-check of every workspace type-check program found exactly six,
1298
+ in three packages, and each was red because a further declaration one layer in
1299
+ was itself wider than the protocol. All three are narrowed here, types only, with
1300
+ no runtime change and no coercion added at any call site:
1301
+
1302
+ - `UserPreferenceRecord.id` and the `cachedRowId` it feeds
1303
+ (`@object-ui/data-objectstack`) are `string`. Module-local, not published —
1304
+ these rows are read back off the protocol, so the union was a claim the wire
1305
+ never makes.
1306
+ - `resolveRecordId`'s return type (`@object-ui/plugin-grid`) is
1307
+ `string | undefined`. Module-local, not published — it annotates `any`-typed
1308
+ row data, so the union was an assertion rather than a measurement.
1309
+
1310
+ ## Migration — no `String(...)` at your call sites
1311
+
1312
+ `RecordContextProvider` still **accepts** `string | number | null | undefined`
1313
+ and narrows it once, itself. A host that mounts a record with a numeric primary
1314
+ key therefore changes nothing: the conversion is paid at that injection
1315
+ boundary, typed, in one place. Consumers of `useRecordContext()` read a
1316
+ `string`.
1317
+
1318
+ `DataSource` implementors are unaffected — TypeScript compares method parameters
1319
+ bivariantly, so an adapter that still declares `id: string | number` continues
1320
+ to satisfy the interface. What changes is the **caller** side: a call that passes
1321
+ a `string | number` to `dataSource.update` is now a type error. A backend whose
1322
+ primary keys are numeric maps them at its own adapter boundary rather than
1323
+ pushing the union through every caller.
1324
+
1325
+ For `TransactionOperation`, the same rule applies one level up: build the
1326
+ operation record with a `string` id. If the id arrives from a numeric-keyed
1327
+ backend, convert it in your adapter — the one place that knows the backend's key
1328
+ type — rather than at each `recordOperation()` call. Nothing about this change
1329
+ alters what is sent over the wire; only the declarations moved.
1330
+ - 0ce32d5: `UseNavigationOverlayOptions.onRowClick` now declares the modifier payload it
1331
+ has always been called with (objectui#9357).
1332
+
1333
+ `useNavigationOverlay`'s `handleClick` invokes the caller-supplied `onRowClick`
1334
+ with two arguments — the record, and the optional `HandleClickModifiers`
1335
+ payload (`metaKey` / `ctrlKey` / `button`) a host needs to implement
1336
+ Cmd/Ctrl/middle-click. The option declared only the record, and `handleClick`
1337
+ carried a type assertion that widened the value at the call site so the code
1338
+ would compile. The second argument was therefore invisible on the one line a
1339
+ host reads, and a host that wanted it had to discover it from the
1340
+ implementation and then spell its own second parameter optional to stay
1341
+ assignable.
1342
+
1343
+ The declaration now names both parameters and the assertion is gone.
1344
+
1345
+ **Source-compatible in both directions, with one measured exception.** A
1346
+ one-parameter handler stays assignable to the widened signature (its extra
1347
+ parameter is optional), and a handler written against the widened signature was
1348
+ already assignable to the old one — measured on this change, both directions.
1349
+
1350
+ **The exception, and the one class that has to change.** A handler passed
1351
+ *directly* to `useNavigationOverlay` whose second parameter is annotated
1352
+ *narrower* than `HandleClickModifiers` no longer type-checks. React's
1353
+ `MouseEvent` is the shape this hits in practice, because until now the payload
1354
+ was only discoverable from the implementation, so a host that wanted it wrote
1355
+ the annotation it saw arrive:
1356
+
1357
+ ```ts
1358
+ useNavigationOverlay({
1359
+ objectName: 'account',
1360
+ // was accepted; now TS2322 — `HandleClickModifiers` is not assignable to
1361
+ // `React.MouseEvent`
1362
+ onRowClick: (record, ev?: React.MouseEvent) => { /* ... */ },
1363
+ });
1364
+ ```
1365
+
1366
+ It compiled before only because the old declaration had no second parameter to
1367
+ check the annotation against. The parameter is checked contravariantly, so the
1368
+ annotation now has to *admit* `HandleClickModifiers`. **The fix is one line at
1369
+ the call site:** annotate the parameter `HandleClickModifiers` (exported from
1370
+ `@object-ui/react`), or drop the annotation and let it be inferred. Either way
1371
+ the handler keeps receiving exactly what it received before — this is a
1372
+ type-level change only, with no runtime behaviour attached.
1373
+
1374
+ No caller *inside this repository* is in that class, and the repository's own
1375
+ type-check re-derives that on every run rather than this sentence asserting it;
1376
+ a host that reaches the hook through a view component's `onRowClick` prop is
1377
+ unaffected either way, because the prop's own declared type is what gets
1378
+ assigned to the option. The boundary is pinned as a `@ts-expect-error` row in
1379
+ this package's `useNavigationOverlay.onRowClickArity-9357` test, so it cannot
1380
+ move without a red check.
1381
+
1382
+ What the widening buys everyone else: a caller who wants the modifier payload
1383
+ can now see, from the published type, that it is there.
1384
+
1385
+ Scope note: this repairs the hook's own option. The pass-through props on the
1386
+ view components that feed it — `ObjectKanban`, `ObjectGallery`, the
1387
+ `plugin-kanban` renderer's `onCardClick`, and the `onRowClick` prop on the other
1388
+ view plugins — each still declare one parameter on their own published face;
1389
+ which spelling that family converges on is objectui#9357's open question and is
1390
+ not decided here.
1391
+ - bbba098: **BREAKING if your code hands a numeric primary key to one of these four** — a
1392
+ record id at the `DataSource` boundary is a `string`, as `@objectstack/spec`
1393
+ declares every record door (objectui#9511, director batch #136 item 5 letter B).
1394
+ objectui#9333 narrowed `DataSource.update`; this entry closes three more doors,
1395
+ and narrows one consumer declaration — `UseViewDataResult.fetchOne` — that feeds
1396
+ `findOne`, the one door this entry leaves open.
1397
+
1398
+ ## What changed
1399
+
1400
+ - **`DataSource.delete`'s `id` parameter** (`@object-ui/types`) — was
1401
+ `string | number`, now `string`.
1402
+ - **`DataSource.bulkUpdate`'s `ids`** (`@object-ui/types`) — was
1403
+ `ReadonlyArray<string | number>`, now `ReadonlyArray<string>`.
1404
+ - **`DataSource.bulkDelete`'s `ids`** (`@object-ui/types`) — same narrowing.
1405
+ - **`UseViewDataResult.fetchOne`** (`@object-ui/react`) — was
1406
+ `(id: string | number) => Promise<T | null>`, now `(id: string) =>
1407
+ Promise<T | null>`. This hook hands its argument straight to
1408
+ `DataSource.findOne`, so the union it admitted was a claim the protocol never
1409
+ made. Exported from the package root, so it is a break for
1410
+ `@object-ui/react` consumers in its own right, not a knock-on.
1411
+
1412
+ The `DataSource` docblock now states the rule once, for the whole interface,
1413
+ so a reader who meets one method does not have to re-derive it from another.
1414
+
1415
+ ## Migration
1416
+
1417
+ Convert where you build the id, not at each call:
1418
+
1419
+ ```ts
1420
+ // before
1421
+ await dataSource.delete('accounts', row.id); // row.id: number
1422
+ await dataSource.bulkDelete?.('accounts', selectedIds); // number[]
1423
+
1424
+ // after
1425
+ await dataSource.delete('accounts', String(row.id));
1426
+ await dataSource.bulkDelete?.('accounts', selectedIds.map(String));
1427
+ ```
1428
+
1429
+ Adapters that **implement** `DataSource` are unaffected: TypeScript compares
1430
+ method parameters bivariantly, so an adapter still declaring `string | number`
1431
+ continues to satisfy the interface. The narrowing binds callers.
1432
+
1433
+ ## Disposition
1434
+
1435
+ **Disposition: no ADR-0087 conversion — nothing authored moves.** The change is
1436
+ on the TypeScript call face only. No zod accept-set narrows, so every metadata
1437
+ document that validates today still validates, and there is no old shape for a
1438
+ conversion layer (ADR-0087 D2) to accept and rewrite, nor a metadata migration
1439
+ step (D3) to replay. What moves is what a numeric-primary-key host sends on the
1440
+ wire for these three methods: `'42'` where it sent `42`, consistently with
1441
+ `update`. The conversion lives in one typed place — the host's own adapter
1442
+ boundary — rather than in a union carried by every caller.
1443
+
1444
+ Ships as `minor` per the launch-window convention: objectui's `major` is a
1445
+ cross-repo pin to `@objectstack`'s so that "same major means compatible" holds
1446
+ across the two repos (`scripts/check-changeset-no-major.mjs`), and objectui's
1447
+ own breaking changes ship as `minor` with the break named where it lands.
1448
+
1449
+ ## One door is deliberately still open
1450
+
1451
+ `DataSource.findOne` keeps `string | number` in this release. Narrowing it is
1452
+ ruled, but its remaining call sites split two ways. `ObjectForm` reads
1453
+ `ObjectFormSchema.recordId` and `DetailView` reads `DetailViewSchema.resourceId`
1454
+ — **authorable** metadata keys whose zod mirrors accept a number today, so
1455
+ narrowing either refuses author JSON that validates now. `DrawerForm`,
1456
+ `ModalForm`, `SplitForm`, `TabbedForm` and `WizardForm` read a `recordId`
1457
+ declared on their own exported schema face; those faces have no zod mirror and
1458
+ no registered node type, so they are TypeScript-only, like
1459
+ `UseViewDataResult.fetchOne` above — but they still cannot narrow first,
1460
+ because `ObjectForm` builds all five of them from its own authorable
1461
+ `ObjectFormSchema` and the refusal simply moves to those hand-off sites.
1462
+
1463
+ So the choice is either an authoring-face narrowing or a reader-side
1464
+ conversion; neither is named by the ruling, and it is carried on objectui#9511.
1465
+ The reason is recorded in the `DataSource` docblock so it is not rediscovered.
1466
+ - c3df43a: `ObjectGrid` takes `onNavigate` as a component prop, and the list channel's navigation callback takes one closed mode token, `'view' | 'new_window'`.
1467
+
1468
+ `ObjectGridSchema.onNavigate`'s documentation told programmatic callers to pass it on `ObjectGridComponentProps`, where the grid's nine other callbacks live — but that interface did not declare it, so a host that followed the advice passed a function the grid never called, with no diagnostic. `ObjectGridComponentProps` now declares `onNavigate`, and the grid reads the prop first and `schema.onNavigate` only when no prop is supplied: when both are given, the prop wins. `onRowClick`, when supplied, still takes the row click before either.
1469
+
1470
+ The callback's second argument is now `RecordNavigateAction`, a new type exported by `@object-ui/types`: the closed union `'view' | 'new_window'`, where `'view'` opens the record page and `'new_window'` opens it in a new browser tab. It is exactly what `useNavigationOverlay` emits, and the vocabulary `@objectstack/spec` 17.5.0 gives the react-tier `ListView` `onNavigate` callback. The same type now sits on all four faces of the channel — `ObjectGridComponentProps.onNavigate`, `ObjectGridSchema.onNavigate`, `ListViewSchema.onNavigate`, and `UseNavigationOverlayOptions.onNavigate` in `@object-ui/react` — where the three existing faces declared `action?: string` and two of them described it as `'view' | 'edit'`. No branch of the hook passes `'edit'`.
1471
+
1472
+ **Clause-②: yes** — one prop added (`ObjectGridComponentProps.onNavigate`), one type exported (`RecordNavigateAction`), and three published callback types narrowed from `(recordId, action?: string) => void` to `(recordId, action: RecordNavigateAction) => void`. What a host has to change:
1473
+
1474
+ - A handler you supply that is written `(id, action?: string) => …`, or with no second parameter at all, still compiles unchanged and receives exactly what it received before.
1475
+ - A handler whose second parameter is annotated with a union that leaves out `'new_window'` — the old documentation's `'view' | 'edit'` is the likely one — is now refused with TS2322. Annotate it `RecordNavigateAction` (or drop the annotation) and handle `'new_window'`, which a Cmd/Ctrl/middle-click and `navigation.mode: 'new_window'` already deliver.
1476
+ - Code that CALLS one of these callbacks itself must now pass the second argument, and it must be one of the two members.
1477
+ - f0f4d6c: `SchemaRenderer` no longer spreads an authored `data` key as a React prop for blocks
1478
+ whose published `data` row is the `ViewData` OBJECT arm (objectui#9571, ruling
1479
+ objectui#8348 Q2-C, decision batch #136 item 3, maintainer 「同意」).
1480
+
1481
+ **Behavioural, deliberately.** 「8348 以协议为准」 (batch #83) retired the bare-array
1482
+ `data` shorthand at the shared record-source ladder, but the key had a second carrier
1483
+ that the ruling did not reach and that outranked the first: `SchemaRenderer` spreads
1484
+ every non-metadata node key as a React prop, and `ObjectGrid` (`passedData`),
1485
+ `ObjectMap` (`dataProp`) and `ObjectGantt` each lift a `data` PROP with an
1486
+ unconditional `Array.isArray` ahead of the ladder. So an authored
1487
+ `data: [ …rows… ]` on `object-grid` / `object-map` / `object-gantt` still drew,
1488
+ end-to-end, after the ladder had refused it. AGENTS.md #0.1: one key, one honoured
1489
+ spelling — the tolerated second carrier is the defect.
1490
+
1491
+ An authored `data` now reaches such a block only as `schema.data`, judged by the
1492
+ block's own row. In development the renderer says so, once per node: the ladder emits
1493
+ no runtime signal of its own, so without that warning the retirement would land as a
1494
+ page that quietly stopped drawing.
1495
+
1496
+ **Unchanged, and refused on purpose.** The `data` PROP itself. Gating the prop on the
1497
+ arm (option B) was refused because that prop is how a host — `plugin-list`'s
1498
+ `ListView`, `ObjectView` — hands down rows it already fetched. A host rendering
1499
+ `<ObjectGrid data={rows} …/>` directly, or passing `data` through `SchemaRenderer`'s
1500
+ own props, still delivers those rows, and still outranks the authored key.
1501
+
1502
+ Blocks on the ARRAY arm are untouched: `object-calendar`'s published row is
1503
+ `z.array(...)`, so its authored bare array is on-contract and keeps the prop seat.
1504
+ Every type the new reading does not list keeps today's behaviour verbatim.
1505
+
1506
+ **Migration.** Author inline rows the declared way —
1507
+ `data: { provider: 'value', items: [...] }` — or keep using the deprecated
1508
+ `staticData: [...]` array, which is unchanged. Both were already the only spellings
1509
+ `os validate` and the save gate accepted.
1510
+
1511
+ `@object-ui/core` gains one export, `recordSourceDataArmForType(type)`: which `data`
1512
+ arm a registered block type declares. It exists for the one consumer that cannot be
1513
+ handed the arm as a parameter — `SchemaRenderer`, which is generic over every
1514
+ registered type.
1515
+ - 3939545: fix(react,app-shell): `usePageAssignment` picks a record page by `type` alone — `pageType` is a key `PageSchema` refuses
1516
+
1517
+ `usePageAssignment` decided whether a page is a record page with
1518
+ `p.pageType ?? (p.type === 'record' ? 'record' : undefined)`. `PageSchema` is a
1519
+ `strictObject` that refuses `pageType` by name: it is one of the schema's
1520
+ declared aliases of `type`, so a page carrying it is a hard parse error that
1521
+ names `type` as the key to write. The first operand read a spelling no parsing
1522
+ page carries, and taught it onward as a live discriminator. The filter now reads
1523
+ `type` alone, and the comment beside it — which said the designer-side
1524
+ `record_detail` spelling was "still expressible in raw metadata" — is corrected:
1525
+ `PageSchema` refuses `pageType`, and `record_detail` left `PageTypeSchema`.
1526
+
1527
+ **Behaviour change, for pages that do not parse.** The metadata cache the hook
1528
+ walks holds `/meta/page` bodies as served — the client runs no schema over
1529
+ them — so the hook can still be handed a page that carries `pageType`:
1530
+
1531
+ - a page with `pageType: 'record'` and no `type: 'record'` is no longer picked;
1532
+ the object renders its synthesized default detail page instead;
1533
+ - a page with `type: 'record'` and some other `pageType` value is now picked.
1534
+
1535
+ Every page that parses behaves as before, because none can carry `pageType`.
1536
+
1537
+ The mirrors of the same discriminator move with it:
1538
+
1539
+ - The navigation page picker (`isStaticPageOption`) reads `type` alone, as the
1540
+ hook does. A row carrying `pageType: 'record'` without `type: 'record'` is now
1541
+ offered as a static page, and a `type: 'record'` row is excluded whatever its
1542
+ `pageType` says.
1543
+ - `recordPageEnvelope` writes `type: 'record'` and no `pageType`. It used to
1544
+ write `type: 'page'` plus `pageType: 'record'`, a body `PageSchema` refuses on
1545
+ both keys. The helper has no caller in this repository.
1546
+ - The record-page preview in the page designer still reads `pageType` on an
1547
+ unsaved draft; its comment now says why, and that the runtime does not match
1548
+ on it.
1549
+ - 7649f43: `SchemaRenderer` now applies the objectui#9571 authored-`data` strip to the legacy
1550
+ `props` alias bag as well, so all three authoring spellings of that key lose the React
1551
+ prop seat on the object arm (objectui#9758, decision batch #167 item 2, letter 剥,
1552
+ maintainer 「其他同意」).
1553
+
1554
+ **The carrier this closes.** objectui#9571 removed an authored `data` from
1555
+ `componentProps` — the node's own non-metadata keys — for blocks whose published `data`
1556
+ row is the `ViewData` OBJECT arm. But `createElement` spreads the legacy `props` alias
1557
+ bag AFTER that strip, so an author who spelled the identical key
1558
+ `props: { data: [ …rows… ] }` kept the seat the ruling had just taken away, and
1559
+ `ObjectGrid` (`passedData`), `ObjectMap` (`dataProp`) and `ObjectGantt` still lifted it
1560
+ with an unconditional `Array.isArray` ahead of the shared ladder. The arm predicate was
1561
+ right; the corpus was one bag short. AGENTS.md #0.1: one key, one honoured spelling.
1562
+
1563
+ Both bags now go through one helper with ONE arm reading, so the question is asked once
1564
+ instead of at two call sites that can drift apart.
1565
+
1566
+ **Unchanged, and refused on purpose.** The `data` PROP itself — `SchemaRenderer`'s own
1567
+ React props are spread LAST and still win, which is how `plugin-list`'s `ListView` and
1568
+ `ObjectView` hand down rows they already fetched. Nothing else the alias carries moves:
1569
+ every other key under `props` is spread exactly as before, the objectui#5123 precedence
1570
+ between `props` and `properties` is untouched, a degenerate non-object `props` still
1571
+ contributes no keys, and blocks on the array arm or on no declared arm keep their
1572
+ authored `data` prop verbatim. There is no validator refusal here: whether the `props`
1573
+ alias exists at all is objectui#4795's pending question ②, which is the maintainer's.
1574
+
1575
+ **Diagnostics.** The development warning objectui#9571 added now fires for the alias
1576
+ carrier too, once per node, naming the key, the arm and the spelling that works. Its
1577
+ objectui#6708 neighbour — the notice that a `props` bag was not read — stops naming
1578
+ `data` on this arm, because after this change both of its sentences are false for that
1579
+ key: it is no longer spread as a React prop, and the remedy it recommends
1580
+ (`properties: { data }`) loses the seat as well. It still names every other key the
1581
+ alias drops on the same node. The objectui#4795 unevaluated-expression scan is
1582
+ deliberately unchanged and still reports an authored `props: { data: '${…}' }`, because
1583
+ that value is still in front of a user.
1584
+
1585
+ **Migration.** Author inline rows the declared way —
1586
+ `data: { provider: 'value', items: [...] }` — or keep the deprecated `staticData: [...]`
1587
+ array, which is unchanged. Both were already the only spellings `os validate` and the
1588
+ save gate accepted.
1589
+ - 0c789a4: `useNavigationOverlay` stops reading the retired `navigation.view` key, and stops substituting an authored name for the navigation-MODE token
1590
+
1591
+ `view.list.navigation.view` was removed in `@objectstack/spec` 17.5.0 under ADR-0049
1592
+ (enforce-or-remove). `useNavigationOverlay` was its one shipped consumer, and the spec's
1593
+ tombstone describes that hook by name: the authored value "was passed straight into the
1594
+ navigation-MODE argument of the console's `onNavigate`, where anything other than `edit`
1595
+ or `view` matched no branch, so the key selected nothing and could silently deaden the
1596
+ row click."
1597
+
1598
+ The second argument of `onNavigate` is the navigation MODE token, not a view name. The
1599
+ hook read `const view = navigation?.view` and spent it at two call sites as
1600
+ `view ?? 'view'` — the no-config early return and the `page` branch — so an authored
1601
+ `{ mode: 'page', view: 'summary_view' }` dispatched `'summary_view'` into a slot hosts
1602
+ read against a closed `edit`/`view` vocabulary. An authored name did not SELECT a view,
1603
+ it SUBSTITUTED for the mode.
1604
+
1605
+ Both call sites now pass the literal `'view'`, which is what every config WITHOUT the key
1606
+ already dispatched. No replacement route is wired into this hook, and no fallback belongs
1607
+ here: per the same tombstone, choosing what opens for a record is page assignment — assign
1608
+ a `record` page to the object and let `isDefault` pick the one that opens — while a list
1609
+ view's `navigation` block only decides HOW the detail is surfaced (`mode`, `size`).
1610
+
1611
+ **Breaking for anyone who authored the key, and for anyone reading the hook's `view`
1612
+ member.** `NavigationOverlayState` no longer publishes `view`. It could only ever carry
1613
+ the retired key, so keeping it would publish a field that is permanently `undefined` —
1614
+ the same "declared, consumed, and wrong" state the retirement exists to end. Measured
1615
+ before removing it: zero readers of that member anywhere in this repository, against a
1616
+ lit control on its sibling members in the same command. Hosts outside this repository
1617
+ were not measured. Per this repo's version policy a breaking change ships as `minor`; the
1618
+ semantics are stated here rather than in the bump.
1619
+
1620
+ Behaviour on the Console is unchanged, and that is the point rather than a caveat:
1621
+ `ObjectView`'s `onNavigate` already routed every non-`new_window` action to the same
1622
+ record-detail route, so the substitution was inaudible exactly where it was loudest in
1623
+ the metadata. Its comment claimed a custom name was "resolved by RecordDetailView from
1624
+ its own config"; no layer ever resolved a view by that name, and the comment is corrected
1625
+ with the read it described.
1626
+
1627
+ Two existing pins asserted the substitution as intended behaviour and are INVERTED rather
1628
+ than deleted — `useNavigationOverlay.modeDefault` and `gridNavigationMembers-8071`, both
1629
+ keeping their authored `view` fixture because it is the one input the old and new
1630
+ implementations disagree about.
1631
+ - ae61ad4: The console form now renders a refusal message the producer explicitly marked
1632
+ as user-facing, instead of always substituting a generic string
1633
+ (objectui#5210).
1634
+
1635
+ An application's hook guards could not talk to their users. When a hook refused
1636
+ a write with 403, the form replaced the server's text with
1637
+ `form.noPermissionToSave` unconditionally — the recorded objectstack#3821 fix,
1638
+ which exists because a raw refusal body puts untranslated platform diagnostics
1639
+ (`FORBIDDEN: insufficient privileges to update showcase_private_note
1640
+ pi-TgoJ4_DM55Fqz`) in front of end users. The external report behind this change
1641
+ had 11 guards whose deliberate, localized guidance — which role owns the action,
1642
+ whom to ask — never reached anyone, and named the incentive that creates:
1643
+ returning 400 instead of 403 for permission failures, degrading the status
1644
+ semantics logs, monitoring and API consumers depend on.
1645
+
1646
+ The maintainer ruling (2026-08-19) was a producer-side opt-in rather than a
1647
+ chattier 403 branch, and the platform half shipped as objectstack `79c46da90`: a hook
1648
+ marks its refusal text with `userMessage` at throw time. This is the consumer
1649
+ half.
1650
+
1651
+ - `@object-ui/react` gains `declaredUserMessage(err)` — the one "is this
1652
+ marked?" read. It answers the marking verbatim, from the two places the
1653
+ adapter boundary parks the envelope (the error itself, where
1654
+ `@objectstack/client` lifts it, and `details`), and `null` for everything
1655
+ else.
1656
+ - The form prefers a marked message over both its generic strings, on ANY
1657
+ status — the marking is status-agnostic; 403 is where this was reported, not
1658
+ a fence the contract draws.
1659
+
1660
+ **Unmarked refusals are unchanged**: a 403 with no marking still shows the
1661
+ generic `form.noPermissionToSave`, and the raw text still goes to the browser
1662
+ console only. objectstack#3821's protection is preserved by construction, not
1663
+ by re-guessing which 403 bodies are presentable — the mark and the marked text
1664
+ are one field, so no boundary that rewraps or substitutes `message` can promote
1665
+ platform prose into the user-facing channel, and platform code never sets it.
1666
+ - ebce5a3: `object-grid` / `object-form` / `detail-view` resolve their data source the same way, and a block that resolves none says so
1667
+
1668
+ The three object-bound blocks disagreed about how the data-source adapter reached
1669
+ them. `object-grid` and `object-form` were registered through wrappers that read
1670
+ it from `SchemaRendererProvider` context; `detail-view` was registered as the raw
1671
+ component, which reads a React `dataSource` prop. `SchemaRenderer` itself reads
1672
+ only context, so the two wirings were mutually exclusive: measured with correct
1673
+ keys in every cell, provider wiring gave the grid `find` 1 and the detail view
1674
+ `findOne` 0, and prop wiring gave exactly the reverse. Neither reported anything.
1675
+
1676
+ All three now resolve the adapter through one rule — an explicit `dataSource`
1677
+ prop first, the provider context second. This is additive: `detail-view` keeps
1678
+ its prop form (and direct `<DetailView dataSource={…} />` callers are untouched),
1679
+ `object-form` gains a prop form it did not have, and `object-grid` no longer
1680
+ throws `useSchemaContext must be used within a SchemaRendererProvider` when a
1681
+ page has no provider.
1682
+
1683
+ And the silence is over. A block in this family that resolves no adapter renders
1684
+ a **No data source resolved** panel naming the block, the object it was about to
1685
+ read, and the ancestor that injects the adapter — instead of a header-only grid,
1686
+ a field-less form card, or nothing at all. The check is opt-in per block, so a
1687
+ placement with inline rows, inline `customFields`, an inline record or an `api`
1688
+ endpoint is untouched.
1689
+
1690
+ New from `@object-ui/react`: `useResolvedDataSource`, `NoDataSourcePanel`,
1691
+ `noDataSourceMessage`, and a `requiresDataSource` prop on `ElementDataSourceGate`.
1692
+ - cac64b3: The spec-bridge's form-view input now accepts everything `@objectstack/spec`'s
1693
+ FormViewSchema accepts — three key types had drifted narrower, and one of them
1694
+ had inverted (objectui#5652).
1695
+
1696
+ `bridges/form-view.ts` held a third hand-written description of the FormViewSchema
1697
+ contract, after the leaf (objectui#5542) and the two containers (objectui#5596)
1698
+ were converged elsewhere. A description nothing compares is one spec release from
1699
+ being a fork, and this one had already drifted on the keys that decide whether a
1700
+ legal document renders:
1701
+
1702
+ - `FormSection.columns` refused the string spelling of a column count, which the
1703
+ contract admits and folds to a number in its own pipe. It is now the contract's
1704
+ type, and the bridge performs the fold — the `object-form` node's section
1705
+ declares `columns` as a number and its container indexes a grid-class map by
1706
+ it, so forwarding `'2'` handed every downstream renderer a value outside the
1707
+ type it declares.
1708
+ - `FormField.dependsOn` was declared `string[]`, which is the exact inverse of the
1709
+ contract: it admitted only the array arm the contract rejects, and refused the
1710
+ bare parent-field name that is the one configuration making `field-selector`
1711
+ and `dynamic-config` work (objectui#5040).
1712
+ - `visibleWhen`, on both fields and sections, was declared `string`, so the
1713
+ ADR-0089 expression object — the arm `evalFieldPredicate` reads — could not be
1714
+ described at all. Both arms now travel whole onto the node.
1715
+
1716
+ A section's `fields` may also be a bare object-field name, the spec shorthand the
1717
+ list bridge already honours. The form bridge ran it through the object mapper
1718
+ instead, producing a field with no identity (`{ name: undefined }`) for the most
1719
+ ordinary section a form can declare; it is now forwarded verbatim, which is what
1720
+ the node's own `fields` slot admits.
1721
+
1722
+ Each drift-prone key's type is now bound to the `@objectstack/spec` symbol that
1723
+ owns it rather than restated, so it cannot drift again, and the compile-time pins
1724
+ in `FormViewWidenedArms.test.ts` fail if a future edit restates any of them by
1725
+ hand. The documented subset itself is unchanged: the keys this bridge does not
1726
+ declare, including the retirement ledger it keeps, stay exactly as they were.
1727
+ - bf28341: refactor(spec-bridge): retire the spec-bridge — `SpecBridge`, `bridgeListView`, `bridgeFormView` (#6366)
1728
+
1729
+ Removed under the 2026-08-27 maintainer ruling on #6366 (Option A — remove, and
1730
+ for `SpecBridge` as a WHOLE, not just the form half), on the same criteria the
1731
+ 17.0.0 entry used to remove `bridgePage` / `bridgeDashboard`: no runtime
1732
+ consumer, and a route that could not deliver a working result even in
1733
+ principle. The stage-1 measurement (2026-08-26, PM-verified) the ruling rests
1734
+ on:
1735
+
1736
+ - **Zero consumers at every reachable endpoint** — no non-test caller in this
1737
+ repo, none in the sibling `objectstack` repo (which does not depend on
1738
+ `@object-ui/react` at all), and a GitHub-wide public code search returning
1739
+ hits only inside this repository. External npm-private hosts are
1740
+ unmeasurable from here; the ruling records fallback C (keep + document the
1741
+ boundary) should evidence of one ever surface.
1742
+ - **The bridged form route was structurally unable to work**: a bridged node
1743
+ carries neither `objectName` nor `customFields`, so `ObjectForm` takes the
1744
+ branch its own code labels "cannot proceed" and renders, in the registry
1745
+ wrapper's words, "a field-less card in silence". The only working host
1746
+ recipe (`{...node, objectName: '...'} as any`) existed solely inside the
1747
+ list bridge's integration tests and was documented nowhere.
1748
+
1749
+ This knowingly reverses the 17.0.0 changelog line "The `list` and `form`
1750
+ bridges are unaffected and remain the live authoring path" — by maintainer
1751
+ ruling, on the same measurement criteria that entry itself applied.
1752
+
1753
+ #5898's restored-key work on the form-view bridge (the
1754
+ `FormViewSpecConformance` suite and the spec keys it carried onto the bridged
1755
+ node) is **superseded by this removal, not fixed** — the route it repaired is
1756
+ gone. #6366's measured type-vocabulary asymmetry (a bridged field carrying
1757
+ `text` where the normalizer produces `field:text`) is likewise mooted rather
1758
+ than repaired.
1759
+
1760
+ The suites pinning the removed route go with it — retirement of a route
1761
+ nothing travels, not quarantine: the seven suites under
1762
+ `react/src/spec-bridge/__tests__/`, and plugin-grid's
1763
+ `specBridgeColumnSpelling` / `specBridgeExportFormats` render-integration
1764
+ suites. `ObjectGrid`'s own column-spelling, export-gate and density behavior
1765
+ keep their non-bridge pins (`columnDeclaredSpellingOnly`, `exportGate`,
1766
+ `exportServer`, `rowHeightOffSpecBoundary`, and core's
1767
+ `normalize-list-view` suite).
1768
+
1769
+ BREAKING CHANGE: the public exports `SpecBridge`, `bridgeListView`,
1770
+ `bridgeFormView` and the types `BridgeContext` / `BridgeFn` / `ObjectDefLite`
1771
+ are removed from `@object-ui/react`. There is no replacement translation
1772
+ layer — author `object-grid` / `object-form` nodes directly (the live path is
1773
+ `app-shell`'s `ObjectView`, which builds them from the object's own metadata),
1774
+ exactly as the 17.0.0 entry already directed for pages and dashboards.
1775
+ - 83fe6e7: row predicates on runtime record surfaces resolve `record.*` only; the bare-field and `data.*` spellings are no longer bound
1776
+
1777
+ Phase 2 of the row-predicate canon (objectui#5330, ruled 2026-08-20, option B;
1778
+ Phase 2 ruled 2026-09-02 and amended 2026-09-05). Until now a
1779
+ row predicate — `visible` / `disabled` / `enabled` on an action renderer, a row
1780
+ action, a `record:alert`, a `page:header` action, a conditional-formatting
1781
+ `condition` — bound the row three ways: canonical `record.status`, bare
1782
+ `status`, and `data.status`. The two non-canonical spellings are retired on
1783
+ every runtime record surface, in both evaluation tiers (`evalRowPredicate` /
1784
+ `partitionRowsByPredicate` in `@object-ui/core`; `usePredicateRecordContext` +
1785
+ `useCondition` in `@object-ui/react`) and for both dialects: a legacy
1786
+ `${data.x}` / `${x}` string on a row surface retires with the CEL spellings.
1787
+
1788
+ **What a retired spelling does now: it faults, exactly as it already did on the
1789
+ server** (`buildScope({ record })` mounts exactly `['record']`, so `status` and
1790
+ `data` are unknown variables there), and each surface applies its EXISTING
1791
+ fault policy — no runtime detector, no "treat as absent" special case, no
1792
+ uniform override:
1793
+
1794
+ - `evalRowPredicate` / `partitionRowsByPredicate` (row kebab, selection bar,
1795
+ `page:header` actions, conditional formatting): the caller's `fallback` —
1796
+ hidden / every row excluded / no style — reported once by the existing fault
1797
+ warning, which names the unknown variable (`Unknown variable: status`) and,
1798
+ on the fast route, carries the `record.` hint.
1799
+ - `useCondition` legs that opt into `throwOnError` (`action:button` and
1800
+ `action:menu` `visible`, `DeclaredActionsBar` `visible`): fail-closed —
1801
+ hidden on every row, reported once as `was hidden/disabled: its predicate
1802
+ threw — status is not defined`.
1803
+ - the non-throwing `useCondition` legs (`action:icon` / `action:group`
1804
+ `visible`, every `disabled` / `enabled`, `record:alert`): fail-soft — shown /
1805
+ greyed / enabled on every row, with the evaluator's own console line.
1806
+ - a host scope that carries its OWN `data` (app-shell's ambient `data: {}`) is
1807
+ left standing: `data.*` on a record surface then reads the host's object — a
1808
+ constant, silent `false` — which is what "no longer bound to the row" means.
1809
+
1810
+ The Phase-1 deprecation warning is removed with the bindings:
1811
+ `warnNonCanonicalRowSpelling` and `resetRowPredicateCanonWarnings` are no
1812
+ longer exported from `@object-ui/core`. `detectNonCanonicalRowSpelling`,
1813
+ `ROW_PREDICATE_CANONICAL_ROOT` and the `NonCanonicalRowSpelling` type stay
1814
+ exported — the offline instrument for sweeping authored metadata.
1815
+
1816
+ The layer rule is unchanged: `data` remains the canonical root on
1817
+ metadata-editing surfaces (ADR-0089 D3, `CANONICAL_ROOT_BY_LAYER`), and
1818
+ app-shell's metadata-admin `SchemaForm` / `predicate.ts` keep binding
1819
+ `{ data: row }` through their own evaluator.
1820
+
1821
+ No stored-metadata survey, export or migration rewrite was run (the maintainer
1822
+ ruled the stored population out of scope, 「不考虑存量」); the Phase-1 warning
1823
+ period was the notice.
1824
+
1825
+ Release note: Phase 1 (`d1ab06f0f` — the canon statement plus the warning) shipped
1826
+ in `@object-ui/core@17.6.0` (npm, 2026-08-24) although its changeset
1827
+ `.changeset/row-predicate-record-canon-5330.md` is still pending on `main`, so
1828
+ the next CHANGELOG section lists Phase 1 and this Phase 2 together: the warning
1829
+ it describes was live from 17.6.0 and is gone from this release on.
1830
+ - d1ab06f: Row predicates declare a canon: `record.*`. The bare shorthand and `data.*` now
1831
+ warn once, and are unchanged otherwise.
1832
+
1833
+ A row predicate (`visible` / `disabled` / `enabled` on an action renderer, a row
1834
+ scope, a `record:alert`) has bound the row three ways since objectui#4075 —
1835
+ `record.status`, bare `status`, and `data.status` — without any of them being
1836
+ declared the contract. The maintainer ruled that question on 2026-08-20
1837
+ (objectui#5330, option B), mirroring the objectstack#7917 option-② precedent for
1838
+ the identical renderer-tolerance shape: **the canon is `record.*`**, and the
1839
+ other two enter a deprecation window.
1840
+
1841
+ The canon states the **server's** accept set, which was this card's first
1842
+ measurement and turns out to be strictly narrower than the renderer's. Measured
1843
+ against `@objectstack/formula@17.1.0`, the engine the server evaluates with:
1844
+
1845
+ | spelling | server runtime | server authoring oracle |
1846
+ |---|---|---|
1847
+ | `record.status` | `{ ok: true, value: true }` | accepted |
1848
+ | bare `status` | `Unknown variable: status` | refused |
1849
+ | `data.status` | `Unknown variable: data` | **silently accepted** |
1850
+
1851
+ `buildScope({ record })` mounts exactly `['record']` — `data` is never bound and
1852
+ the row's fields are never flattened to top level. The three-way binding is a
1853
+ client tolerance with no server counterpart, which is why the warning belongs on
1854
+ this side.
1855
+
1856
+ `data.*` is the dangerous one, and the reason the warning exists. `data` is in
1857
+ `@objectstack/formula`'s `SCOPE_ROOTS`, so the server's bare-identifier oracle
1858
+ waves it through — that list is a deliberately generous "never faults" lint
1859
+ baseline, not the runtime accept set. A `data.*` row predicate therefore passes
1860
+ every authoring gate the platform has and then binds nothing at runtime: not an
1861
+ error, a constant `false`. A `visible` that is constantly false is a button that
1862
+ silently never appears — the objectui#4075 fail-closed signature.
1863
+
1864
+ What ships:
1865
+
1866
+ - `@object-ui/core` exports `detectNonCanonicalRowSpelling`,
1867
+ `warnNonCanonicalRowSpelling`, `resetRowPredicateCanonWarnings` and
1868
+ `ROW_PREDICATE_CANONICAL_ROOT` from a new `evaluator/rowPredicateCanon.ts`,
1869
+ which carries the canon statement and the measurement.
1870
+ - Both evaluation tiers report once, in dev: `evalRowPredicate` (core) and
1871
+ `useCondition` (react, for bags bound by `usePredicateRecordContext`).
1872
+ - Detection reuses the server's own oracles (`collectCelRootIdentifiers`,
1873
+ `firstUndeclaredReference`) rather than a regex, so no second dialect
1874
+ judgement is invented client-side.
1875
+
1876
+ **No spelling is removed and no behaviour changes.** Every predicate that
1877
+ resolved before resolves now — the ruling defers removal behind a stored-metadata
1878
+ survey, and the warning is what makes that survey possible (ADR-0078: a
1879
+ tolerance nothing ever reports can never be retired).
1880
+
1881
+ The deprecation is scoped to the **runtime record layer**. `data` remains the
1882
+ canonical root one layer over, in a metadata-editing form (ADR-0089 D3
1883
+ `CANONICAL_ROOT_BY_LAYER`), and the detector stands down there.
1884
+ - dba7d84: New export: `useSettledSchema` — the settled-schema RESOLUTION half shared by
1885
+ `ObjectKanban` / `ObjectView` / `ObjectCalendar`'s fetch-gate hand copies
1886
+ (objectui#6482, maintainer ruling Option A). It tracks whether an object's
1887
+ definition has finished resolving FOR THE KEY THE CURRENT RENDER IS ASKING
1888
+ ABOUT, returning `{ ready, def }` from one piece of internal state so `ready`
1889
+ and `def` can never be observed inconsistently and a stale key can never read
1890
+ as ready — the structural fix for the `ObjectTree` defect (objectui#6481)
1891
+ where a definition and a separate, one-way-latched "settled" boolean could
1892
+ disagree for a render after the object changed.
1893
+
1894
+ Gate PLACEMENT — which effect branch actually waits on `ready` — stays a
1895
+ per-component decision and is not part of this hook; see the hook's own doc
1896
+ comment. Existing hand copies are migrated on their own subsequent cards, not
1897
+ by this change.
1898
+ - c86185e: Bind `record` into the node-level visibility evaluator, and stop a hoisted
1899
+ `properties.visible` swallowing a declared `visibleWhen`.
1900
+
1901
+ `@objectstack/spec` has declared since ADR-0089 that a page component's
1902
+ `visibleWhen` binds the row — `ui/page.zod.ts`: *"Binds `record`,
1903
+ `current_user`, `page.<var>`"*. `SchemaRenderer` bound no `record` at all. Its
1904
+ evaluator was built from the ambient predicate scope, `data: dataSource` (the
1905
+ connector **adapter**, not the row) and `page: pageVariables`; the row lives in
1906
+ `RecordContext`, which that evaluator never read.
1907
+
1908
+ Because the surface is fail-soft, a `record.*` predicate did not misfire — it
1909
+ resolved to **shown**. Both polarities of the same predicate returned the same
1910
+ verdict, so a visibility gate silently did not gate, on every block on every
1911
+ record page. Measured on `record:alert`, `record:path`, `page:card` and
1912
+ `element:text`.
1913
+
1914
+ Three changes, all in `SchemaRenderer`'s evaluation memo:
1915
+
1916
+ - **`record` is bound**, as the `record` root only — the three roots the
1917
+ describe promises and nothing more. Not as bare fields, and never over
1918
+ `data`, which is what `${data.*}` in a props bag resolves against. Bound
1919
+ conditionally, so "no row" binds nothing rather than shadowing a `record` a
1920
+ host supplied through the ambient scope.
1921
+ - **`visibleWhen` is tested before `visible`.** The memo hoists `properties.*`
1922
+ onto the node, so a node carrying `properties.visible` short-circuited the
1923
+ declared node predicate — the one key the spec tells authors to write was the
1924
+ one key that could be silently ignored. The two deprecated aliases
1925
+ (`visibleOn` / `visibility`) deliberately keep their rank: they normalize into
1926
+ `visibleWhen` at parse, so a spec-parsed page never reaches them.
1927
+ - **An unresolvable predicate is loud** (dev builds). Fail-soft answered "this
1928
+ predicate is broken" and "this predicate said yes" with the same word. The
1929
+ verdict is unchanged on every path — `evaluateCondition` already returned
1930
+ `true` for every unevaluable predicate, including the non-negated `hidden` /
1931
+ `hiddenOn` legs where that `true` means HIDE — so only the silence moved.
1932
+
1933
+ **Behaviour change, stated plainly:** a shipped page whose node-level
1934
+ `record.*` predicate was previously inert now evaluates. A block that was
1935
+ permanently visible may begin to hide — which is the point, but it is a verdict
1936
+ change, not a no-op. `properties.visible` is unaffected in verdict: an
1937
+ in-tree census found **zero** node-level `record.*` predicates on page
1938
+ components, so nothing in this repository changes verdict.
1939
+
1940
+ ### Patch Changes
1941
+
1942
+ - 7b10bef: `ElementDataSourceGate` now reports a saved view's refused row cap on the renderer path
1943
+ (objectui#10015).
1944
+
1945
+ objectui#9928 made `@object-ui/core` drop a saved view's `pagination.pageSize` (or legacy flat
1946
+ `limit`) when the contract refuses it: `0`, a negative, or a fraction. The drop is correct, but on
1947
+ the renderer path it removed the author's only diagnostic. The node's cap went from `0` to absent,
1948
+ and the consuming renderer treats an absent key as "not a mistake". The gate had never reported a
1949
+ VIEW cap, so nothing reported it at all.
1950
+
1951
+ The gate now emits `elementDataSourceRefusedLimitMessage`, the same sentence `ViewDataProvider`
1952
+ already uses on the path with no renderer. It is emitted once per declaration, from an effect and
1953
+ never from render, on the same `console.warn` channel. It fires only when the refusal changed what
1954
+ the block receives: the block reads a row cap, the binding declares no `limit` of its own, and the
1955
+ component's own cap is not a usable one. A view with no cap, a usable view cap, a usable component
1956
+ cap, a binding `limit`, or a block with no row cap stays silent. It never fires together with the
1957
+ gate's existing displaced-cap message (objectui#9899), because that message needs a usable cap to
1958
+ have been written and, with the view's cap refused, the only one left is a binding `limit`. No
1959
+ wire value changes.
1960
+ - 0aacecc: `useRecordSearch` no longer keys its search effect on the identity of the
1961
+ caller-supplied `getDisplayName` option (objectui#10044).
1962
+
1963
+ The resolver is read only inside a run, to label hits; it never decides whether a
1964
+ run happens. It was nevertheless the last entry of the search effect's dependency
1965
+ list, and that effect's cleanup clears the pending debounce timer. A caller passing
1966
+ the obvious spelling for an optional callback, an inline arrow, handed the hook a new
1967
+ function on every render, with two opposite results: a parent re-rendering faster
1968
+ than `debounceMs` (250 ms by default) never searched at all, and a parent re-rendering
1969
+ sparsely issued a second, identical request after the first had gone out. This is
1970
+ the dependency AGENTS.md §5 commandment #10 bans.
1971
+
1972
+ Each run now reads the resolver of the latest render through a ref, so an inline
1973
+ resolver is safe. A swapped resolver labels the hits of the next run; results already
1974
+ shown keep their labels. The option's name, type and default are unchanged.
1975
+ - 162621b: `useSettledSchema` no longer republishes an EQUAL definition as a new object,
1976
+ so swapping the adapter on a record-bound view costs ONE record query instead
1977
+ of two.
1978
+
1979
+ The hook refetches `getObjectSchema` whenever the adapter identity changes, and
1980
+ every consumer its doc comment instructs — `ObjectGantt`, `ObjectCalendar`,
1981
+ `ObjectView`, `ObjectTimeline`, `ObjectGallery` — keys its record fetch on the
1982
+ returned `def`, which is what `AGENTS.md` §5 commandment #10 tells a consumer to
1983
+ do (key on the payload, never on a memo identity). The producer half of that
1984
+ same sentence is what this fixes: a byte-identical answer arriving as a fresh
1985
+ object re-fired every one of those fetches, and the duplicate query carried the
1986
+ same `$expand` set as the one before it. Measured first-hand on `ObjectGantt`
1987
+ with an instrumented adapter, with a control swap in the same command; the
1988
+ counts, and the control that makes them a discrimination rather than a counting
1989
+ artefact, are asserted by
1990
+ `packages/react/src/hooks/__tests__/useSettledSchema.equalPayload-10106.test.tsx`
1991
+ (objectui#10106).
1992
+
1993
+ A definition that genuinely CHANGED across the swap still publishes a new
1994
+ object and still re-queries — that case is pinned in the same file, because
1995
+ "fetch less" would have been the wrong repair.
1996
+ - 2ad3671: Correct the read rule in the `InlineEditContextValue.draft` doc comment (objectui#10466).
1997
+
1998
+ It told hosts to read a field's live value as `draft[name] ?? data[name]`. That read
1999
+ falls back to the saved value when the draft value is `null` or `undefined`. A field the
2000
+ user emptied is an own draft key with an empty value, and so is a single select or radio
2001
+ emptied by a cascade clear, which stages `null` since objectui#10291. A host that followed
2002
+ the comment showed the value the user had just removed, and handed it back to an option
2003
+ widget that pruned it, which pruned it again on every render (the loop objectui#7190 fixed
2004
+ on the highlights strip).
2005
+
2006
+ The comment now teaches the own-key read: take the value from the staged record
2007
+ `{ ...data, ...draft }`. An own draft key wins even when its value is empty
2008
+ (`null`, `undefined`, `''`), and the saved value is read only when the key is absent. The
2009
+ comment ships in `dist` typings and is what a host author sees on hover. No runtime
2010
+ behaviour changes. A source-text pin keeps the comment from teaching a fallback read again.
2011
+ - 3d614ea: fix(react,app-shell): a search hit's label no longer prints a field the viewer may not read (objectui#10500)
2012
+
2013
+ The ⌘K command palette, the full-page search results and the `global:search`
2014
+ block label each record hit with `getRecordDisplayName`, and `useRecordSearch`
2015
+ handed that resolver the record as served. On a backend that does not strip
2016
+ denied fields, a `titleFormat` token or a declared `nameField` that the loaded
2017
+ permission policy denies therefore printed as the hit's label.
2018
+
2019
+ `useRecordSearch` takes a new optional `fieldReadPolicy` option: pass
2020
+ `usePermissions()` from `@object-ui/permissions`. Once that policy is loaded,
2021
+ each hit is labelled from its record with the denied fields removed, before any
2022
+ `getDisplayName` resolver reads it. The record id is always kept, so the
2023
+ `Record #` id floor still applies. A denied field is read exactly as an absent
2024
+ one, and the label falls through to its next source, the label ObjectStack's
2025
+ `FieldMasker` row already yields. When the policy finishes loading, a search
2026
+ that is already showing runs again, so hits labelled before the policy loaded
2027
+ are relabelled. The three app-shell search surfaces now pass the policy. The
2028
+ record title (objectui#10434) and the lookup option label (objectui#10411)
2029
+ follow the same rule.
2030
+
2031
+ Without the option, before a policy loads, and with no permission provider
2032
+ mounted, nothing is removed and every label is unchanged. A title the server
2033
+ computed for a `searchAll` hit is still shown as the server sent it, and a
2034
+ hit's `raw` payload is still the record as served.
2035
+
2036
+ This is defence in depth: ObjectStack's `FieldMasker` already removes the
2037
+ fields a user may not read from the rows it serves, so on that backend no
2038
+ label changes.
2039
+ - 9a5f998: `@object-ui/core` exports `withoutDeniedFields(record, policy, objectName, extraKeep?)`, the field-read rule for one record: the record as the viewer may read it on `objectName` (objectui#10594).
2040
+
2041
+ Every field the loaded `policy` denies is removed, which leaves the row ObjectStack's `FieldMasker` already serves. `id`, `_id` and any key in `extraKeep` (a declared id field) are never judged. Before a policy loads, with no policy, with no object name, or for a value that is not an object, the record comes back as it is; when nothing is withheld the SAME object comes back. `policy` is structural (`isLoaded` and `checkField`), so `usePermissions()` from `@object-ui/permissions` satisfies it and `@object-ui/core` gains no dependency.
2042
+
2043
+ The renderer's display surfaces that each carried a module-private copy of this rule now call the export, with no change in what they draw:
2044
+
2045
+ - `@object-ui/fields`: the lookup cell's name and the user cell's name and avatar (the package entry), and the lookup editor's option label and search chip avatar (`LookupField`).
2046
+ - `@object-ui/react`: `useRecordSearch`'s hit label under `fieldReadPolicy`.
2047
+ - `@object-ui/app-shell`: the record page's title and the row its blocks read (`RecordDetailView`).
2048
+ - `@object-ui/plugin-detail`: `DetailView`'s header title and `record:details`' title dedupe. The package-internal `withoutDeniedFields` module is removed; it was never a package export.
2049
+ - 256b4c9: A filter refused as malformed (`INVALID_FILTER`) no longer throws out of render or out of a click
2050
+ handler at the sites that merge filters there. That holds whether this layer's filter converter
2051
+ refused it or, at the drill seam, the spec's own `parseFilterAST`. Each site now surfaces the
2052
+ refusal the way its siblings already do (objectui#10789). The filter stays **refused**: nothing
2053
+ that was refused is accepted, and no site falls back to "no filter".
2054
+
2055
+ - **`ElementDataSourceGate` / `useElementDataSource`** (`@object-ui/react`): the gate's merge of
2056
+ the component's `filter` with the composed binding, and the merge of a saved view's filter
2057
+ with the binding's own, lowered through the throwing converter inside render. A refused
2058
+ filter from either now makes the binding `missing` with a new optional `filterRefusal` on the
2059
+ result (`error` carries its message). The gate then draws, in place of the block, the
2060
+ malformed-filter notice the wrapped blocks draw: `view.malformedFilter`, naming the refused
2061
+ operator or field. `element:record_picker`, which reads the same hook, shows its
2062
+ configuration-error panel with the refusal.
2063
+ - **`PeoplePicker` / `RecordPickerDialog`** (`@object-ui/fields`): the recents merge and the
2064
+ dialog's `mergedFilter` lower through `toFilterNodeSafely`; a refusal is shown in the picker's
2065
+ existing error state and no query runs without the filter.
2066
+ - **Drill-downs** (`ObjectChart`, `ObjectPivotTable`, `DatasetWidget`, `DatasetReportRenderer`):
2067
+ a widget or report filter refused at the drill seam made the drill click throw. The converter
2068
+ refuses some (a spec `$not`, say). The spec's `parseFilterAST` refuses others, such as a scalar
2069
+ on a list operator in either dialect: `[['stage', 'in', 'won']]` or `{ stage: { $in: 'won' } }`.
2070
+ The drill is now not opened or emitted (never with the scope dropped), and the refusal is logged
2071
+ with `console.warn`, naming the operator. `composeDrillFilter` (`@object-ui/core`) now throws
2072
+ one refusal type: the spec's `INVALID_FILTER` error is re-raised as a `FilterOperatorError` with
2073
+ its message and the same `INVALID_FILTER` / 400 envelope, and any other error passes through
2074
+ unchanged.
2075
+ - **`ListView` export** (`@object-ui/plugin-list`): the server export builds its filter inside
2076
+ its `try`, so a refusal is named in the export menu instead of escaping the click.
2077
+ - **`convertFiltersToAST`** (`@object-ui/core`): an `$or` member that is the TRUE identity no
2078
+ longer stops the members after it from being read. `{ $or: [{}, { a: {} }] }` is now refused
2079
+ like `{ $or: [{ a: {} }, {}] }` already was, instead of answering TRUE (every row); a TRUE
2080
+ disjunct beside members that lower still absorbs its `$or`.
2081
+ - 328abeb: Four Console surfaces that read English under a zh-CN session now read the session's
2082
+ language (objectui#10900). English stays the default.
2083
+
2084
+ - **The generic action success toast.** When an action declares no `successMessage`, the
2085
+ runner falls back to "Action completed successfully".
2086
+ That fallback now goes through a translator: `ActionRunner.setTranslator(translate)` is
2087
+ new in `@object-ui/core`, and `<ActionProvider>` and `useActionRunner` in
2088
+ `@object-ui/react` install the session's `t` on the runner they build. The pack key is
2089
+ `actions.completedSuccessfully`. An author's `successMessage` still reaches the toast
2090
+ untranslated, and a runner with no translator installed still toasts the English sentence.
2091
+ - **The social sign-in buttons on the login and sign-up pages.** `SocialSignInButtons`
2092
+ takes a new `buttonText` prop, a template whose `{provider}` is replaced with the
2093
+ provider's display name; unset, the buttons keep "Continue with {provider}" and
2094
+ "Sign up with {provider}". `LoginForm` and `RegisterForm` take a new `socialButton`
2095
+ label and pass it, with their existing `orText` label, to the buttons. `orText` was
2096
+ documented as the divider label but rendered nowhere; it now sets the divider under the
2097
+ buttons, and its documented default is corrected from "or" to "or continue with email",
2098
+ the text that divider has always shown. The console's login and sign-up pages and
2099
+ `@object-ui/app-shell`'s `DefaultLoginPage` / `DefaultRegisterPage` pass the new
2100
+ `auth.login.*` and `auth.register.*` `socialButton` / `orText` keys. The provider's display
2101
+ name is inserted as-is: the component's own label for the branded providers it knows,
2102
+ otherwise the name the server reports.
2103
+ - **Build Doctor.** The build conversation's Build Doctor button (its accessible name and
2104
+ both tooltips) and the title of the drawer it opens read `console.ai.buildDoctor`,
2105
+ `console.ai.buildDoctorTitle` and `console.ai.buildDoctorDisabledTitle`.
2106
+ - **Setup → marketplace.** On `system/marketplace` and the pages under it, the breadcrumb
2107
+ segment after System reads `console.breadcrumb.marketplace` instead of the humanized URL
2108
+ slug; other `system/*` segments are unchanged. The zh marketplace search placeholder
2109
+ reads 「按名称或标识搜索应用…」 instead of 「按名称或 manifest ID 搜索应用…」. The search
2110
+ itself is unchanged: it matches the display name, the identifier and the description.
2111
+
2112
+ All ten locale packs carry the nine new keys; no existing `en` value changes.
2113
+
2114
+ **Clause-②: yes** — besides the nine pack keys, the public surface widens by four optional
2115
+ members: `ActionRunner.setTranslator`, `SocialSignInButtonsProps.buttonText`,
2116
+ `LoginFormLabels.socialButton` and `RegisterFormLabels.socialButton`. Nothing is removed,
2117
+ renamed or narrowed, no accept set changes, and `@object-ui/react` exports nothing new.
2118
+ - 0645133: fix(react): one Ctrl+Z undoes one record write, and Ctrl+Z inside a text field is the field's own undo (objectui#11081)
2119
+
2120
+ What a user saw: with two or more undoable actions behind them, one Ctrl+Z in the
2121
+ console reverted SEVERAL saved record writes, one per mounted `useGlobalUndo`
2122
+ (every console route mounts at least two), with a toast for each. And pressing
2123
+ Ctrl+Z while typing in a field did not undo the typing: the keypress was
2124
+ `preventDefault`ed and the last saved record write was reverted instead.
2125
+
2126
+ What changes: however many `useGlobalUndo` instances are mounted, one `window`
2127
+ `keydown` listener serves them, so one Ctrl+Z (or Ctrl+Shift+Z) pops at most one
2128
+ operation, and exactly one instance answers it: the longest-mounted live one,
2129
+ whose data source runs the write and whose toast shows. That is the instance that
2130
+ already answered whenever a single operation was stacked, so a single Ctrl+Z
2131
+ behaves as before. A keypress whose target is an `input`, `textarea`, `select` or
2132
+ contenteditable region is left alone: nothing is popped, the default is not
2133
+ prevented, and the browser's own undo runs in the field. The listener is removed
2134
+ when the last instance unmounts.
2135
+
2136
+ Unchanged: the hook's signature and return shape, and a toast's Undo button, which
2137
+ still runs through the instance that raised the toast.
2138
+ - a8b9889: A related list inside a record now places its child object's `record_related` actions on each row (objectui#11270; the renderer half of the enforce answer on objectstack-ai/objectstack#20937).
2139
+
2140
+ The spec declares the action location `record_related` ("actions on a related list section inside a record"), and nothing in objectui read it, so an action placed only there rendered nowhere. Triage read the location as row placement, scoped to a parent record: `list_item` shows on the child's rows wherever the child is listed, and `record_related` shows on them only in a related list inside a parent record.
2141
+
2142
+ - **The host bridge** (`RelatedRecordActionsBridge`, `@object-ui/app-shell`) puts the child object's `record_related` actions in each row's action menu beside its `list_item` actions, in the child's declared order, when a parent record is in scope. An action that declares both renders once. The child object's own list view still reads `list_item` only, so a `record_related`-only action does not appear there.
2143
+ - **The authored `actions` channel** on `record:related_list` (`@object-ui/plugin-detail`) accepts an id whose action declares `record_related` and places it in each row's menu. The refusal notice for an id that cannot be placed now names the three related-list locations: `list_toolbar`, `list_item` and `record_related`.
2144
+ - **The action designer's preview** (`@object-ui/app-shell`) draws a `record_related` action on the rows of a list, with the frame `list_item` uses. It used to draw it as a button in a section header, which is not where the console places it. `record_section` keeps its section-header frame.
2145
+ - `@object-ui/react`: the `RelatedRecordHandlers.rowActions` doc comment names both row locations. No code change.
2146
+
2147
+ ⚠️ Behaviour change: a child action that declares `record_related` now shows on its related-list rows inside a record, where it used to show nowhere.
2148
+ - b654d4e: `toRenderableSchema` declares its parameter as `SchemaNode` (objectui#11479).
2149
+
2150
+ The parameter used to spell out `SchemaNode`'s members one by one. It now names the union itself, so it follows `SchemaNode` instead of keeping a hand-written copy of it. Nothing it accepts changes today: the two spellings are the same type, and the function's return type and behaviour are untouched.
2151
+
2152
+ ⚠️ **Dated note, 2026-10-02 — the parameter narrows with `SchemaNode` — objectui#11466.** At this change the parameter's two spellings were the same type and nothing it accepted changed; now, later in this same release, `SchemaNode`'s object arm is `DeclaredNode`, the union of the declared node types, so the parameter follows it and no longer accepts a value typed `BaseSchema`. That is the point of naming the union instead of copying it. The rest of this entry is kept as the reading of this change.
2153
+ - 546ddf7: A node-gate visibility predicate that FAULTS now says so in a production build, once per
2154
+ distinct predicate source (objectui#6038, maintainer ruling 2026-08-25, option B: "the
2155
+ silence is no longer an accepted property"). Observability only — no verdict moves.
2156
+
2157
+ `SchemaRenderer`'s visibility chain is fail-open: a predicate that cannot be evaluated
2158
+ resolves to the same answer as one that said yes, so a gate that stops biting looks
2159
+ exactly like a gate the author got right. The diagnostic that names it (objectui#5454 /
2160
+ objectui#5687) sat behind a `__DEV__` short-circuit, because the only fault-detection
2161
+ channel available was `throwOnError`, and on the CEL branch `evaluateCelCondition`
2162
+ implements that by evaluating **twice** — too expensive to ship for every predicate of
2163
+ every node.
2164
+
2165
+ **What production actually printed before, measured per dialect on the built evaluator**
2166
+ — the card's premise held for one dialect of three, and the other two failed in opposite
2167
+ directions:
2168
+
2169
+ | dialect | production console, before |
2170
+ |---|---|
2171
+ | bare string | **nothing** |
2172
+ | `{ dialect: 'cel' }` envelope | one generic line, deduped per source |
2173
+ | `${…}` template | one generic line **per evaluation**, never deduped |
2174
+
2175
+ So the dialect objectstack#11254 measured a live gate breaking on was the silent one,
2176
+ while the template dialect was the console flood the ruling's rate-limit clause exists to
2177
+ prevent.
2178
+
2179
+ **The fix reports the fault the evaluator already detected, at the same number of engine
2180
+ calls.** `EvaluationOptions.onFault` is a new passback on `@object-ui/core`'s
2181
+ `ExpressionEvaluator`: every fault site is already inside a `catch`, or already holds the
2182
+ canonical engine's failure reason, so nothing is evaluated twice. It mirrors, one layer
2183
+ up, the seam `FieldPredicateDiagnostic` already documents (`warn: false` plus a reason
2184
+ passback), and supplying it transfers reporting to the caller so one fault stays one
2185
+ line. Pinned: the CEL branch performs the same number of record reads with the passback
2186
+ as without it, and strictly fewer than the `throwOnError` probe.
2187
+
2188
+ `SchemaRenderer` passes it in production and reports through the **same** reporter the dev
2189
+ branch uses — same message, same severity, same dedupe `Set`, same key. Development and
2190
+ production now print the identical line for the identical fault; the `__DEV__` gate no
2191
+ longer decides *whether* a fault is reported, only *how* it is detected.
2192
+
2193
+ `page:tabs` item-level `visibleWhen` (`@object-ui/components`) is covered by the same
2194
+ reporter and the same rate limit. It swallowed the identical fault under a different
2195
+ helper, and it was the worse of the two: the node gate at least reported in development,
2196
+ while a faulting item predicate was silent in *both* builds on a gate whose false verdict
2197
+ removes an entire tab, header and panel.
2198
+
2199
+ **Rate limit:** deduped per (node type, gate key, predicate source) — never per render and
2200
+ never per node instance. A two-hundred-row list of one broken predicate is one line; a
2201
+ second distinct predicate source still gets its own line. Both halves are pinned, because
2202
+ a test that asserts only "a warning was emitted" is equally green on an implementation
2203
+ that emitted fifty, and one that asserts only "exactly one" is equally green on an
2204
+ implementation that suppresses everything.
2205
+
2206
+ **Not changed by this card, deliberately:** the fail-open semantics themselves; the
2207
+ objectui#5687 adapter-only `data.*` report, which stays development-only under its own
2208
+ 2026-08-22 ruling (that path is not a fault — the predicate evaluated perfectly, against
2209
+ the wrong object); and the `features` root on `/forms/:name`, which objectui#6262 settled
2210
+ by refusing it in form-view predicates rather than wiring it.
2211
+
2212
+ `reportUnresolvableVisibilityPredicate`, `formatUnresolvableVisibilityMessage`,
2213
+ `UNRESOLVABLE_VISIBILITY_PREFIX` and `__resetVisibilityPredicateWarnings` are now exported
2214
+ from `@object-ui/react` so every surface that evaluates a node `visibleWhen` shares one
2215
+ reporter and one rate limit — a second copy would mean a second dedupe `Set`, and one
2216
+ authored predicate would be entitled to one line per package instead of one line.
2217
+ - 22ba927: `SchemaRenderer` now hands a stable `schema` object identity to a node carrying
2218
+ `responsiveStyles` (objectui#6270).
2219
+
2220
+ ADR-0065 scoped styles make a styled node take a branch that rebuilds the schema
2221
+ object to merge the generated scope class into `className`. That rebuild was not
2222
+ memoised, so it allocated a new object on **every** `SchemaRenderer` render — even
2223
+ when the `evaluatedSchema` memo directly above it held. Every downstream renderer
2224
+ that memoises on `[schema]` therefore saw a fresh identity and re-ran: concretely
2225
+ `ObjectMap`'s `dataConfig` and `mapConfig`, and the whole marker cascade below them
2226
+ (`markers` → `filteredMarkers` → `clusteredData` / `markerBounds` → `initialViewState`).
2227
+ Only nodes on that branch were affected — a plain node was already handed the
2228
+ memoised `evaluatedSchema` itself.
2229
+
2230
+ The trigger is narrower than "has `responsiveStyles`" reads: it needs one of the four
2231
+ sized breakpoint keys (`large` / `medium` / `small` / `xsmall`). A `{ base: … }` shape
2232
+ never took the branch and was never affected.
2233
+
2234
+ The scoped-style computation now lives in a `useMemo` keyed on
2235
+ `[evaluatedSchema, autoStyleId]`, hoisted above the renderer's early returns — its old
2236
+ use site sits after them, so a memo written there would have been a conditional hook.
2237
+ A genuinely changed `className`, interpolated value or breakpoint still produces a new
2238
+ identity, so nothing goes stale.
2239
+ - 5967be0: Extends objectui#5687's adapter-only `data.*` constant-predicate diagnostic — a node-gate
2240
+ predicate that evaluates perfectly, against the wrong object, because at the node tier
2241
+ `data` is the data-source adapter, not the row — to the `disabled` / `disabledOn` gate
2242
+ (objectui#6504, maintainer ruling 2026-08-27 option A).
2243
+
2244
+ A node written `{ "type": "button", "disabled": "data.status == 'locked'" }` evaluates
2245
+ cleanly (no fault, so objectui#6445's fault reporter correctly stays silent), and on the
2246
+ constant's other polarity (`data.locked == null`, `!data.assignee`, or an adapter that
2247
+ answers nothing) hands the gate a constant `true` that greys the control out on every row,
2248
+ in every build, with nothing on the console. This leg now names it, in development only —
2249
+ option C (always-on) was excluded, outside the #5687 precedent.
2250
+
2251
+ The copy is new, not reused: the visibility leg's sentence ("a constant `false` hides the
2252
+ node on every row") is written about the opposite polarity and would be false on this gate.
2253
+ The enablement leg's own sentence names the constant-`true` direction — the control renders
2254
+ DISABLED, greyed out, indistinguishable from a gate the author meant to close.
2255
+
2256
+ Both legs carry the same dissolution pointer: this diagnostic — visibility AND enablement
2257
+ together — dissolves when objectui#5330's `data.*` deprecation window closes.
2258
+
2259
+ Dev-only, no verdict change, no interpolation change, no published type widened (the new
2260
+ `AdapterOnlyPredicateGateKind` type and the new prefix constant are module-internal, not
2261
+ re-exported from the package entry — matching objectui#5687's own symbols, which never were
2262
+ either).
2263
+ - 12402a9: Drop the undeclared object-level `titleField` from the object page's five
2264
+ remaining view-config seams, and from the record-search memo key
2265
+
2266
+ `ObjectView` resolves a title field for seven view kinds. Two of them —
2267
+ calendar and gantt — already read `viewDef.<kind>?.titleField || 'name'`. The
2268
+ other five (timeline, kanban, map, gallery, tree) carried a three-rung chain
2269
+ with `objectDef.titleField` in the middle, so one file answered the same
2270
+ question two different ways. This converges the five on the shape the two
2271
+ siblings already had; it is a convergence, not a removal.
2272
+
2273
+ The middle rung could never fire for legal metadata. `@objectstack/spec`'s
2274
+ object schema is a `strictObject`, so
2275
+ `ObjectSchema.safeParse({ …, titleField: 'x' })` is rejected with
2276
+ `unrecognized_keys` — the same issue code a nonsense key gets — while
2277
+ `nameField`, `displayNameField` and `titleFormat` all parse (measured against
2278
+ `@objectstack/spec@17.2.0`, the dist this repo installs). objectui#6531
2279
+ established that measurement and dropped the twin read inside
2280
+ `getRecordDisplayName`. Reading a key no producer can ship is the
2281
+ consumer-side alias AGENTS.md Commandment #0.1 bans.
2282
+
2283
+ Behaviour for every legal config is unchanged, and both directions are pinned:
2284
+ a view that declares its own `titleField` still wins on every kind, a view that
2285
+ declares none still floors at `'name'`, and an object carrying the
2286
+ contract-rejected key is now honoured by no kind. Re-pointing the middle rung
2287
+ at the declared `nameField` was considered and rejected: it would have added a
2288
+ rung calendar and gantt do not have — increasing the divergence — and, unlike
2289
+ this change, it would have altered behaviour for legal configs.
2290
+
2291
+ `useRecordSearch`'s candidate signature — the memo key that decides when the
2292
+ cross-object fanout re-runs — appended `o?.titleField ?? ''` to every entry.
2293
+ Because no legal object definition can carry the key, that half was permanently
2294
+ `''`: a constant suffix in a cache signature, and the last thing in the repo
2295
+ that read as evidence some producer supplies it. The signature is now the object
2296
+ name alone, which is the only field of an object definition the effect actually
2297
+ consumes. Change detection is unaffected — a changed candidate name still
2298
+ re-runs the fanout, and a new array with identical content still does not.
2299
+ - 46b9bc9: A `props` config bag on a component-renderer node is now named at render
2300
+ instead of dropped in silence (objectui#6708).
2301
+
2302
+ `SchemaRenderer` HOISTS every `properties.*` value onto the node, so a key
2303
+ written under `properties` is a real value on `schema.<key>` by the time a
2304
+ renderer destructures it. `props` — the annotated legacy alias of the same bag
2305
+ — is NOT hoisted: it is evaluated and then spread as React props on the created
2306
+ element. A renderer declared as `({ schema })`, which is the normal shape for
2307
+ the component renderers, therefore never sees it. The `element:*` family is the
2308
+ exception: its `readProps()` merges `{ ...schema.props, ...schema.properties }`,
2309
+ so the same spelling is honoured there.
2310
+
2311
+ Every gate accepts the `props` spelling — `BaseSchema` is `.passthrough()` with
2312
+ `[key: string]: any` — and the docs call it a supported alias, so nothing
2313
+ between the author and the screen said a word. Re-measured on `faac0d935`
2314
+ through the real `SchemaRenderer` with a probe that records both channels:
2315
+
2316
+ | node | React prop `data` | `schema.data` |
2317
+ |---|---|---|
2318
+ | `props: { data: "${data.customers}" }` | the evaluated array | absent |
2319
+ | `properties: { data: "${data.customers}" }` | the evaluated array | the array |
2320
+
2321
+ Same key, same value, one envelope apart. The expression is evaluated on both
2322
+ legs, so this is a dropped value rather than an unevaluated one. Read through a
2323
+ real `data-table` (objectui#6665's four-leg pin) the same pair renders
2324
+ `No results found` against the two rows.
2325
+
2326
+ The diagnostic's level and dedupe were chosen from a census, which the ruling
2327
+ made a precondition. Every JSON document, every `json` fence in every
2328
+ `.md`/`.mdx`, and every TypeScript object literal in the repo was walked for
2329
+ nodes carrying both `type` and `props`: 39 such nodes, 22 of them on
2330
+ component-renderer types, and 19 of those 22 are test fixtures exercising this
2331
+ shape on purpose. The authored, non-test corpus holds 5 — three of which are
2332
+ deliberate counter-examples in the skills guides. Nothing floods, so the level
2333
+ is not softened for volume; the dedupe is keyed on the MESSAGE rather than on
2334
+ the schema object, so a metadata generator emitting one wrong envelope across
2335
+ many nodes still gets one line while two genuinely different nodes get two.
2336
+
2337
+ `console.warn`, matching objectui#6575 and objectui#6665 — the two prior
2338
+ instances of this exact "you declared something and the renderer dropped it"
2339
+ shape — rather than the `console.error` its neighbour at this tier uses for a
2340
+ raw `${...}` placed verbatim in front of a user. Nothing is placed here; a
2341
+ value is dropped.
2342
+
2343
+ No behaviour change, which is the entire reason this arm was chosen. Hoisting
2344
+ `props` to parity with `properties` was refused at ruling: it would weld the
2345
+ legacy alias in as a permanent second spelling, against this repo's
2346
+ alias-retirement direction. Refusing the key at parse stays blocked on the
2347
+ `.passthrough()` ceiling (objectui#5155 / objectui#6269). What every renderer
2348
+ receives is pinned byte-for-byte against a reading captured on the tree before
2349
+ the diagnostic existed. Nothing is added to the published surface either — the
2350
+ predicate, message builder, prefix constant and test-only reset are
2351
+ module-internal and are not re-exported from the package entry, matching
2352
+ objectui#6575's own symbols. The trap stops being silent; it does not stop
2353
+ being a trap.
2354
+ - d47de51: `useSchemaPersistence` no longer writes its adapter ref during render
2355
+ (objectui#6745).
2356
+
2357
+ The hook keeps the live adapter in a ref so `save`/`load`/`list`/`remove` can be
2358
+ created once and still reach the newest adapter at call time. That ref was
2359
+ written in the render body:
2360
+
2361
+ ```
2362
+ const defaultAdapter = useRef(createLocalStorageAdapter());
2363
+ const adapterRef = useRef(adapter ?? defaultAdapter.current);
2364
+ adapterRef.current = adapter ?? defaultAdapter.current; // during render
2365
+ ```
2366
+
2367
+ which `react-hooks/refs` flags on three counts. A render React discards or
2368
+ replays — StrictMode, a Suspense retry, a concurrent interruption — still
2369
+ performed that write, so a save could be routed through an adapter belonging to
2370
+ a render that never committed.
2371
+
2372
+ The write now happens in `useInsertionEffect`, and the default adapter comes
2373
+ from `useMemo` instead of a ref read during render.
2374
+
2375
+ **Timing is preserved for every legal call site.** Insertion effects run in the
2376
+ mutation phase — before every layout effect in the tree, before paint, and
2377
+ before any event handler can fire — so a changed `adapter` prop is in place
2378
+ before anything that may legally invoke these methods can observe it. This is
2379
+ deliberately *not* `useEffect` (which lands after paint) or `useLayoutEffect`
2380
+ (a child's layout effects run before its parent's); either would route a call
2381
+ made earlier in the same commit to the previous adapter. The only window that
2382
+ changed is a read during the render phase itself, which no legal consumer has:
2383
+ `save`/`load`/`list`/`remove` are side effects and are never callable during
2384
+ render.
2385
+
2386
+ Also fixed in passing, on the same lines: `useRef(createLocalStorageAdapter())`
2387
+ invoked the factory on **every** render and discarded all but the first result.
2388
+ The `useMemo` runs it once. The adapter is a stateless facade over
2389
+ `localStorage` and its identity is never exposed, so this is unobservable
2390
+ beyond the saved work.
2391
+
2392
+ No API, signature or observable behaviour change for any supported call site.
2393
+ - 3fe6463: `SchemaRenderer`: a non-object `props` bag is no longer object-spread into
2394
+ indexed React props (objectui#6752).
2395
+
2396
+ A node written `{ type: 'card', props: 'not-a-bag' }` reached `createElement`
2397
+ carrying nine React props named `0` through `8`, one per character, because
2398
+ `{ ...'not-a-bag' }` enumerates a string's character indices. Nothing threw and
2399
+ nothing was logged, so the symptom — a component handed nine props it never
2400
+ declared — sat a long way from the `props` value that caused it. Measured
2401
+ through the real `SchemaRenderer`; the in-repo corpus has no such node today, so
2402
+ this is a latent shape rather than a live failure.
2403
+
2404
+ The canonical `properties` branch already carried the wider guard. Its comment
2405
+ claimed the guard was hoist-specific, and that turned out not to survive
2406
+ measurement: ablating it leaves the indexed keys the hoist puts on the node
2407
+ completely unchanged, and moves only whether `schema.properties` still holds the
2408
+ value the author wrote. The reason is channel-independent — a degenerate value
2409
+ must not have its shape reinterpreted by an object spread — so `props` now
2410
+ carries the same guard, both bags share one `isConfigBag` predicate, and the
2411
+ `properties` comment states the measured reason instead of the old one.
2412
+
2413
+ Both sites that spread the bag are covered: the evaluation memo (so
2414
+ `schema.props` keeps the authored value) and `propsWithoutCanonicalKeys` (so the
2415
+ `createElement` spread does not re-enumerate it). A degenerate bag now
2416
+ contributes no keys, and the authored value is passed through unmangled on the
2417
+ React prop named `props`.
2418
+
2419
+ Unchanged: a normal object `props` (still evaluated, still spread per key),
2420
+ objectui#5123's two-bag precedence, the `properties` hoist, and objectui#6708's
2421
+ dropped-`props` diagnostic.
2422
+ - 31ab372: A degenerate `properties` bag no longer reaches the element as indexed React
2423
+ props (objectui#6760).
2424
+
2425
+ `properties` is the spec spelling of a node's config bag. The hoist that copies
2426
+ `properties.*` onto the node's top level walked it with `Object.entries`
2427
+ unconditionally, so a non-object value was enumerated rather than skipped:
2428
+ measured on `c6732825d`, `{ type, properties: 'not-a-bag' }` reached the element
2429
+ as nine React props named `0` … `8`, and `properties: ['x', 'y']` as `0`, `1`.
2430
+ Nobody authored those keys — they are the walk's reading of a string's character
2431
+ indices.
2432
+
2433
+ The hoist now asks the same `isConfigBag` question the evaluation memo and the
2434
+ `props` bag already ask (objectui#6752, objectui#6761). Of the two arms the card
2435
+ left open, this is "guard the hoist" rather than "declare that the hoist may
2436
+ enumerate anything", because objectui#5123 ruled that a key gets one answer
2437
+ whichever channel reads it — and the alternative would have answered one
2438
+ authored mistake two ways, with the reinterpreting half falling on the canonical
2439
+ spelling while the quiet half fell on its legacy alias.
2440
+
2441
+ Nothing else moves: a real object bag hoists exactly as before (including the
2442
+ `type`/`id` keys the hoist has always refused to copy), the authored
2443
+ `properties` value still reaches renderers on both channels unchanged, and
2444
+ `properties: 42` / `properties: true` were already contributing no keys.
2445
+ - 67fc3b0: `useETagCache`, `useGlobalUndo` and `useOffline` stop writing their config refs
2446
+ during render (objectui#6797).
2447
+
2448
+ Each of the three kept a "latest value" ref that was assigned in the render
2449
+ body, one `react-hooks/refs` warning apiece on this base
2450
+ (`useETagCache.ts:204`, `useGlobalUndo.ts:57`, `useOffline.ts:262` — all three
2451
+ `Cannot update ref during render`, all three the WRITE only; unlike
2452
+ `useSchemaPersistence` none of them also READ a ref during render). A ref
2453
+ written in the render body is also written by renders React discards or
2454
+ replays — StrictMode's double render, a Suspense retry, a concurrent
2455
+ interruption — so a tree that never committed could publish its config to
2456
+ callbacks that outlive it.
2457
+
2458
+ The write moved to `useInsertionEffect` in all three, but that shape was chosen
2459
+ per hook rather than carried over, because what each ref protects differs:
2460
+
2461
+ - **`useETagCache`** — five resolved config scalars read by five `useCallback`s
2462
+ with `[]` deps whose identity is part of the published result. Re-keying them
2463
+ on the config values would have changed `fetchWithETag`'s identity whenever a
2464
+ caller's `ttl` moved, re-firing consumer effects keyed on it, so the ref
2465
+ stays.
2466
+ - **`useGlobalUndo`** — the whole options bag. Every caller passes a fresh
2467
+ inline literal with inline `onUndo` / `onRedo` closures, and the keydown
2468
+ effect is keyed on `undo` / `redo`, so the ref is the only thing keeping
2469
+ those two stable while still reaching the newest callbacks.
2470
+ - **`useOffline`** — `config.sync`, read by one caller (`sync`) that is
2471
+ *already* unstable (deps `[enabled, queue]`). Here the ref protects RETAINED
2472
+ closures rather than an identity: the auto-sync effect deliberately captures
2473
+ a `sync` and fires it 100ms later, and that closure must still see the newest
2474
+ `batchSize`. Dropping the ref for a `syncConfig?.batchSize` dep would have
2475
+ changed what that retained closure reads, so it was rejected.
2476
+
2477
+ `useInsertionEffect` runs in the mutation phase — ahead of every layout effect,
2478
+ ref attachment and paint — so the only window any of the three defers is the
2479
+ render phase itself, where none of the affected callbacks is legally callable.
2480
+ `useEffectEvent` would be the idiomatic answer but is React 19.2+, and this
2481
+ package's peer range starts at React 18.
2482
+
2483
+ **No behavioural change is claimed for callers that exist today**: reverting any
2484
+ of the three implementations leaves the whole suite green, and the new pins pass
2485
+ against the old code and the new code alike. They guard the next edit — each
2486
+ file's discriminating pin fails under both `useEffect` and `useLayoutEffect`.
2487
+ - 33a3b3c: `useETagCache` builds its config object once per hook instance instead of once
2488
+ per render (objectui#6817).
2489
+
2490
+ `useRef({ enabled, storage, storagePrefix, maxEntries, ttl })` evaluated that
2491
+ literal on **every** render and kept only the first result, so every later
2492
+ render allocated a five-key object that was discarded. It now comes from a
2493
+ `useMemo` keyed on the five values, which is also what the ref's
2494
+ `useInsertionEffect` write publishes.
2495
+
2496
+ `patch`, not `minor`: nothing a published consumer can observe changes. The
2497
+ public shape, the returned callbacks' identities and the values the stable
2498
+ `[]`-deps callbacks read off the ref are all unchanged — the object's identity
2499
+ is private to the hook, so the only difference is the allocation that no longer
2500
+ happens. Same pattern PR objectui#6796 repaired in `useSchemaPersistence`; this
2501
+ is the half of that class the `react-hooks/refs` rule structurally cannot see,
2502
+ which is why it needed a test rather than a lint fix.
2503
+ - 3ed3eec: fix(plugin-form,plugin-list,plugin-view,react): read `SchemaRendererContext` as declared, not through a cast to `any` (objectui#7209)
2504
+
2505
+ Six reads of the renderer context erased its type. Five cast at the read —
2506
+ `useContext(SchemaRendererContext as React.Context<any>)` in the
2507
+ `embeddable-form` and `object-master-detail-form` bridges, in `ListViewBlock`
2508
+ and in `useResolvedDataSource`, and the same cast spelled with a bare
2509
+ `Context<any>` in `useElementDataSource` — and the `object-view` renderer's
2510
+ module re-declared the imported context as a `React.Context<any>`. Through
2511
+ that cast a read of a member the context does not declare compiled clean,
2512
+ which is how the phantom `ctx.formValues ?? ctx.data` channel retired by
2513
+ objectui#7206 went unnoticed. Each of these reads now sees `SchemaRendererContextType` as
2514
+ `@object-ui/react` declares it, so such a read is a compile error on the day it
2515
+ is written.
2516
+
2517
+ No runtime behaviour changes. Removing the casts surfaced no read of an
2518
+ undeclared member; it surfaced two places where the declared `null` ("no
2519
+ adapter bound") met a prop that does not admit it:
2520
+
2521
+ - `embeddable-form` handed that `null` to `EmbeddableForm`'s optional
2522
+ `dataSource`. It now collapses it to `undefined`, as
2523
+ `object-master-detail-form` already did; `EmbeddableForm` only ever tests the
2524
+ adapter for truthiness, so the two absences behave the same.
2525
+ - `object-view` / `view` hand it to `ObjectViewProps.dataSource`, which is
2526
+ declared required and stays so (objectui#7842). That one value now carries a
2527
+ narrow, commented assertion instead of the whole context being erased; the
2528
+ value passed is the same as before.
2529
+
2530
+ The published signatures of `useResolvedDataSource` and `useElementDataSource`
2531
+ do not move.
2532
+ - 2ce2612: A record-page URL now names the object the clicked rows actually came from, in
2533
+ `ObjectTree` and `ObjectCalendar`.
2534
+
2535
+ `useNavigationOverlay` builds `/{objectName}/record/{id}` out of whatever it is handed,
2536
+ and both components handed it the bare top-level `schema.objectName` while resolving
2537
+ their own rows through the ruled record-source ladder (`data`, then
2538
+ `staticData`, then `objectName`). `77cb489b4` published `objectName` as that ladder's
2539
+ THIRD RUNG and not as a parallel "page object" concept, so a block has exactly one
2540
+ record source — and a row fetched through `data.object` whose click built
2541
+ `/{schema.objectName}/record/{id}` named a record that the URL's own object does not
2542
+ contain.
2543
+
2544
+ Two shapes change, both toward the object the rows came from:
2545
+
2546
+ - a block carrying **both** bindings navigated to the top-level key and now navigates to
2547
+ `data.object`;
2548
+ - a **data-only** block had no name to build a URL from at all, so the hook took its
2549
+ `/{id}` leg — an unrouted path that paints a blank page — and now builds the routed
2550
+ record URL.
2551
+
2552
+ `ObjectCalendar` is where the divergence was plainest: on one click it resolved the
2553
+ detail drawer through the ladder and the navigation URL through the top-level key. The
2554
+ URL now reuses the very `schemaObjectName` that already keys the calendar's record query
2555
+ and its `$expand` derivation, so query, drawer and URL agree by construction.
2556
+
2557
+ **Nothing else moves.** Both converted sites keep a site-local `?? schema.objectName`
2558
+ tail for the off-contract `data: { provider: 'object' }` that carries no `object`
2559
+ (`ViewDataSchema` declares it required) — the same tail `ObjectTree`'s `headerObjectName`
2560
+ already carries, and the same conservatism `b041b9c0c` applied when it published the
2561
+ shared reader. `useNavigationOverlay`'s own signature is unchanged: it still takes an
2562
+ `objectName`, and only what callers hand it has changed.
2563
+
2564
+ The hook's `@example` stops prescribing `objectName: schema.objectName`. That prose is
2565
+ why there were copies to convert at all — component authors copied the divergence out of
2566
+ the documentation, correctly, as written — so it now points at
2567
+ `resolveRecordSourceObjectName` and says explicitly that a caller with no data config
2568
+ has nothing above rung three and should keep passing `schema.objectName`.
2569
+
2570
+ `ObjectKanban` is deliberately **not** converted: it has no data config, no
2571
+ `getDataConfig`, and its `data` is a raw row array rather than a `ViewData` binding, so
2572
+ `schema.objectName` already IS its record source and its board, drawer and URL already
2573
+ agree.
2574
+ - d1bebb0: The unevaluated-expression diagnostic now lists `properties` among the channels
2575
+ that evaluate and read back, matching the sibling `props`-bag diagnostic that
2576
+ tells authors to write their keys there.
2577
+
2578
+ Both messages are dev-build diagnostics in `SchemaRenderer`, and one node can
2579
+ trip both. They disagreed: `propsBagDiagnostic` said *"`props` is NOT hoisted
2580
+ onto the node — only `properties.*` is … Write them under `properties`
2581
+ instead"*, while `unevaluatedExpression` enumerated *"channels that do evaluate
2582
+ and read back today"* as `content` or host-side resolution — omitting
2583
+ `properties`. An author who hit both was told to use a channel the other message
2584
+ said did not work.
2585
+
2586
+ The enumeration was the wrong half, established by measurement rather than by
2587
+ reading the `COMPAT` label on the hoist. `properties` has no retirement on
2588
+ record: `@objectstack/spec@17.2.0` calls the vocabulary carried there *"ALIVE —
2589
+ this is not dead surface to retire under ADR-0049"*, keeps
2590
+ `PageComponentSchema.properties` as the open carrier on purpose, gates it at the
2591
+ authoring door, and tombstones no part of it via `retiredKey()`. In this repo
2592
+ `props`, not `properties`, is the spelling annotated as the legacy alias.
2593
+
2594
+ Diagnostic text only — no evaluation, hoist or schema behaviour changed.
2595
+ - 7fb22a1: The published visibility diagnostic no longer tells an author that `app` is a bound
2596
+ expression-scope root (objectui#8155 follow-up).
2597
+
2598
+ `@object-ui/react`'s `SCOPE_TIER_ADVICE['app-shell']` — the paragraph
2599
+ `reportUnresolvableVisibilityPredicate` prints in production when a predicate cannot be
2600
+ evaluated — read "App-shell predicates bind `current_user` … plus `app` and `features`".
2601
+ objectui#8155 removed `app` from `buildExpressionScope`, so that sentence was printed at
2602
+ exactly the moment a saved `app.*` predicate faulted, and it answered "why did my
2603
+ predicate not resolve?" by naming the root that is the reason. The line now names the
2604
+ five roots the provider really binds: `current_user`, its `user` / `ctx.user` / `os.user`
2605
+ aliases, and `features`.
2606
+
2607
+ **Swept as a class, not as two coordinates.** Every other place in this tree that stated
2608
+ `app` was a bound expression-scope root is corrected in the same change — the diagnostic
2609
+ copy and its byte-pin, the ambient-scope docblocks in `@object-ui/react`
2610
+ (`SchemaRenderer`, `useExpression`), `@object-ui/core` (`ActionRunner.ParamDef.visible`,
2611
+ `RowPredicateOptions.scope`), `@object-ui/components` (`form.tsx`, `containers.tsx`),
2612
+ `@object-ui/plugin-detail`, `@object-ui/plugin-form` (docblock and README),
2613
+ `@object-ui/app-shell` and the console app, plus fourteen test fixtures that transcribed
2614
+ the old bag with an `app` key. The fixtures in `@object-ui/app-shell` now call
2615
+ `buildExpressionScope` instead of transcribing it, so that pair cannot drift again.
2616
+
2617
+ Nothing that was true before objectui#8155 changed: the `app` prop on
2618
+ `ExpressionProvider` and the `app` field on its React context value are untouched (they
2619
+ were never CEL roots), and no root other than `app` was added to or removed from any
2620
+ message, bag or fixture.
2621
+
2622
+ Two release-bound corrections travel with it. The producer-side card objectstack#16420
2623
+ was closed `not_planned` on 2026-09-07 by the same ruling that removed the root, not left
2624
+ open; and the consequence of a stale `app.*` predicate is **not** uniformly "fails open" —
2625
+ it is per surface, and the changeset that carries the removal now states the seven
2626
+ measured directions instead of one.
2627
+ - ad66d79: Stop binding an ambient `data` root on record surfaces, so a `data.*` predicate faults
2628
+ loudly instead of resolving against the host's bag (objectui#8166, ruled 2026-09-10).
2629
+
2630
+ **User-visible behaviour change, at RUNTIME and at DEBUG time — not at authoring time.**
2631
+ Nothing an author sees while writing a predicate moves: the authoring lint's accept set
2632
+ lives in `@objectstack/formula`'s `SCOPE_ROOTS`, which still contains `data`, and
2633
+ `data.status == 'x'` still lints clean at `scope: 'record'`. Splitting that list per
2634
+ scope is the producer-side half and is not this repo's to make. What changes is what
2635
+ the runtime does with such a predicate once it is saved.
2636
+
2637
+ **What was wrong.** `83fe6e741` (Phase 2 of the objectui#5330 canon) retired `data.*`
2638
+ on runtime record surfaces — the row is bound as `record.*` and nothing else. But
2639
+ `@object-ui/app-shell`'s `buildExpressionScope` kept binding an ambient `data`, so a
2640
+ predicate the linter had waved through also *resolved* at runtime, against that bag
2641
+ rather than against the row. Measured on `main` before this change, one authored
2642
+ `visibleWhen: "data.status == 'x'"` meant three different things:
2643
+
2644
+ - on every `ExpressionProvider` mount and on `RecordFormPage`'s own evaluator, the
2645
+ ambient `data` was `{}`, so the engine answered `[runtime] No such key: status` — a
2646
+ fault, warned once, and the field-rule fallback applied (fail-open for `visibleWhen`);
2647
+ - on `AppContent`'s field-list evaluator for the global record-form modal in EDIT mode
2648
+ the ambient `data` was **the record being edited**, so the predicate resolved, with no
2649
+ diagnostic at all, off the wrong layer's object — an author testing there would have
2650
+ seen it "work";
2651
+ - in CREATE mode on that same modal it fell back to the first case.
2652
+
2653
+ **What changes.** `buildExpressionScope` no longer accepts or binds `data`, and the two
2654
+ imperative call sites (`AppContent`, `RecordFormPage`) stop passing one. A `data.*`
2655
+ predicate on any app-shell surface now produces the engine's own verdict — `[type]
2656
+ Unknown variable: data` — on both diagnostic channels (the one-time `console.warn` and
2657
+ the `onFault` passback), which is the same verdict the server gives the same string and
2658
+ the same shape `app` has produced since objectui#8155.
2659
+
2660
+ **The fault is loud, not fatal.** The verdict a faulting predicate resolves to is
2661
+ unchanged: `visibleWhen` still fails OPEN, `readonlyWhen` / `requiredWhen` still fail
2662
+ permissive (`@object-ui/core`'s `fieldRules.ts`; the direction is objectui#8069's open
2663
+ question, not this change's). So a record form renders exactly as before except that the
2664
+ console now names the root. Nothing throws.
2665
+
2666
+ **Migration.** Rewrite `data.foo` as `record.foo` on any runtime record surface — the
2667
+ canonical spelling since `83fe6e741`, and the only one that reaches the row. The
2668
+ metadata-admin designer is unaffected: its `data` is the DRAFT under edit (ADR-0089 D3,
2669
+ `CANONICAL_ROOT_BY_LAYER` = `{ runtime: 'record', metadata: 'data' }`), bound by
2670
+ `views/metadata-admin/predicate.ts` through its own builder, which takes only the
2671
+ identity roots from this bag and assigns its own `data` last.
2672
+
2673
+ `@object-ui/react` carries a docblock correction only: the `app-shell` tier paragraph in
2674
+ `utils/visibilityDiagnostic.ts` described the bag this change edits. `@object-ui/components`
2675
+ carries no runtime change at all — three form/predicate tests hand-transcribe the app-shell
2676
+ bag (importing it would invert the package dependency) and each literal carried the removed
2677
+ `data: {}`; the transcriptions are corrected so they cannot go on describing a scope that no
2678
+ longer exists.
2679
+ - e36acd4: `SchemaRenderer` no longer hands an authored `objectFields` to any component as
2680
+ a React prop (objectui#8818, completing maintainer decision batch #70,
2681
+ objectui#7742).
2682
+
2683
+ `objectFields` is the object's field catalogue. The predicate layer reads it to
2684
+ decide how a conditional-formatting rule compares a relation field. Batch #70
2685
+ ruled it a runtime prop that a host injects, never authorable metadata, but the
2686
+ renderer's fixed metadata strip list did not name it. So a node that authored
2687
+ the key had it spread onto whatever component its type resolved to. It now
2688
+ joins that strip list, and all three authoring spellings are dropped: a
2689
+ top-level `objectFields`, `properties: { objectFields }`, and the legacy
2690
+ `props: { objectFields }` alias, which is spread separately and is stripped
2691
+ too.
2692
+
2693
+ Runtime hosts are unaffected. Pass the catalogue as a React prop, either on
2694
+ the component directly (as `ObjectKanban` does for the board it mounts) or as
2695
+ an `objectFields` prop on `SchemaRenderer` itself. `SchemaRenderer` spreads its
2696
+ own props last, so a host prop still arrives. No schema face ever declared the
2697
+ key, so no published type or validator changes.
2698
+ - b9d47ec: Resolve filter placeholders on the drill "escape hatch", so one drill has one scope
2699
+ whatever `drillDown.target` says (objectui#9022).
2700
+
2701
+ Every widget composes its drill filter from the RAW authored filter, so
2702
+ `{current_user_id}`, `{current_org_id}` and the relative-date macros were still
2703
+ literals when they reached the host's `openRecordList`. The in-place drawer arm never
2704
+ had the defect — the `object-data-table` it renders resolves the filter in its own
2705
+ fetch — so only the navigate arm, and the "Open in list →" button in the drawer header,
2706
+ wrote the placeholder into the URL. `filter[close_date][gte]={current_quarter_start}`
2707
+ parses back on the read side as an ordinary string comparand, so the destination list
2708
+ matched nothing, silently, and in disagreement with the chart bar the user had just
2709
+ clicked (that chart scopes its own bars by the RESOLVED value).
2710
+
2711
+ `useOpenRecordList` now expands both placeholder vocabularies against the session scope
2712
+ it already sits inside, immediately before serialization. It is the single host handler
2713
+ every escape hatch funnels through — the chart's navigate arm and its own header
2714
+ button, `DrillDownDrawer`'s navigate arm, and `OpenInListButton` — so both widget
2715
+ families are covered by one resolution rather than N widget seams. A drill carrying no
2716
+ placeholder produces a byte-identical URL: both vocabularies substitute whole-string
2717
+ `{token}` values only.
2718
+
2719
+ The `DrillNavigationValue.openRecordList` contract now states that resolving is the
2720
+ host implementation's job, since an out-of-repo host wiring the same context would
2721
+ otherwise reproduce the split.
2722
+ - efc1c9c: **A node-gate predicate parked under `props` is now REFUSED BY NAME on the console, instead of gating nothing in silence (objectui#9108).**
2723
+
2724
+ A node may spell its config bag `properties` (the spec spelling) or `props` (the annotated legacy alias). `SchemaRenderer` hoists `properties.*` onto the node; nothing copies `props.*`, and both node gates read the post-hoist node — so a predicate that arrived under the alias was never one of the keys either gate could see. Measured at node level, four rows: `props: { visible: false }` and `props: { hidden: true }` both RENDERED, while `properties: { visible: false }` and `properties: { hidden: true }` each hid correctly. Fail-**open** and silent by construction: a gate that never bit renders exactly like a gate that said yes, so no user and no screenshot can find it.
2725
+
2726
+ **No verdict moves, and that is the ruled outcome rather than a limitation.** The alias is refused, ⛔ not honoured: nothing is hoisted, `schema.<KEY>` stays undefined for a renderer declared as `({ schema })`, and every element receives the byte-identical props bag it received before. What changes is that the eight node-gate predicate keys (`visibleWhen` / `visible` / `visibleOn` / `visibility` / `hidden` / `hiddenOn` / `disabled` / `disabledOn`) are named on the console when they are parked there, with the migration that fixes them. The maintainer ruling of 2026-09-13 closed the honour arm (PR objectui#9144) on the ground that honouring the alias would have made *"8 predicate keys work while the rest stayed silently dropped — and partly working is harder to learn from than not working"*.
2727
+
2728
+ The line is `console.error`, in development **and in production** — the same posture objectui#6038 gave the unresolvable-predicate report, because a node gate that has stopped biting in production is exactly the defect that must not be able to sit live and undiscovered. It is rate-limited to one line per distinct authoring bug for the lifetime of the page. One measured carve-out: `disabled` on a bag-reading `element:*` node is honoured by that renderer itself (`disabled={props.disabled || running}`), so it is not refused there.
2729
+ - b234a84: `ElementDataSourceGate`: a row cap the contract refuses is no longer counted as
2730
+ "the component authored one" (objectui#9899).
2731
+
2732
+ The shared gate decides, per key, whether a component's own value should suppress
2733
+ the one a bound saved view supplies. For `columns` it has always decided that by
2734
+ CONTENT — an authored-but-empty list yields, because "supplying the columns is the
2735
+ reason a view was named". For the row cap it decided by PRESENCE, so a value
2736
+ `@objectstack/spec` refuses — `pagination.pageSize: 0`, a negative, a fraction —
2737
+ counted as authorship and suppressed the view's legitimate cap.
2738
+
2739
+ **What an author saw.** Measured end to end on `list-view` rather than inferred: a
2740
+ component carrying `pagination.pageSize: 0` under a view supplying `7` put
2741
+ `$top: 100` on the wire — the renderer's own default, because that renderer already
2742
+ refuses a non-positive page size. So the view's cap never reached the query and the
2743
+ read was WIDER than the view asked for, in the one direction a named view exists to
2744
+ prevent. The same branch feeds the flat `limit` key that `object-kanban`,
2745
+ `object-timeline`, `record:related_list` and the form panels read.
2746
+
2747
+ After this change the view's cap is written, and the same fixture puts `$top: 7` on
2748
+ the wire.
2749
+
2750
+ **The diagnostic moved with the decision.** Before this change the consuming
2751
+ renderer was the one telling the author their `0` had been dropped. Now that the
2752
+ gate substitutes a usable cap, that renderer correctly sees nothing to report — so
2753
+ the gate states it instead, once per declaration, on the same `console.warn` channel
2754
+ the renderers use. It names the refused value, the key it was written on, and the
2755
+ cap used in its place. Exactly one message is emitted before and after; this is not
2756
+ a silent substitution, and it is not a clamp — the author's number is refused, not
2757
+ repaired.
2758
+
2759
+ **Unchanged:** a legitimate authored cap still wins over the view's, and an explicit
2760
+ cap on the `dataSource` binding still wins over both.
2761
+ - befd40c: fix(react): the refused-`data` dev warning can be reset between tests
2762
+
2763
+ The dedupe `Set` behind the objectui#9571 / objectui#9758 diagnostic — "the
2764
+ authored `data` key was NOT passed to the component as a React prop" — lived in
2765
+ `SchemaRenderer.tsx` with nothing able to clear it, while every other dev
2766
+ diagnostic in this package's `utils/` directory pairs its `Set` with an exported
2767
+ test-only reset. `propsBagDiagnostic.ts` states the consequence in its own
2768
+ reset's docblock: the second test to assert the same warning reads the first
2769
+ test's dedupe entry and sees silence, "a green run that checked nothing". The
2770
+ absence direction is the dangerous one — a test asserting this warning did NOT
2771
+ fire for a `type#id` an earlier test already warned for is green whatever the
2772
+ renderer does, and it reports that green as coverage.
2773
+
2774
+ The `Set`, the emit and a new `__resetRefusedDataPropWarnings` now live in
2775
+ `utils/refusedDataPropDiagnostic.ts`, beside the siblings whose idiom they
2776
+ follow. `index.ts` re-exports `SchemaRenderer.js` with a star, so a reset
2777
+ exported from where the `Set` used to sit would have become a member of this
2778
+ package's published entry for the sake of a test affordance; the entry is
2779
+ unchanged by this change, and so are the warning's text, its dedupe key and both
2780
+ `__DEV__`-gated call sites.
2781
+
2782
+ The pin renders one `type#id` across three `it()` blocks — the warning fires,
2783
+ fires again in the next block, then draws silence for host-supplied rows beside
2784
+ a control that proves the channel could still speak. Ablating the reset's body
2785
+ turns the last two red; the absence assertion alone stays green under it, which
2786
+ is the defect it exists to refuse.
2787
+ - 98c3a74: Fix `createLazyPlugin`'s JSDoc example, which taught a call that does not compile.
2788
+
2789
+ The `@example` block passed `() => import('@object-ui/plugin-grid')` as the
2790
+ `importFn`. ObjectUI plugin packages export their components by name and have no
2791
+ `default`, so that call supplies the module namespace object — rejected by the
2792
+ compiler (`Property 'default' is missing`) and, at runtime, handed to
2793
+ `React.lazy` as the component. The examples now unwrap the named export via the
2794
+ `async` spelling, which is the form that actually type-checks. This ships in the
2795
+ published `.d.ts`, so it is what editors show on hover.
2796
+ - 9850c6e: `SchemaRenderer`'s node visibility gate now emits a dev-only warning when a
2797
+ predicate reads `data.*` and the data-source adapter cannot answer that read, so
2798
+ an author who wrote `properties: { visible: "data.status == 'draft'" }` sees the
2799
+ constant they authored instead of shipping it (objectui#5687).
2800
+
2801
+ At the node tier `data` is the data-source **adapter** — the object `${data.total}`
2802
+ in a props bag resolves against — and it has never been the row. A predicate
2803
+ written with the deprecated `data.*` spelling therefore resolves `undefined ==
2804
+ 'draft'`, which is a perfectly good `false`: the block is hidden on every row, and
2805
+ because it does not throw, the unresolvable-predicate reporter added for
2806
+ objectui#5454 never fired. Measured on this base, the same predicate written as a
2807
+ `{ dialect: 'cel' }` envelope *does* throw and *was* already reported — so whether
2808
+ an author heard about the identical mistake depended on which dialect they happened
2809
+ to write it in, which is the arbitrariness objectui#5454 existed to remove.
2810
+
2811
+ **No verdict changes and no interpolation changes** (maintainer ruling,
2812
+ 2026-08-22, option A). The node tier keeps its documented `data` = adapter
2813
+ semantics; objectui#5330's row binding does not extend here. The evaluator's answer
2814
+ is returned exactly as computed and `${data.*}` interpolation is untouched — only
2815
+ the silence moved.
2816
+
2817
+ The report fires on a `data.*` read the bound adapter answers with `undefined`, not
2818
+ on the spelling. A genuine adapter read stays silent (`data.total > 0` against an
2819
+ adapter carrying `total`), a canonical `record.*` predicate stays silent, and a
2820
+ correctly-hiding gate stays silent. Dev builds only, `console.warn`, deduped per
2821
+ node type + key + predicate source — the same module, Set and lifecycle as the
2822
+ objectui#5454 reporter.
2823
+
2824
+ This loudness is temporary by design: it dissolves when objectui#5330's deprecation
2825
+ window for the `data.*` row spelling closes.
2826
+ - b2ea297: `ObjectSchemaMetadata` is now derived from `@objectstack/spec`'s `ServiceObject`
2827
+ instead of being a hand-written copy (objectui#5362; maintainer ruling
2828
+ 2026-08-20: the object document type belongs to the spec).
2829
+
2830
+ What changes on the published type surface:
2831
+
2832
+ - **Gained:** the full spec object-document surface, including the three keys
2833
+ the runtime already reads but the old interface rejected as excess
2834
+ properties: `icon`, `titleFormat`, `listViews` (plus `pluralLabel`,
2835
+ `nameField`, `displayNameField`, `managedBy` as a spec key, and the rest of
2836
+ the spec document).
2837
+ - **Removed:** nine members the old interface declared that no objectui
2838
+ runtime code reads and the spec document does not know: `extends`,
2839
+ `triggers`, `primary_key`, `relationships`, `name_field` (the spec spelling
2840
+ is `nameField`), `soft_delete`, `audit_trail`, `version`, `cache`.
2841
+ `ObjectTrigger` and `ObjectRelationship` remain exported unchanged.
2842
+ - **Kept:** `editMode` — the one measured client-side member the runtime reads
2843
+ (`recordFormNavigation` / `AppContent`) — now declared on the new
2844
+ `ObjectSchemaClientExtensions` interface, which the derivation intersects.
2845
+ Note the spec's strict parse rejects `editMode` on published documents
2846
+ (`unrecognized_keys`); it is a client-type member only.
2847
+
2848
+ Spelling settlement: `listViews` (camelCase) is canonical — `list_views`
2849
+ appears nowhere in `@objectstack/spec` 17.2.0. Runtime read sites in
2850
+ `@object-ui/app-shell` and `@object-ui/react` keep a documented snake-spelling
2851
+ READ fallback for stored pre-settlement documents (that stock has never been
2852
+ censused — objectstack#7917); the CRUD guide and its pinned transcription now
2853
+ author the canonical spelling.
2854
+ - 4f14ad7: `SchemaRenderer`'s node visibility gate now also catches the `${…}`-templated
2855
+ spelling of the objectui#5454/#5687 diagnostics when it is written inside
2856
+ `properties`, e.g. `properties: { visible: "${data.status == 'draft'}" }`
2857
+ (objectui#5756).
2858
+
2859
+ The `properties.*` evaluation loop runs, and interpolates every `${…}` template it
2860
+ finds, **before** the visibility gate ever sees the value — so by the time the
2861
+ existing diagnostics ran, a template-spelled predicate had already collapsed into a
2862
+ plain boolean and there was no predicate text left to inspect. The bare-string
2863
+ spelling of the exact same gate (`properties: { visible: "data.status == 'draft'"
2864
+ }`) was unaffected — nothing interpolates a string with no `${` in it — and was
2865
+ already reported; only the template spelling was structurally invisible.
2866
+
2867
+ Reached by moving the diagnostic check to inside the `properties` evaluation loop,
2868
+ on the predicate's raw (pre-interpolation) text, gated on the key being one of the
2869
+ six visibility keys the render chain consults (`visibleWhen` / `visible` /
2870
+ `visibleOn` / `visibility` / `hidden` / `hiddenOn`) — a `properties.content`
2871
+ interpolation, or any other non-visibility key, is untouched and stays silent.
2872
+
2873
+ Reports only the key that actually **decides** the node's visibility, mirroring
2874
+ objectui#5454's own leg semantics (its reporter is likewise only ever invoked on
2875
+ the leg the chain's early-return sequence actually reaches): a `properties.visible`
2876
+ template that a co-declared `visibleWhen` outranks is not reported for deciding
2877
+ nothing.
2878
+
2879
+ **No verdict changes and no interpolation changes.** The diagnostic call's return
2880
+ value is discarded; the real verdict is still computed afterward, off the
2881
+ post-evaluation, post-hoist schema, by the same code path as before this change.
2882
+ Same two reporters as objectui#5454/#5687 (unresolvable-predicate / adapter-only-data
2883
+ predicate), same dedupe `Set`, same `console.warn` severity, same dev-only gate —
2884
+ only the silence moved, one render-step earlier.
2885
+ - f90b8fb: `toRenderableSchema`'s header now says the bridge is permanent, instead of instructing
2886
+ callers to remove it (objectui#4622).
2887
+
2888
+ No executable line changes — but the artifact is **not** unchanged, and that is worth
2889
+ stating plainly rather than rounding to "comment-only". This package builds with plain
2890
+ `tsc`, and nothing in its config chain sets `"removeComments"`, so the default `false` holds and the JSDoc
2891
+ is emitted into `dist/schema-input.js` as well as `dist/schema-input.d.ts` — it is both
2892
+ what an editor shows on hover at every call site and bytes that ship.
2893
+
2894
+ Measured by building the package the way the repo builds it, at both revisions:
2895
+ `dist/schema-input.js` grows from 1,486 to 2,377 bytes (1.45 KB to 2.32 KB), and from 850
2896
+ to 1,266 bytes gzipped (0.83 KB to 1.24 KB) — **+891 bytes raw, +416 gzipped**. All 19
2897
+ differing lines in the emitted file are JSDoc continuations and the three executable lines
2898
+ are byte-identical, so the growth is the paragraph and nothing else. The trade is
2899
+ deliberate: roughly 0.4 KB gzipped, against the five-hour `Build Docs` outage the old
2900
+ paragraph's instruction produced once already.
2901
+
2902
+ The old closing paragraph said the two competing repo-wide `SchemaNode` spellings "have
2903
+ not been reconciled" and that "when it lands, the call sites using this can go back to
2904
+ forwarding directly". Both halves went false when PR #4608 merged, and the second half is
2905
+ the harmful one: it is an instruction whose trigger condition has now fired, sitting
2906
+ directly above the function a future author is about to call.
2907
+
2908
+ The reconciliation (objectui#4580 / PR #4608) resolved the collision in favour of
2909
+ `@object-ui/types`' union — `@object-ui/core` now re-exports it rather than hand-declaring
2910
+ an interface — while `SchemaRenderer`'s prop stays deliberately narrow per objectui#4548
2911
+ ruling Q2 (`schema: BaseSchema | string | null | undefined`, no `number` / `boolean`). So
2912
+ a `SchemaNode` became *less* assignable to that prop, not more, and the bridge is a
2913
+ permanent crossing between two intentionally different types rather than scaffolding
2914
+ awaiting a merge.
2915
+
2916
+ Following the old instruction has a measured cost: five `apps/site` call sites were
2917
+ forwarding directly when PR #4608 landed, and `Build Docs` was red on `main` for roughly
2918
+ five hours until PR #4621 routed all five through this function (objectui#4617).
2919
+
2920
+ ⚠️ **Dated note, 2026-10-01 — the prop's union widens — objectui#11364.** Later in this same release `SchemaRendererProps.schema` becomes `BaseSchema | AuthoringNode | string | null | undefined`, and `toRenderableSchema`'s return widens with it by reference. So the union spelled above (`schema: BaseSchema | string | null | undefined`) no longer describes the release as a whole. What it was quoted for still holds: the prop declares no `number` / `boolean`, so the bridge stays the crossing from `SchemaNode`. The rest of this entry is kept as the reading of this change.
2921
+
2922
+ ⚠️ **Dated note, 2026-10-02 — the prop takes the declared-node union — objectui#11466.** At this change the prop was `BaseSchema | string | null | undefined` (and `BaseSchema | AuthoringNode | string | null | undefined` after objectui#11364's note above); now, later in this same release, it is `DeclaredNode | string | null | undefined`, and `SchemaNode`'s object arm is `DeclaredNode` too. The prop still declares no `number` / `boolean`, so the bridge stays the crossing from `SchemaNode`. The rest of this entry is kept as the reading of this change.
2923
+ - f53a8d0: fix(react): stop the form-view bridge silently dropping 18 spec keys
2924
+
2925
+ `spec-bridge/bridges/form-view.ts` promised (#2545) that "every serializable
2926
+ spec key is either mapped onto the `object-form` node or listed here with an
2927
+ explicit reason for being ignored". Measured against `@objectstack/spec` 17.2.0
2928
+ the promise was false for 18 keys, because the conformance test enforcing it ran
2929
+ its completeness loop over its own hand-written fixture rather than the
2930
+ contract's key set.
2931
+
2932
+ Seventeen of them now reach the node, at the destinations the receiving layer
2933
+ already reads:
2934
+
2935
+ - `FormViewSchema.buttons` / `.defaults` — `ObjectFormSchema` declares both and
2936
+ `ObjectForm` folds them at render (action-button visibility/labels, and
2937
+ create-mode initial values).
2938
+ - `FormSection.pane` — explicit split-pane placement; without it a spec-authored
2939
+ split form fell back to the positional rule, so reordering sections moved them
2940
+ across the divider.
2941
+ - `FormSection.visibleOn` — the deprecated spelling now folds onto `visibleWhen`,
2942
+ matching the contract's own parse-time normalisation and the field path.
2943
+ - Thirteen `FormFieldSchema` keys — `maxLength`, `minLength`, `min`, `max`,
2944
+ `precision`, `scale`, `multiple`, `immutable`, `span`, `language`, `keyField`,
2945
+ `disclosure`, `fields` — so authored constraints, composite config and field
2946
+ width survive the bridge instead of ending there.
2947
+
2948
+ `publicPicker` is deliberately not carried and now says so: it is a server-side
2949
+ public-lookup authorization opt-in with no client destination.
2950
+
2951
+ The conformance test's key set is now derived from the contract's own shape at
2952
+ all three levels, so a spec key that is neither mapped nor explained fails the
2953
+ suite by construction.
2954
+ - d91aed9: Name the case-only spelling when a component type misses the registry.
2955
+
2956
+ Registry lookup is exactly case-sensitive, so a node typed `Page` misses a registered `page` and falls through to the OBJUI-001 "Unknown component type" panel. Because the mistake is usually uniform across a document, the symptom is not one broken widget — it is the whole page rendering as error panels, with nothing in the message pointing at the cause.
2957
+
2958
+ Both surfaces that report the miss now name the spelling that would have resolved. `SchemaRenderer`'s panel reads `Unknown component type: Page — did you mean 'page'?`, and `objectui check` reports `Unknown schema type "Page" in <file> — did you mean "page"?`. When no known type differs by case alone, neither says anything extra — `zzz` gains no bogus suggestion, and this is case matching, not an edit distance, so `pge` suggests nothing either.
2959
+
2960
+ **Lookup itself does not change.** `Page` still misses, still fails, and still renders the panel; only the message teaches. Normalising the lookup was considered and rejected (objectui#5247, maintainer ruling 2026-08-19): it would make two spellings valid everywhere, permanently, and legalise the typo class (`PAGE`, `pAge`) along with the PascalCase convention.
2961
+
2962
+ Each surface reads its candidates from the set it can actually trust — the renderer from the live `ComponentRegistry` (including pending lazy stubs), the CLI from the registration-derived `KNOWN_SCHEMA_TYPES` snapshot — so neither can suggest a type nothing registers.
2963
+ - Updated dependencies [97abedc]
2964
+ - Updated dependencies [b46c58f]
2965
+ - Updated dependencies [a507334]
2966
+ - Updated dependencies [ad694ac]
2967
+ - Updated dependencies [6f96fca]
2968
+ - Updated dependencies [afb2284]
2969
+ - Updated dependencies [3ac2de8]
2970
+ - Updated dependencies [63f4f92]
2971
+ - Updated dependencies [c131d9e]
2972
+ - Updated dependencies [5f00ff4]
2973
+ - Updated dependencies [c9e073a]
2974
+ - Updated dependencies [7b395d8]
2975
+ - Updated dependencies [0879812]
2976
+ - Updated dependencies [8cedb0d]
2977
+ - Updated dependencies [6cc910b]
2978
+ - Updated dependencies [061f5e8]
2979
+ - Updated dependencies [2dd4d3f]
2980
+ - Updated dependencies [e686f4d]
2981
+ - Updated dependencies [4ab4f1b]
2982
+ - Updated dependencies [b57107d]
2983
+ - Updated dependencies [e3ea4f9]
2984
+ - Updated dependencies [9ec8ceb]
2985
+ - Updated dependencies [bb5d4ee]
2986
+ - Updated dependencies [dc666f7]
2987
+ - Updated dependencies [3335767]
2988
+ - Updated dependencies [8b1f066]
2989
+ - Updated dependencies [af243c1]
2990
+ - Updated dependencies [961ceaa]
2991
+ - Updated dependencies [f3f4e4c]
2992
+ - Updated dependencies [a05c350]
2993
+ - Updated dependencies [1dbb993]
2994
+ - Updated dependencies [8c10f4f]
2995
+ - Updated dependencies [90dac98]
2996
+ - Updated dependencies [6096f20]
2997
+ - Updated dependencies [544aca2]
2998
+ - Updated dependencies [ea02938]
2999
+ - Updated dependencies [a14fb23]
3000
+ - Updated dependencies [ae98f1d]
3001
+ - Updated dependencies [f98eddf]
3002
+ - Updated dependencies [ce6bd99]
3003
+ - Updated dependencies [a5b08c9]
3004
+ - Updated dependencies [86982ac]
3005
+ - Updated dependencies [5e67837]
3006
+ - Updated dependencies [ff14e29]
3007
+ - Updated dependencies [9b28151]
3008
+ - Updated dependencies [64563a9]
3009
+ - Updated dependencies [1a5003f]
3010
+ - Updated dependencies [8acc51b]
3011
+ - Updated dependencies [ea9d17f]
3012
+ - Updated dependencies [45362a3]
3013
+ - Updated dependencies [2a943bf]
3014
+ - Updated dependencies [4357a27]
3015
+ - Updated dependencies [3261e64]
3016
+ - Updated dependencies [8740e86]
3017
+ - Updated dependencies [d22b37b]
3018
+ - Updated dependencies [1daf477]
3019
+ - Updated dependencies [6c2f3c5]
3020
+ - Updated dependencies [fb13e85]
3021
+ - Updated dependencies [c2d8659]
3022
+ - Updated dependencies [e0f8202]
3023
+ - Updated dependencies [ac2d6f1]
3024
+ - Updated dependencies [9fbbb17]
3025
+ - Updated dependencies [c3a26cc]
3026
+ - Updated dependencies [a66e58e]
3027
+ - Updated dependencies [d89492c]
3028
+ - Updated dependencies [9a5f998]
3029
+ - Updated dependencies [9327397]
3030
+ - Updated dependencies [17cc3a3]
3031
+ - Updated dependencies [4758b33]
3032
+ - Updated dependencies [f905090]
3033
+ - Updated dependencies [9c78ebe]
3034
+ - Updated dependencies [12809a5]
3035
+ - Updated dependencies [f9c06ef]
3036
+ - Updated dependencies [5ad3b88]
3037
+ - Updated dependencies [f9d772b]
3038
+ - Updated dependencies [97b6c21]
3039
+ - Updated dependencies [baac95a]
3040
+ - Updated dependencies [29b45f6]
3041
+ - Updated dependencies [39b8d51]
3042
+ - Updated dependencies [17b323e]
3043
+ - Updated dependencies [b956e69]
3044
+ - Updated dependencies [f812f37]
3045
+ - Updated dependencies [33e58d8]
3046
+ - Updated dependencies [256b4c9]
3047
+ - Updated dependencies [fec3b1a]
3048
+ - Updated dependencies [b8e0941]
3049
+ - Updated dependencies [0c50f18]
3050
+ - Updated dependencies [1dae95a]
3051
+ - Updated dependencies [e32dae1]
3052
+ - Updated dependencies [30b11ad]
3053
+ - Updated dependencies [4aebea0]
3054
+ - Updated dependencies [f976774]
3055
+ - Updated dependencies [3d6badf]
3056
+ - Updated dependencies [25cb364]
3057
+ - Updated dependencies [c6678b1]
3058
+ - Updated dependencies [0638322]
3059
+ - Updated dependencies [e3782d2]
3060
+ - Updated dependencies [db0beb2]
3061
+ - Updated dependencies [997ce38]
3062
+ - Updated dependencies [ae0b9d3]
3063
+ - Updated dependencies [3b469c8]
3064
+ - Updated dependencies [990a2d6]
3065
+ - Updated dependencies [6650259]
3066
+ - Updated dependencies [4f8b7f8]
3067
+ - Updated dependencies [9e6619f]
3068
+ - Updated dependencies [f6ae5e2]
3069
+ - Updated dependencies [eb97ce6]
3070
+ - Updated dependencies [7343376]
3071
+ - Updated dependencies [b2683a2]
3072
+ - Updated dependencies [dded788]
3073
+ - Updated dependencies [b45d463]
3074
+ - Updated dependencies [54a7830]
3075
+ - Updated dependencies [f3135a4]
3076
+ - Updated dependencies [b5696d3]
3077
+ - Updated dependencies [3f9d926]
3078
+ - Updated dependencies [e978ed5]
3079
+ - Updated dependencies [6a7f24e]
3080
+ - Updated dependencies [b3c96d6]
3081
+ - Updated dependencies [8d0ca91]
3082
+ - Updated dependencies [c30c8dd]
3083
+ - Updated dependencies [244d516]
3084
+ - Updated dependencies [ac15833]
3085
+ - Updated dependencies [ac15833]
3086
+ - Updated dependencies [e2dffc9]
3087
+ - Updated dependencies [328abeb]
3088
+ - Updated dependencies [24d3e65]
3089
+ - Updated dependencies [95a7c8d]
3090
+ - Updated dependencies [e227156]
3091
+ - Updated dependencies [cc4e476]
3092
+ - Updated dependencies [92970c4]
3093
+ - Updated dependencies [d570eaa]
3094
+ - Updated dependencies [4b742f4]
3095
+ - Updated dependencies [42687ba]
3096
+ - Updated dependencies [6cd8f66]
3097
+ - Updated dependencies [24a0f14]
3098
+ - Updated dependencies [797a30f]
3099
+ - Updated dependencies [b4075c0]
3100
+ - Updated dependencies [9b85600]
3101
+ - Updated dependencies [e327c89]
3102
+ - Updated dependencies [2eaf5be]
3103
+ - Updated dependencies [bf7ab35]
3104
+ - Updated dependencies [99878d8]
3105
+ - Updated dependencies [3c13675]
3106
+ - Updated dependencies [63ab761]
3107
+ - Updated dependencies [0eb9f36]
3108
+ - Updated dependencies [ae582b7]
3109
+ - Updated dependencies [51c2949]
3110
+ - Updated dependencies [8732846]
3111
+ - Updated dependencies [db11afd]
3112
+ - Updated dependencies [154075a]
3113
+ - Updated dependencies [582edef]
3114
+ - Updated dependencies [19f484f]
3115
+ - Updated dependencies [0a78a20]
3116
+ - Updated dependencies [615346d]
3117
+ - Updated dependencies [75dcc81]
3118
+ - Updated dependencies [55a12a8]
3119
+ - Updated dependencies [edfcf5a]
3120
+ - Updated dependencies [0a3e540]
3121
+ - Updated dependencies [f61dab1]
3122
+ - Updated dependencies [b0a05dd]
3123
+ - Updated dependencies [dd5ff19]
3124
+ - Updated dependencies [54997ff]
3125
+ - Updated dependencies [81f8498]
3126
+ - Updated dependencies [a782fa7]
3127
+ - Updated dependencies [1d6a23d]
3128
+ - Updated dependencies [76e9df0]
3129
+ - Updated dependencies [6158e4c]
3130
+ - Updated dependencies [cff8641]
3131
+ - Updated dependencies [c27b575]
3132
+ - Updated dependencies [0e6e76b]
3133
+ - Updated dependencies [bf43afa]
3134
+ - Updated dependencies [385ebc5]
3135
+ - Updated dependencies [858eafb]
3136
+ - Updated dependencies [a8c5509]
3137
+ - Updated dependencies [c2a8d23]
3138
+ - Updated dependencies [cd5b19a]
3139
+ - Updated dependencies [17dc167]
3140
+ - Updated dependencies [20d23be]
3141
+ - Updated dependencies [20d23be]
3142
+ - Updated dependencies [1263e40]
3143
+ - Updated dependencies [e6bc087]
3144
+ - Updated dependencies [9419df1]
3145
+ - Updated dependencies [a7557a7]
3146
+ - Updated dependencies [7d074ba]
3147
+ - Updated dependencies [6158e4c]
3148
+ - Updated dependencies [6158e4c]
3149
+ - Updated dependencies [52aad5c]
3150
+ - Updated dependencies [1563d3e]
3151
+ - Updated dependencies [58da8ae]
3152
+ - Updated dependencies [138ad45]
3153
+ - Updated dependencies [138ad45]
3154
+ - Updated dependencies [5262f7d]
3155
+ - Updated dependencies [6aa029b]
3156
+ - Updated dependencies [770cc5b]
3157
+ - Updated dependencies [d893290]
3158
+ - Updated dependencies [1a88ce2]
3159
+ - Updated dependencies [a1a44d6]
3160
+ - Updated dependencies [e0a9c67]
3161
+ - Updated dependencies [5638529]
3162
+ - Updated dependencies [5638529]
3163
+ - Updated dependencies [d0fba91]
3164
+ - Updated dependencies [c476be0]
3165
+ - Updated dependencies [c82ff39]
3166
+ - Updated dependencies [6c3da53]
3167
+ - Updated dependencies [31987bd]
3168
+ - Updated dependencies [3c3ce15]
3169
+ - Updated dependencies [063119f]
3170
+ - Updated dependencies [e100589]
3171
+ - Updated dependencies [304f611]
3172
+ - Updated dependencies [e46ee77]
3173
+ - Updated dependencies [f9c8c4e]
3174
+ - Updated dependencies [6e9c8d2]
3175
+ - Updated dependencies [9547063]
3176
+ - Updated dependencies [3f6efd6]
3177
+ - Updated dependencies [55d18c6]
3178
+ - Updated dependencies [c4ab6d0]
3179
+ - Updated dependencies [0e9058b]
3180
+ - Updated dependencies [c4ab6d0]
3181
+ - Updated dependencies [5988b6b]
3182
+ - Updated dependencies [6158e4c]
3183
+ - Updated dependencies [00ccdf7]
3184
+ - Updated dependencies [9d9ed54]
3185
+ - Updated dependencies [4a1adb7]
3186
+ - Updated dependencies [ca3de72]
3187
+ - Updated dependencies [83e3f83]
3188
+ - Updated dependencies [401611b]
3189
+ - Updated dependencies [2c0ddf2]
3190
+ - Updated dependencies [4abc0aa]
3191
+ - Updated dependencies [f560ded]
3192
+ - Updated dependencies [2b188fa]
3193
+ - Updated dependencies [f68e0a0]
3194
+ - Updated dependencies [aea682a]
3195
+ - Updated dependencies [fcdc8ec]
3196
+ - Updated dependencies [2d576e4]
3197
+ - Updated dependencies [8366acc]
3198
+ - Updated dependencies [95e58a3]
3199
+ - Updated dependencies [9d7419b]
3200
+ - Updated dependencies [fc7db05]
3201
+ - Updated dependencies [9ed8d0f]
3202
+ - Updated dependencies [c73cdb5]
3203
+ - Updated dependencies [6f5719e]
3204
+ - Updated dependencies [d0097af]
3205
+ - Updated dependencies [432882b]
3206
+ - Updated dependencies [64dae8e]
3207
+ - Updated dependencies [b06e374]
3208
+ - Updated dependencies [06a8af5]
3209
+ - Updated dependencies [6a91586]
3210
+ - Updated dependencies [a04d7c6]
3211
+ - Updated dependencies [f3c2bb0]
3212
+ - Updated dependencies [978507b]
3213
+ - Updated dependencies [9801765]
3214
+ - Updated dependencies [9cebfca]
3215
+ - Updated dependencies [460575f]
3216
+ - Updated dependencies [594704f]
3217
+ - Updated dependencies [d3995fe]
3218
+ - Updated dependencies [d88e20f]
3219
+ - Updated dependencies [2d7304d]
3220
+ - Updated dependencies [993f312]
3221
+ - Updated dependencies [636b236]
3222
+ - Updated dependencies [d6d8fb9]
3223
+ - Updated dependencies [64d624d]
3224
+ - Updated dependencies [053fdc8]
3225
+ - Updated dependencies [41b7ce3]
3226
+ - Updated dependencies [ae476b8]
3227
+ - Updated dependencies [95bad12]
3228
+ - Updated dependencies [d2fb6ef]
3229
+ - Updated dependencies [fda49e5]
3230
+ - Updated dependencies [94e2fa7]
3231
+ - Updated dependencies [490d9a9]
3232
+ - Updated dependencies [fc62bb4]
3233
+ - Updated dependencies [41df893]
3234
+ - Updated dependencies [0cba1b7]
3235
+ - Updated dependencies [00f3eb5]
3236
+ - Updated dependencies [1ec291c]
3237
+ - Updated dependencies [453dbaa]
3238
+ - Updated dependencies [95f8704]
3239
+ - Updated dependencies [69a2163]
3240
+ - Updated dependencies [24e027e]
3241
+ - Updated dependencies [2c3cd1b]
3242
+ - Updated dependencies [90665e0]
3243
+ - Updated dependencies [8d3a529]
3244
+ - Updated dependencies [5ac2e2c]
3245
+ - Updated dependencies [7e19d03]
3246
+ - Updated dependencies [b08b7eb]
3247
+ - Updated dependencies [1e946c9]
3248
+ - Updated dependencies [546ddf7]
3249
+ - Updated dependencies [864154e]
3250
+ - Updated dependencies [b023625]
3251
+ - Updated dependencies [75bd83d]
3252
+ - Updated dependencies [44d075b]
3253
+ - Updated dependencies [40c479a]
3254
+ - Updated dependencies [971d387]
3255
+ - Updated dependencies [ee851c3]
3256
+ - Updated dependencies [6414dfd]
3257
+ - Updated dependencies [a8d5c71]
3258
+ - Updated dependencies [905b21f]
3259
+ - Updated dependencies [88e9109]
3260
+ - Updated dependencies [2c45966]
3261
+ - Updated dependencies [db3a600]
3262
+ - Updated dependencies [3a3db76]
3263
+ - Updated dependencies [0d723a3]
3264
+ - Updated dependencies [0c95d3d]
3265
+ - Updated dependencies [3e4fa2c]
3266
+ - Updated dependencies [b5b928a]
3267
+ - Updated dependencies [5fa06c4]
3268
+ - Updated dependencies [52a43de]
3269
+ - Updated dependencies [195052f]
3270
+ - Updated dependencies [e4559d1]
3271
+ - Updated dependencies [2c71482]
3272
+ - Updated dependencies [a26b9e4]
3273
+ - Updated dependencies [5ef9c4f]
3274
+ - Updated dependencies [46f0bb4]
3275
+ - Updated dependencies [06b82b8]
3276
+ - Updated dependencies [6f81384]
3277
+ - Updated dependencies [5d3a2d1]
3278
+ - Updated dependencies [8f1d995]
3279
+ - Updated dependencies [b362c1b]
3280
+ - Updated dependencies [5127378]
3281
+ - Updated dependencies [dddb942]
3282
+ - Updated dependencies [00c665e]
3283
+ - Updated dependencies [29754cf]
3284
+ - Updated dependencies [3c2b6f7]
3285
+ - Updated dependencies [b84dc18]
3286
+ - Updated dependencies [ac8abb0]
3287
+ - Updated dependencies [9d86e1d]
3288
+ - Updated dependencies [3a5817f]
3289
+ - Updated dependencies [99a3c2d]
3290
+ - Updated dependencies [5961030]
3291
+ - Updated dependencies [c8ea8af]
3292
+ - Updated dependencies [9602dc8]
3293
+ - Updated dependencies [3190414]
3294
+ - Updated dependencies [4e480f5]
3295
+ - Updated dependencies [38a123c]
3296
+ - Updated dependencies [299102e]
3297
+ - Updated dependencies [d7acad6]
3298
+ - Updated dependencies [45a9aeb]
3299
+ - Updated dependencies [713db46]
3300
+ - Updated dependencies [bf3a03c]
3301
+ - Updated dependencies [cb55718]
3302
+ - Updated dependencies [831be72]
3303
+ - Updated dependencies [29cb85b]
3304
+ - Updated dependencies [3e028c8]
3305
+ - Updated dependencies [d0889e2]
3306
+ - Updated dependencies [ce503e5]
3307
+ - Updated dependencies [f20dcf0]
3308
+ - Updated dependencies [4ca30d0]
3309
+ - Updated dependencies [7a5da14]
3310
+ - Updated dependencies [fff9645]
3311
+ - Updated dependencies [9c3b7ce]
3312
+ - Updated dependencies [2c1c967]
3313
+ - Updated dependencies [4d5f9b4]
3314
+ - Updated dependencies [d6ceb8d]
3315
+ - Updated dependencies [969ba84]
3316
+ - Updated dependencies [7977ff9]
3317
+ - Updated dependencies [3beef6d]
3318
+ - Updated dependencies [dbd5194]
3319
+ - Updated dependencies [b392674]
3320
+ - Updated dependencies [4f3a1e2]
3321
+ - Updated dependencies [2acd8e1]
3322
+ - Updated dependencies [f07b976]
3323
+ - Updated dependencies [045d20b]
3324
+ - Updated dependencies [a2d2515]
3325
+ - Updated dependencies [adb2a86]
3326
+ - Updated dependencies [503cd8b]
3327
+ - Updated dependencies [3619792]
3328
+ - Updated dependencies [3561bd2]
3329
+ - Updated dependencies [bf97b98]
3330
+ - Updated dependencies [320374d]
3331
+ - Updated dependencies [b0d308d]
3332
+ - Updated dependencies [40f34b4]
3333
+ - Updated dependencies [8063bcb]
3334
+ - Updated dependencies [b74a859]
3335
+ - Updated dependencies [d4493fd]
3336
+ - Updated dependencies [240b80f]
3337
+ - Updated dependencies [77cb489]
3338
+ - Updated dependencies [bfaa158]
3339
+ - Updated dependencies [777e5c6]
3340
+ - Updated dependencies [0c386dd]
3341
+ - Updated dependencies [9e37d9b]
3342
+ - Updated dependencies [5ad86dd]
3343
+ - Updated dependencies [16a725f]
3344
+ - Updated dependencies [4dfdcc3]
3345
+ - Updated dependencies [ce986aa]
3346
+ - Updated dependencies [446d93d]
3347
+ - Updated dependencies [ecd9cb2]
3348
+ - Updated dependencies [98d4108]
3349
+ - Updated dependencies [0e3b3be]
3350
+ - Updated dependencies [512bc90]
3351
+ - Updated dependencies [a29ae2d]
3352
+ - Updated dependencies [220c18d]
3353
+ - Updated dependencies [4388f71]
3354
+ - Updated dependencies [0b1ac58]
3355
+ - Updated dependencies [c93b4d5]
3356
+ - Updated dependencies [c1fe272]
3357
+ - Updated dependencies [8ad218d]
3358
+ - Updated dependencies [3e41187]
3359
+ - Updated dependencies [5f78953]
3360
+ - Updated dependencies [639114c]
3361
+ - Updated dependencies [639114c]
3362
+ - Updated dependencies [639114c]
3363
+ - Updated dependencies [1490691]
3364
+ - Updated dependencies [1f31d3a]
3365
+ - Updated dependencies [d8ec8d6]
3366
+ - Updated dependencies [351eb31]
3367
+ - Updated dependencies [866cd1d]
3368
+ - Updated dependencies [20c04b2]
3369
+ - Updated dependencies [01c9023]
3370
+ - Updated dependencies [48c19bd]
3371
+ - Updated dependencies [a6d8b8d]
3372
+ - Updated dependencies [b652514]
3373
+ - Updated dependencies [adbda1b]
3374
+ - Updated dependencies [8952395]
3375
+ - Updated dependencies [e2b3826]
3376
+ - Updated dependencies [0348bc9]
3377
+ - Updated dependencies [e8c553b]
3378
+ - Updated dependencies [2e32ed4]
3379
+ - Updated dependencies [7c3df8f]
3380
+ - Updated dependencies [a4514e8]
3381
+ - Updated dependencies [db3896c]
3382
+ - Updated dependencies [e75f4c9]
3383
+ - Updated dependencies [19f1639]
3384
+ - Updated dependencies [47547d0]
3385
+ - Updated dependencies [1bee5d0]
3386
+ - Updated dependencies [858cd72]
3387
+ - Updated dependencies [cfc9b6d]
3388
+ - Updated dependencies [554f2b6]
3389
+ - Updated dependencies [669d71b]
3390
+ - Updated dependencies [6a99bb2]
3391
+ - Updated dependencies [ed27d7c]
3392
+ - Updated dependencies [52c8cf7]
3393
+ - Updated dependencies [2ceb43a]
3394
+ - Updated dependencies [7cdd2b9]
3395
+ - Updated dependencies [52c8cf7]
3396
+ - Updated dependencies [3399704]
3397
+ - Updated dependencies [81a2eb1]
3398
+ - Updated dependencies [caa0cd3]
3399
+ - Updated dependencies [20cb8db]
3400
+ - Updated dependencies [25c7d58]
3401
+ - Updated dependencies [00d2fa6]
3402
+ - Updated dependencies [77b2a18]
3403
+ - Updated dependencies [c6198c2]
3404
+ - Updated dependencies [721d1e0]
3405
+ - Updated dependencies [c2f0f48]
3406
+ - Updated dependencies [1237ae4]
3407
+ - Updated dependencies [51eb515]
3408
+ - Updated dependencies [c354ce5]
3409
+ - Updated dependencies [8fe8e5c]
3410
+ - Updated dependencies [feac439]
3411
+ - Updated dependencies [efbd566]
3412
+ - Updated dependencies [2a5bf45]
3413
+ - Updated dependencies [9587fc9]
3414
+ - Updated dependencies [e62c44e]
3415
+ - Updated dependencies [23b9958]
3416
+ - Updated dependencies [5d0876c]
3417
+ - Updated dependencies [b041b9c]
3418
+ - Updated dependencies [ce2aaef]
3419
+ - Updated dependencies [544ecba]
3420
+ - Updated dependencies [bc640ec]
3421
+ - Updated dependencies [1e215c4]
3422
+ - Updated dependencies [da6e191]
3423
+ - Updated dependencies [3e377c9]
3424
+ - Updated dependencies [a3eb5d0]
3425
+ - Updated dependencies [4ce14f1]
3426
+ - Updated dependencies [aef97e5]
3427
+ - Updated dependencies [2af1fa7]
3428
+ - Updated dependencies [a137d0c]
3429
+ - Updated dependencies [caf477f]
3430
+ - Updated dependencies [f6375da]
3431
+ - Updated dependencies [967e5d8]
3432
+ - Updated dependencies [c907a9c]
3433
+ - Updated dependencies [a4611b3]
3434
+ - Updated dependencies [20316ba]
3435
+ - Updated dependencies [d3499b3]
3436
+ - Updated dependencies [91f9276]
3437
+ - Updated dependencies [309c75e]
3438
+ - Updated dependencies [c9f9bae]
3439
+ - Updated dependencies [18897a4]
3440
+ - Updated dependencies [8b7ea39]
3441
+ - Updated dependencies [a915064]
3442
+ - Updated dependencies [dcbf0b2]
3443
+ - Updated dependencies [52cac38]
3444
+ - Updated dependencies [93fea2e]
3445
+ - Updated dependencies [53ded82]
3446
+ - Updated dependencies [1422a92]
3447
+ - Updated dependencies [d05fe17]
3448
+ - Updated dependencies [a480f79]
3449
+ - Updated dependencies [f08d1a8]
3450
+ - Updated dependencies [64a252d]
3451
+ - Updated dependencies [786bc91]
3452
+ - Updated dependencies [75fca96]
3453
+ - Updated dependencies [7ca6ddd]
3454
+ - Updated dependencies [f1cd290]
3455
+ - Updated dependencies [5a41ce7]
3456
+ - Updated dependencies [8d50bc2]
3457
+ - Updated dependencies [604476d]
3458
+ - Updated dependencies [335abea]
3459
+ - Updated dependencies [0f5cadf]
3460
+ - Updated dependencies [4f9f1ee]
3461
+ - Updated dependencies [66e8b2a]
3462
+ - Updated dependencies [aa083cd]
3463
+ - Updated dependencies [12b5992]
3464
+ - Updated dependencies [b93e245]
3465
+ - Updated dependencies [c842594]
3466
+ - Updated dependencies [290de37]
3467
+ - Updated dependencies [e1c27e4]
3468
+ - Updated dependencies [8c8da45]
3469
+ - Updated dependencies [8cd8eb5]
3470
+ - Updated dependencies [cf1d29e]
3471
+ - Updated dependencies [1bd79c8]
3472
+ - Updated dependencies [af9e957]
3473
+ - Updated dependencies [b1030c7]
3474
+ - Updated dependencies [c974edf]
3475
+ - Updated dependencies [ad852b6]
3476
+ - Updated dependencies [6778809]
3477
+ - Updated dependencies [0758bd8]
3478
+ - Updated dependencies [ee4d19f]
3479
+ - Updated dependencies [496d31d]
3480
+ - Updated dependencies [f57ca75]
3481
+ - Updated dependencies [0ea7054]
3482
+ - Updated dependencies [9a853f2]
3483
+ - Updated dependencies [cb847fd]
3484
+ - Updated dependencies [ee70287]
3485
+ - Updated dependencies [3e98e13]
3486
+ - Updated dependencies [a695f50]
3487
+ - Updated dependencies [fc32921]
3488
+ - Updated dependencies [4eaa835]
3489
+ - Updated dependencies [8f9d87a]
3490
+ - Updated dependencies [b1777ae]
3491
+ - Updated dependencies [24845c4]
3492
+ - Updated dependencies [6f864cf]
3493
+ - Updated dependencies [24d1edd]
3494
+ - Updated dependencies [645087c]
3495
+ - Updated dependencies [33f4a19]
3496
+ - Updated dependencies [6e9a3d4]
3497
+ - Updated dependencies [5323168]
3498
+ - Updated dependencies [841dd2b]
3499
+ - Updated dependencies [3014fc0]
3500
+ - Updated dependencies [dacb402]
3501
+ - Updated dependencies [846cec0]
3502
+ - Updated dependencies [91facae]
3503
+ - Updated dependencies [b38014e]
3504
+ - Updated dependencies [474797d]
3505
+ - Updated dependencies [704e695]
3506
+ - Updated dependencies [a407bd6]
3507
+ - Updated dependencies [317dbce]
3508
+ - Updated dependencies [309728c]
3509
+ - Updated dependencies [aa08d7e]
3510
+ - Updated dependencies [3a43a15]
3511
+ - Updated dependencies [f76f436]
3512
+ - Updated dependencies [ce45a03]
3513
+ - Updated dependencies [421544b]
3514
+ - Updated dependencies [fb01022]
3515
+ - Updated dependencies [e9d9212]
3516
+ - Updated dependencies [ecfb693]
3517
+ - Updated dependencies [abc1b18]
3518
+ - Updated dependencies [81a51db]
3519
+ - Updated dependencies [67749c7]
3520
+ - Updated dependencies [507b61b]
3521
+ - Updated dependencies [512c84b]
3522
+ - Updated dependencies [fb3a101]
3523
+ - Updated dependencies [d4733f2]
3524
+ - Updated dependencies [7c9145f]
3525
+ - Updated dependencies [f391ede]
3526
+ - Updated dependencies [f5cfbbd]
3527
+ - Updated dependencies [f5cfbbd]
3528
+ - Updated dependencies [8b532cb]
3529
+ - Updated dependencies [64c3cdd]
3530
+ - Updated dependencies [4d65991]
3531
+ - Updated dependencies [c42554e]
3532
+ - Updated dependencies [555b4ec]
3533
+ - Updated dependencies [1ccfc23]
3534
+ - Updated dependencies [542718f]
3535
+ - Updated dependencies [7f27bc5]
3536
+ - Updated dependencies [0a174f3]
3537
+ - Updated dependencies [676f677]
3538
+ - Updated dependencies [f95b140]
3539
+ - Updated dependencies [541ce4e]
3540
+ - Updated dependencies [6479086]
3541
+ - Updated dependencies [d79f525]
3542
+ - Updated dependencies [d1865d2]
3543
+ - Updated dependencies [9662aca]
3544
+ - Updated dependencies [55ba3ff]
3545
+ - Updated dependencies [f1190b0]
3546
+ - Updated dependencies [561abef]
3547
+ - Updated dependencies [ef52001]
3548
+ - Updated dependencies [6a4680b]
3549
+ - Updated dependencies [c3a4273]
3550
+ - Updated dependencies [abf710d]
3551
+ - Updated dependencies [093af32]
3552
+ - Updated dependencies [1bd1be7]
3553
+ - Updated dependencies [d234fa9]
3554
+ - Updated dependencies [adf5812]
3555
+ - Updated dependencies [5058336]
3556
+ - Updated dependencies [2f6b2bf]
3557
+ - Updated dependencies [2028b31]
3558
+ - Updated dependencies [63601ab]
3559
+ - Updated dependencies [c372b29]
3560
+ - Updated dependencies [8693b85]
3561
+ - Updated dependencies [58b7b3d]
3562
+ - Updated dependencies [84defab]
3563
+ - Updated dependencies [681d3f1]
3564
+ - Updated dependencies [969d4f2]
3565
+ - Updated dependencies [f3bc481]
3566
+ - Updated dependencies [b79aac2]
3567
+ - Updated dependencies [93fc0e7]
3568
+ - Updated dependencies [a4b723f]
3569
+ - Updated dependencies [2b10ca0]
3570
+ - Updated dependencies [7db4a81]
3571
+ - Updated dependencies [19a0b0e]
3572
+ - Updated dependencies [526fc11]
3573
+ - Updated dependencies [f8e3e9a]
3574
+ - Updated dependencies [3b6bc69]
3575
+ - Updated dependencies [6732df4]
3576
+ - Updated dependencies [fe9e0d0]
3577
+ - Updated dependencies [63fb72c]
3578
+ - Updated dependencies [0b8c638]
3579
+ - Updated dependencies [804831c]
3580
+ - Updated dependencies [279e48e]
3581
+ - Updated dependencies [8700d6d]
3582
+ - Updated dependencies [8db2a0f]
3583
+ - Updated dependencies [30443fb]
3584
+ - Updated dependencies [da45e6b]
3585
+ - Updated dependencies [835f0f3]
3586
+ - Updated dependencies [ed35b44]
3587
+ - Updated dependencies [729e851]
3588
+ - Updated dependencies [96919a4]
3589
+ - Updated dependencies [20b507a]
3590
+ - Updated dependencies [2e471dc]
3591
+ - Updated dependencies [be50942]
3592
+ - Updated dependencies [775e079]
3593
+ - Updated dependencies [7e8b3c0]
3594
+ - Updated dependencies [53374dc]
3595
+ - Updated dependencies [f6fb83f]
3596
+ - Updated dependencies [2049b03]
3597
+ - Updated dependencies [2bf34f7]
3598
+ - Updated dependencies [15b33ae]
3599
+ - Updated dependencies [7cbc724]
3600
+ - Updated dependencies [fb91ac9]
3601
+ - Updated dependencies [641fb55]
3602
+ - Updated dependencies [8524372]
3603
+ - Updated dependencies [7cbefa5]
3604
+ - Updated dependencies [72d6587]
3605
+ - Updated dependencies [a272a4f]
3606
+ - Updated dependencies [55f39ee]
3607
+ - Updated dependencies [0970a0e]
3608
+ - Updated dependencies [e427e9c]
3609
+ - Updated dependencies [bbc9dc3]
3610
+ - Updated dependencies [02f1813]
3611
+ - Updated dependencies [ba0b61a]
3612
+ - Updated dependencies [ac716ff]
3613
+ - Updated dependencies [f0f3cd5]
3614
+ - Updated dependencies [ab856ed]
3615
+ - Updated dependencies [20f3e65]
3616
+ - Updated dependencies [bbba098]
3617
+ - Updated dependencies [87af769]
3618
+ - Updated dependencies [3be720e]
3619
+ - Updated dependencies [43c0d17]
3620
+ - Updated dependencies [c3df43a]
3621
+ - Updated dependencies [d16d0e9]
3622
+ - Updated dependencies [bbe57fd]
3623
+ - Updated dependencies [272a530]
3624
+ - Updated dependencies [1779e8d]
3625
+ - Updated dependencies [f0f4d6c]
3626
+ - Updated dependencies [4128188]
3627
+ - Updated dependencies [269880a]
3628
+ - Updated dependencies [b253c4e]
3629
+ - Updated dependencies [78a9c67]
3630
+ - Updated dependencies [4a7ef0d]
3631
+ - Updated dependencies [dea17b4]
3632
+ - Updated dependencies [89bb77a]
3633
+ - Updated dependencies [06611e4]
3634
+ - Updated dependencies [44152c4]
3635
+ - Updated dependencies [dc3893d]
3636
+ - Updated dependencies [1bbaa16]
3637
+ - Updated dependencies [6ee259a]
3638
+ - Updated dependencies [e708426]
3639
+ - Updated dependencies [3b6d53b]
3640
+ - Updated dependencies [276d174]
3641
+ - Updated dependencies [2982ed9]
3642
+ - Updated dependencies [a8198de]
3643
+ - Updated dependencies [05a49f2]
3644
+ - Updated dependencies [a78cd37]
3645
+ - Updated dependencies [5ea623e]
3646
+ - Updated dependencies [5eabe86]
3647
+ - Updated dependencies [ca5d671]
3648
+ - Updated dependencies [32bf2d6]
3649
+ - Updated dependencies [af4fb29]
3650
+ - Updated dependencies [c698a81]
3651
+ - Updated dependencies [ff5ef1c]
3652
+ - Updated dependencies [9a97800]
3653
+ - Updated dependencies [6bca0e4]
3654
+ - Updated dependencies [81c0bc4]
3655
+ - Updated dependencies [b2e85a9]
3656
+ - Updated dependencies [60500cb]
3657
+ - Updated dependencies [d5c1f52]
3658
+ - Updated dependencies [c7cd2b6]
3659
+ - Updated dependencies [b2065e7]
3660
+ - Updated dependencies [2fcefb9]
3661
+ - Updated dependencies [77f846a]
3662
+ - Updated dependencies [bc5870c]
3663
+ - Updated dependencies [b55a346]
3664
+ - Updated dependencies [065bba7]
3665
+ - Updated dependencies [f760064]
3666
+ - Updated dependencies [6791717]
3667
+ - Updated dependencies [8ea3bee]
3668
+ - Updated dependencies [100547e]
3669
+ - Updated dependencies [3a58149]
3670
+ - Updated dependencies [6d1c155]
3671
+ - Updated dependencies [d7573b3]
3672
+ - Updated dependencies [bf3edfe]
3673
+ - Updated dependencies [6ce89da]
3674
+ - Updated dependencies [0e05aac]
3675
+ - Updated dependencies [5aed9e4]
3676
+ - Updated dependencies [83c77dc]
3677
+ - Updated dependencies [18a8e7d]
3678
+ - Updated dependencies [e7957ab]
3679
+ - Updated dependencies [f7e34ca]
3680
+ - Updated dependencies [e719ebd]
3681
+ - Updated dependencies [516583b]
3682
+ - Updated dependencies [f9e4f91]
3683
+ - Updated dependencies [6ef48b1]
3684
+ - Updated dependencies [fa429cf]
3685
+ - Updated dependencies [ed8df3e]
3686
+ - Updated dependencies [b470e91]
3687
+ - Updated dependencies [8b446f5]
3688
+ - Updated dependencies [8e74b27]
3689
+ - Updated dependencies [8ebd57f]
3690
+ - Updated dependencies [617707a]
3691
+ - Updated dependencies [c40f3b8]
3692
+ - Updated dependencies [7357447]
3693
+ - Updated dependencies [199d31b]
3694
+ - Updated dependencies [3e01cb5]
3695
+ - Updated dependencies [7138bc1]
3696
+ - Updated dependencies [cef27e2]
3697
+ - Updated dependencies [4e8622b]
3698
+ - Updated dependencies [dffd752]
3699
+ - Updated dependencies [06973aa]
3700
+ - Updated dependencies [50798f3]
3701
+ - Updated dependencies [6a576c9]
3702
+ - Updated dependencies [105f3c5]
3703
+ - Updated dependencies [3ccd9e8]
3704
+ - Updated dependencies [689b979]
3705
+ - Updated dependencies [e546222]
3706
+ - Updated dependencies [fd13f52]
3707
+ - Updated dependencies [fffa30d]
3708
+ - Updated dependencies [fb336df]
3709
+ - Updated dependencies [20e317c]
3710
+ - Updated dependencies [0fce2ef]
3711
+ - Updated dependencies [42df928]
3712
+ - Updated dependencies [8e00bfd]
3713
+ - Updated dependencies [8d37efb]
3714
+ - Updated dependencies [0e2ddd4]
3715
+ - Updated dependencies [b7479ab]
3716
+ - Updated dependencies [9118a31]
3717
+ - Updated dependencies [de570cc]
3718
+ - Updated dependencies [b2ea297]
3719
+ - Updated dependencies [5b5a5c3]
3720
+ - Updated dependencies [14582b8]
3721
+ - Updated dependencies [51e144e]
3722
+ - Updated dependencies [19cbf10]
3723
+ - Updated dependencies [a691c0b]
3724
+ - Updated dependencies [1e66879]
3725
+ - Updated dependencies [c5200f0]
3726
+ - Updated dependencies [af3861f]
3727
+ - Updated dependencies [515f171]
3728
+ - Updated dependencies [1f4e029]
3729
+ - Updated dependencies [7b43319]
3730
+ - Updated dependencies [258d264]
3731
+ - Updated dependencies [8033ad1]
3732
+ - Updated dependencies [c00bf28]
3733
+ - Updated dependencies [93127bd]
3734
+ - Updated dependencies [f2158ec]
3735
+ - Updated dependencies [759606e]
3736
+ - Updated dependencies [fd8dace]
3737
+ - Updated dependencies [a51fa0c]
3738
+ - Updated dependencies [51f3d8d]
3739
+ - Updated dependencies [78cbdb5]
3740
+ - Updated dependencies [b7543a9]
3741
+ - Updated dependencies [6c6cee7]
3742
+ - Updated dependencies [83fe6e7]
3743
+ - Updated dependencies [d1ab06f]
3744
+ - Updated dependencies [38a9568]
3745
+ - Updated dependencies [91783c4]
3746
+ - Updated dependencies [ca39427]
3747
+ - Updated dependencies [2d36552]
3748
+ - Updated dependencies [b2437a7]
3749
+ - Updated dependencies [f157423]
3750
+ - Updated dependencies [7a90afd]
3751
+ - Updated dependencies [eddc1dd]
3752
+ - Updated dependencies [c9327c9]
3753
+ - Updated dependencies [920165d]
3754
+ - Updated dependencies [968dc1e]
3755
+ - Updated dependencies [3c73d99]
3756
+ - Updated dependencies [ed71d9e]
3757
+ - Updated dependencies [7776fc2]
3758
+ - Updated dependencies [e76634c]
3759
+ - Updated dependencies [fb96ecb]
3760
+ - Updated dependencies [1170ed1]
3761
+ - Updated dependencies [f75810e]
3762
+ - Updated dependencies [92814db]
3763
+ - Updated dependencies [4d73b07]
3764
+ - @object-ui/core@17.7.0
3765
+ - @object-ui/types@17.7.0
3766
+ - @object-ui/i18n@17.7.0
3767
+ - @object-ui/data-objectstack@17.7.0
3768
+
3
3769
  ## 17.6.0
4
3770
 
5
3771
  ### Minor Changes