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