@object-ui/core 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 (147) hide show
  1. package/CHANGELOG.md +7260 -0
  2. package/README.md +228 -14
  3. package/dist/actions/ActionEngine.d.ts +4 -2
  4. package/dist/actions/ActionEngine.js +13 -8
  5. package/dist/actions/ActionRunner.d.ts +473 -69
  6. package/dist/actions/ActionRunner.js +792 -125
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/UndoManager.d.ts +8 -13
  9. package/dist/actions/actionKeys.d.ts +9 -7
  10. package/dist/actions/actionKeys.js +67 -14
  11. package/dist/actions/actionResponse.js +2 -2
  12. package/dist/actions/index.d.ts +2 -0
  13. package/dist/actions/index.js +2 -0
  14. package/dist/actions/navigationTarget.d.ts +41 -0
  15. package/dist/actions/navigationTarget.js +20 -0
  16. package/dist/actions/recordDelete.d.ts +154 -0
  17. package/dist/actions/recordDelete.js +110 -0
  18. package/dist/adapters/ApiDataSource.d.ts +8 -4
  19. package/dist/adapters/ApiDataSource.js +19 -7
  20. package/dist/adapters/ValueDataSource.d.ts +6 -1
  21. package/dist/adapters/ValueDataSource.js +1090 -98
  22. package/dist/adapters/batchTransaction.d.ts +1 -1
  23. package/dist/adapters/batchTransaction.js +1 -1
  24. package/dist/adapters/resolveDataSource.d.ts +9 -6
  25. package/dist/adapters/resolveDataSource.js +9 -12
  26. package/dist/builder/schema-builder.d.ts +38 -81
  27. package/dist/builder/schema-builder.js +33 -152
  28. package/dist/data-scope/ViewDataProvider.d.ts +8 -25
  29. package/dist/data-scope/ViewDataProvider.js +27 -54
  30. package/dist/data-scope/element-data-source.d.ts +261 -7
  31. package/dist/data-scope/element-data-source.js +342 -52
  32. package/dist/data-scope/index.d.ts +9 -4
  33. package/dist/data-scope/index.js +9 -4
  34. package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
  35. package/dist/evaluator/ExpressionEvaluator.js +184 -12
  36. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  37. package/dist/evaluator/FormulaFunctions.js +166 -27
  38. package/dist/evaluator/deadlineSettled.d.ts +40 -0
  39. package/dist/evaluator/deadlineSettled.js +45 -0
  40. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  41. package/dist/evaluator/declaredPredicate.js +117 -16
  42. package/dist/evaluator/fieldRules.d.ts +88 -4
  43. package/dist/evaluator/fieldRules.js +208 -31
  44. package/dist/evaluator/index.d.ts +2 -0
  45. package/dist/evaluator/index.js +2 -0
  46. package/dist/evaluator/listConditional.d.ts +103 -34
  47. package/dist/evaluator/listConditional.js +127 -50
  48. package/dist/evaluator/optionRules.d.ts +77 -6
  49. package/dist/evaluator/optionRules.js +87 -10
  50. package/dist/evaluator/predicateInput.d.ts +15 -4
  51. package/dist/evaluator/predicateInput.js +35 -2
  52. package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
  53. package/dist/evaluator/rowPredicateCanon.js +65 -115
  54. package/dist/evaluator/subjectPermissions.d.ts +105 -0
  55. package/dist/evaluator/subjectPermissions.js +86 -0
  56. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  57. package/dist/evaluator/unevaluablePredicate.js +70 -0
  58. package/dist/index.d.ts +15 -0
  59. package/dist/index.js +83 -2
  60. package/dist/protocols/index.d.ts +0 -1
  61. package/dist/protocols/index.js +9 -1
  62. package/dist/registry/Registry.d.ts +382 -34
  63. package/dist/registry/Registry.js +247 -12
  64. package/dist/registry/WidgetRegistry.js +7 -3
  65. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  66. package/dist/registry/html-tier-intrinsics.js +110 -0
  67. package/dist/registry/public-blocks.d.ts +8 -0
  68. package/dist/registry/public-blocks.js +57 -0
  69. package/dist/theme/ThemeEngine.js +1 -1
  70. package/dist/utils/affordanceGrants.d.ts +371 -0
  71. package/dist/utils/affordanceGrants.js +252 -0
  72. package/dist/utils/chart-category-key.d.ts +82 -0
  73. package/dist/utils/chart-category-key.js +84 -0
  74. package/dist/utils/chart-measure-key.d.ts +64 -0
  75. package/dist/utils/chart-measure-key.js +65 -0
  76. package/dist/utils/chart-presentation.d.ts +95 -112
  77. package/dist/utils/chart-presentation.js +169 -104
  78. package/dist/utils/chart-series.d.ts +165 -5
  79. package/dist/utils/chart-series.js +123 -18
  80. package/dist/utils/column-sortability.d.ts +157 -0
  81. package/dist/utils/column-sortability.js +217 -0
  82. package/dist/utils/dashboard-filters.d.ts +38 -6
  83. package/dist/utils/dashboard-filters.js +107 -61
  84. package/dist/utils/dataset-format.d.ts +211 -30
  85. package/dist/utils/dataset-format.js +389 -46
  86. package/dist/utils/date-display.d.ts +346 -0
  87. package/dist/utils/date-display.js +644 -0
  88. package/dist/utils/date-macros.js +7 -0
  89. package/dist/utils/drill-down.d.ts +78 -0
  90. package/dist/utils/drill-down.js +93 -0
  91. package/dist/utils/emptiness.d.ts +96 -0
  92. package/dist/utils/emptiness.js +101 -0
  93. package/dist/utils/expand-fields.d.ts +81 -9
  94. package/dist/utils/expand-fields.js +81 -9
  95. package/dist/utils/extract-records.d.ts +65 -5
  96. package/dist/utils/extract-records.js +65 -8
  97. package/dist/utils/filter-converter.d.ts +158 -6
  98. package/dist/utils/filter-converter.js +1098 -24
  99. package/dist/utils/filter-tokens.d.ts +49 -5
  100. package/dist/utils/filter-tokens.js +105 -33
  101. package/dist/utils/grouping-fields.d.ts +61 -0
  102. package/dist/utils/grouping-fields.js +84 -0
  103. package/dist/utils/managedBy.d.ts +3 -3
  104. package/dist/utils/managedBy.js +5 -5
  105. package/dist/utils/native-date-value.d.ts +48 -0
  106. package/dist/utils/native-date-value.js +167 -0
  107. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  108. package/dist/utils/non-grid-row-ceiling.js +141 -0
  109. package/dist/utils/normalize-list-view.d.ts +135 -11
  110. package/dist/utils/normalize-list-view.js +243 -64
  111. package/dist/utils/number-display.d.ts +63 -14
  112. package/dist/utils/number-display.js +34 -12
  113. package/dist/utils/object-aggregate-query.d.ts +113 -0
  114. package/dist/utils/object-aggregate-query.js +50 -0
  115. package/dist/utils/parent-scope.d.ts +123 -0
  116. package/dist/utils/parent-scope.js +140 -0
  117. package/dist/utils/predicate-fields.d.ts +45 -12
  118. package/dist/utils/predicate-fields.js +141 -12
  119. package/dist/utils/record-color.d.ts +75 -0
  120. package/dist/utils/record-color.js +65 -0
  121. package/dist/utils/record-source.d.ts +298 -0
  122. package/dist/utils/record-source.js +331 -0
  123. package/dist/utils/record-title.d.ts +148 -8
  124. package/dist/utils/record-title.js +220 -26
  125. package/dist/utils/reference-keys.d.ts +21 -22
  126. package/dist/utils/reference-keys.js +532 -29
  127. package/dist/utils/sort-query.d.ts +115 -15
  128. package/dist/utils/sort-query.js +240 -13
  129. package/dist/utils/text-comparand.d.ts +92 -0
  130. package/dist/utils/text-comparand.js +99 -0
  131. package/dist/utils/without-denied-fields.d.ts +49 -0
  132. package/dist/utils/without-denied-fields.js +52 -0
  133. package/dist/validation/index.d.ts +8 -4
  134. package/dist/validation/index.js +8 -4
  135. package/dist/validation/schema-validator.d.ts +1 -1
  136. package/dist/validation/schema-validator.js +225 -83
  137. package/dist/validation/server-owned-value.d.ts +5 -1
  138. package/dist/validation/server-owned-value.js +5 -1
  139. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  140. package/dist/validation/validators/object-validation-engine.js +14 -3
  141. package/package.json +8 -9
  142. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  143. package/dist/data-scope/DataScopeManager.js +0 -229
  144. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  145. package/dist/protocols/ResponsiveProtocol.js +0 -158
  146. package/dist/validation/validation-engine.d.ts +0 -88
  147. package/dist/validation/validation-engine.js +0 -434
@@ -5,53 +5,6 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
- /**
9
- * `@object-ui/core` — ElementDataSource resolution (objectstack#5576).
10
- *
11
- * `PageComponentSchema.dataSource` (the spec's `ElementDataSourceSchema`) is the
12
- * per-element data binding a page component uses to query a different object
13
- * than the page's own context:
14
- *
15
- * ```json
16
- * { "object": "account", "view": "hot", "filter": { … }, "sort": [ … ], "limit": 20 }
17
- * ```
18
- *
19
- * This module owns the two pure halves of consuming it — telling the METADATA
20
- * apart from a runtime data-source adapter, and composing a named saved view
21
- * with the binding's own keys. Both are needed in two places (the render path in
22
- * `@object-ui/plugin-list`, and `ViewDataProvider.resolveElementDataSource`), so
23
- * they live here rather than being written twice.
24
- *
25
- * ## Composition semantics
26
- *
27
- * The spec's own `describe()` text settles the one case that could go either
28
- * way. `filter` is documented as "**Additional** filter criteria" — additional
29
- * to what the named view already restricts — so a view filter and a binding
30
- * filter COMBINE under `and`; the binding can only narrow the view, never widen
31
- * it. That is also the safe direction for AI-authored metadata: a typo in a
32
- * per-element filter cannot silently expose rows the saved view excluded.
33
- *
34
- * Nothing else in the binding is described as additional, and the remaining keys
35
- * are single-valued (one ordering, one cap), so for those the rule is **the view
36
- * provides the baseline and an explicit key overrides it**:
37
- *
38
- * | key | source | rule |
39
- * |------------|-----------------------------------------|-----------------------------|
40
- * | `object` | binding (required) | binding wins |
41
- * | `columns` | view only — the binding has no such key | view |
42
- * | `filter` | view + binding | AND-combined ("additional") |
43
- * | `sort` | view or binding | binding overrides view |
44
- * | `limit` | view (`pagination.pageSize`) or binding | binding overrides view |
45
- * | `viewType` | view only | view |
46
- *
47
- * A lone `filter` — only the view has one, or only the binding does — is passed
48
- * through in the shape it was stored in; only the two-source case is lowered to
49
- * an ObjectQL AST, because that is what combining requires.
50
- *
51
- * `viewType` rides along because `list-view` IS the multi-kind view container
52
- * (its registry `inputs` enumerate grid/kanban/gallery/…): naming a saved kanban
53
- * view and rendering a grid would be a silently wrong answer.
54
- */
55
8
  import { mergeFilterNodes } from '../utils/filter-converter.js';
56
9
  import { resolveViewId } from '../utils/resolve-view-id.js';
57
10
  const isPlainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
@@ -128,8 +81,42 @@ export function resolveSavedView(views, requested, object) {
128
81
  const id = resolveViewId(requested, Object.keys(views), object);
129
82
  return id === undefined ? undefined : views[id];
130
83
  }
131
- /** Read a saved view's row cap — `pagination.pageSize`, or a flat `limit`. */
132
- function savedViewLimit(view) {
84
+ /**
85
+ * What the contract admits as a row cap.
86
+ *
87
+ * A deliberate LOCAL RESTATEMENT of the predicate the repaired read points
88
+ * spell as `isUsableRowLimit` / `isUsablePageSize`. It is ⛔ not imported and
89
+ * ⛔ not extracted to a shared helper: this family now spells the same rule at
90
+ * each site that enforces it, and `isUsableRowLimit` / `isUsablePageSize` is
91
+ * what enumerates them. Hoisting it into one module would be a cross-package
92
+ * move nobody has chartered, and it would take the rule out of the file whose
93
+ * reader needs to see it. The cost — one more copy that could drift — is
94
+ * written here rather than hidden.
95
+ *
96
+ * The destination is declared positive at both ends: the spec's element data
97
+ * source declares `limit` a positive integer, and a saved view's pagination
98
+ * declares `pageSize` a positive integer with a default. So `0` is not a
99
+ * spelling whose meaning this layer may choose — it is a value the contract
100
+ * already refuses.
101
+ */
102
+ function isUsableRowLimit(value) {
103
+ return typeof value === 'number' && Number.isInteger(value) && value > 0;
104
+ }
105
+ /**
106
+ * The row cap a saved view CARRIES, before the contract is applied to it —
107
+ * `pagination.pageSize`, or a flat `limit`.
108
+ *
109
+ * Split out from {@link savedViewLimit} so the resolver and the diagnostic read
110
+ * the same carrier by construction. Two readers would be free to drift, and the
111
+ * drift would be invisible: a value one of them reports and the other keeps, or
112
+ * the reverse.
113
+ *
114
+ * ⚠️ Selection is still by `typeof === 'number'`, unchanged: a non-numeric
115
+ * `pagination.pageSize` falls through to the flat `limit` exactly as it did
116
+ * before. Only the POSITIVITY question is new, and it is asked once, by
117
+ * {@link isUsableRowLimit}, on whichever carrier won.
118
+ */
119
+ function savedViewRawLimit(view) {
133
120
  if (!view)
134
121
  return undefined;
135
122
  const pagination = view.pagination;
@@ -138,6 +125,167 @@ function savedViewLimit(view) {
138
125
  }
139
126
  return typeof view.limit === 'number' ? view.limit : undefined;
140
127
  }
128
+ /**
129
+ * Read a saved view's row cap — `pagination.pageSize`, or a flat `limit` —
130
+ * and hand back only a cap the contract admits.
131
+ *
132
+ * ## Why DROP, and not clamp or refuse
133
+ *
134
+ * Before this guard, `typeof … === 'number'` admitted `0`, negatives and
135
+ * fractions, and they lowered unchecked into the composed `limit`. Measured on
136
+ * both consumers of that key rather than inferred, and the two answers differ:
137
+ *
138
+ * - through a RENDERER, the refused value reached a block that has its own
139
+ * guard, so the block dropped it and drew its own default — the named view's
140
+ * cap went missing and the read went WIDER than the view asked for;
141
+ * - through `ViewDataProvider.resolveElementDataSource`, which forwards this
142
+ * key to `DataFetcher.fetchRecords` with NO guard of its own, `0`, `-10` and
143
+ * `25.5` reached the fetcher verbatim.
144
+ *
145
+ * ⛔ CLAMP is refused: this layer has no default to clamp to. Every consuming
146
+ * block owns its own default and `ViewDataProvider` owns none, so a number
147
+ * invented here would override a default the author never asked it to, and
148
+ * quietly substituting a number the author never wrote is the quieter half of
149
+ * this same defect.
150
+ *
151
+ * ⛔ THROWING is refused: this function is pure and sits under every block that
152
+ * can be bound to a view, so a throw would take out the page over one
153
+ * declaration — worse than the defect.
154
+ *
155
+ * ⇒ DROP. An absent `limit` is the honest statement that the view supplied no
156
+ * usable cap, and it is the one thing every consumer already handles.
157
+ *
158
+ * The loud half is {@link elementDataSourceRefusedLimitMessage}; see its doc for
159
+ * why the message is built here and reported by the caller.
160
+ */
161
+ function savedViewLimit(view) {
162
+ const raw = savedViewRawLimit(view);
163
+ return isUsableRowLimit(raw) ? raw : undefined;
164
+ }
165
+ /**
166
+ * Read the binding's own row cap, and hand back only a cap the contract admits.
167
+ *
168
+ * The other operand of the `limit` chain gets the same positivity check as
169
+ * {@link savedViewLimit}, through the same predicate. Maintainer ruling on
170
+ * objectui#10016 (option A): a binding `limit` the contract refuses (`0`, a
171
+ * negative, a fraction, a value that is not a number at all) is NOT AUTHORED.
172
+ * It yields exactly as an absent one does, to the saved view's usable cap and,
173
+ * when that is absent too, to the consumer's own default.
174
+ *
175
+ * This is objectui#10009's precedent applied as a rule rather than re-decided:
176
+ * there, one layer up, a value the contract refuses was ruled not authored and
177
+ * the other source won. Here the same question is asked of the two operands of
178
+ * one resolver, and it gets the same answer. The repo therefore holds ONE
179
+ * precedence rule for "whose row cap is used", not one per pair of operands.
180
+ *
181
+ * The two answers the ruling refused, and why:
182
+ *
183
+ * - ⛔ dropping the binding's cap AND skipping the view's: a bad value on the
184
+ * binding would then discard the view's legitimate cap, which is the least
185
+ * explicable outcome for an author;
186
+ * - ⛔ refusing loudly and rendering nothing: a blank region at render time
187
+ * makes an author's slip expensive without making it any harder to write.
188
+ * The loud refusal belongs on the WRITE surface, not here.
189
+ *
190
+ * `null` stays what `??` always made it: absent.
191
+ */
192
+ function bindingLimit(config) {
193
+ return isUsableRowLimit(config.limit) ? config.limit : undefined;
194
+ }
195
+ /**
196
+ * The binding operand's half of {@link elementDataSourceRefusedLimitMessage}.
197
+ * Module-private: callers reach it through that one exported builder, so there
198
+ * is still exactly one channel for a refused row cap, whichever operand it was.
199
+ *
200
+ * Unlike the view's message it needs no condition on the other operand. A
201
+ * usable binding cap is used whatever the view carries, so a refused one is
202
+ * ALWAYS a cap the consumer did not get, and saying so is always true.
203
+ */
204
+ function refusedBindingLimitMessage(binding, viewName, object) {
205
+ const raw = binding?.limit;
206
+ if (raw === undefined || raw === null)
207
+ return null;
208
+ if (isUsableRowLimit(raw))
209
+ return null;
210
+ const where = viewName
211
+ ? `dataSource binding on ${object} (view "${viewName}")`
212
+ : `dataSource binding on ${object}`;
213
+ // `String('20')` would print a string exactly like the number it is not.
214
+ const shown = typeof raw === 'number' ? String(raw) : String(JSON.stringify(raw));
215
+ return (`[ObjectUI] ElementDataSource: the ${where} declares \`limit: ${shown}\`, `
216
+ + 'which is not a positive integer. A row cap must be a positive integer '
217
+ + '(the spec declares this binding’s `limit` positive), so the binding’s cap '
218
+ + 'was treated as not authored and ignored, exactly as if the binding declared '
219
+ + 'no `limit`: the cap comes from the next source that declares a usable one, '
220
+ + 'or else from the consumer’s own default.');
221
+ }
222
+ /**
223
+ * The diagnostic half of {@link savedViewLimit} and {@link bindingLimit}.
224
+ * `null` means "nothing to say".
225
+ *
226
+ * ## Which operand
227
+ *
228
+ * `operand` picks the refusal to describe; it defaults to `'view'`, the one
229
+ * this builder described before objectui#10016. Each operand gets its own
230
+ * sentence, and each names its operand, so the two refusals stay
231
+ * distinguishable when both fire. They do fire together when the binding AND
232
+ * the view each carry a refused cap: two declarations, one message each.
233
+ *
234
+ * `binding` is the binding as authored. The view's sentence needs it too: it
235
+ * says the consumer falls back to its own default, and that is only true when
236
+ * the binding supplied no usable cap. A usable binding cap is used whatever the
237
+ * view carries, so a refused view cap under one changed nothing and is not
238
+ * reported. That condition lives HERE, once, rather than at each caller,
239
+ * because every caller needs it and two copies could drift.
240
+ *
241
+ * ## Why a BUILDER here, and not a warning from the composer
242
+ *
243
+ * This is the shape {@link elementDataSourceViewNotFoundMessage} already
244
+ * established in this module, for the reason its doc gives: built here so every
245
+ * caller reports the same defect the same way. It is followed rather than
246
+ * re-decided because the alternative is ruled out mechanically —
247
+ * {@link composeElementDataSource} is PURE and the render path calls it from a
248
+ * `useMemo`, so a warning emitted inside it would fire during render and fire
249
+ * again on every re-render. The repaired relay one layer up emits from an
250
+ * effect for exactly that reason; a pure function has no effect to emit from,
251
+ * so it hands the caller the words instead.
252
+ *
253
+ * ## Why the message has to exist at all
254
+ *
255
+ * Dropping alone would be a SILENT change on the renderer path: today the
256
+ * refused value travels as far as a block whose own guard reports it once, and
257
+ * after this repair that block receives nothing and correctly says nothing —
258
+ * an absent key is not a mistake. So the repair would remove the only place the
259
+ * author was being told. The layer that makes the decision is the layer that
260
+ * reports it.
261
+ *
262
+ * ⛔ NOT a second guard: the predicate lives once, in {@link isUsableRowLimit},
263
+ * and the view's carrier is read once, by {@link savedViewRawLimit}; this reads
264
+ * both.
265
+ *
266
+ * ⚠️ It speaks only about a cap THIS layer dropped. A non-numeric
267
+ * `pagination.pageSize` never became a limit here, before or after, so there is
268
+ * nothing for this layer to report about it. A non-numeric binding `limit` is
269
+ * different: `??` used to pass it through, so this layer now drops it and says
270
+ * so.
271
+ */
272
+ export function elementDataSourceRefusedLimitMessage(view, viewName, object, binding, operand = 'view') {
273
+ if (operand === 'binding')
274
+ return refusedBindingLimitMessage(binding, viewName, object);
275
+ if (isUsableRowLimit(binding?.limit))
276
+ return null;
277
+ const raw = savedViewRawLimit(view);
278
+ if (raw === undefined)
279
+ return null;
280
+ if (isUsableRowLimit(raw))
281
+ return null;
282
+ const where = viewName ? `saved view "${viewName}" on ${object}` : `saved view on ${object}`;
283
+ return (`[ObjectUI] ElementDataSource: ${where} declares a row cap of ${String(raw)}, `
284
+ + 'which is not a positive integer. A row cap must be a positive integer '
285
+ + '(the spec declares this binding’s `limit` positive, and refuses a zero '
286
+ + 'or negative `pagination.pageSize`), so the view’s cap was ignored and no '
287
+ + 'cap was lowered from it — the consuming block uses its own default.');
288
+ }
141
289
  /**
142
290
  * Read a saved view's render kind. `type` is the spec's key (and what
143
291
  * `ObjectView` reads); `viewType` is objectui's legacy spelling, folded by
@@ -165,7 +313,7 @@ function savedViewColumns(view) {
165
313
  *
166
314
  * Pure — the caller resolves the view (see {@link resolveSavedView}) and owns
167
315
  * the "named a view that does not exist" decision. Passing `undefined` /`null`
168
- * for `view` composes a binding with no view, which is exactly the pre-#5576
316
+ * for `view` composes a binding with no view, which is exactly the pre-objectstack-ai/objectstack#5576
169
317
  * behaviour for the other four keys.
170
318
  *
171
319
  * See the module doc for the per-key rule table; `filter` is the only key that
@@ -194,7 +342,10 @@ export function composeElementDataSource(config, view) {
194
342
  const sort = config.sort ?? view?.sort;
195
343
  if (sort !== undefined)
196
344
  composed.sort = sort;
197
- const limit = config.limit ?? savedViewLimit(view);
345
+ // One rule for both operands (objectui#10016): a refused cap is not
346
+ // authored, so a refused binding `limit` yields to the view's cap exactly as
347
+ // an absent one does.
348
+ const limit = bindingLimit(config) ?? savedViewLimit(view);
198
349
  if (limit !== undefined)
199
350
  composed.limit = limit;
200
351
  const viewType = savedViewType(view);
@@ -207,10 +358,149 @@ export function composeElementDataSource(config, view) {
207
358
  * have. Built here so the render path and `ViewDataProvider` report the same
208
359
  * defect the same way, and so the known-view list (the thing that actually gets
209
360
  * an author unstuck) is never omitted by one caller.
361
+ *
362
+ * ## Why the empty case may not say "this object has no saved views"
363
+ *
364
+ * An EMPTY `knownViews` reaches this function from two different worlds, and
365
+ * this function cannot tell them apart:
366
+ *
367
+ * 1. the saved views were read and there are none;
368
+ * 2. the read never successfully happened.
369
+ *
370
+ * World 2 is not hypothetical and not a bug to be fixed upstream: the shipped
371
+ * `ObjectStackAdapter.listViews` contract degrades EVERY failure — refused,
372
+ * offline, malformed — to `[]` on the RESOLVED path (deliberate, and objectui#8151
373
+ * keeps it), so a broken read arrives at `useElementDataSource` as a successful
374
+ * empty answer. The hook's own discrimination catches a REJECTION, which is a
375
+ * shape that contract never produces, so nothing separates the two worlds by the
376
+ * time the count lands here.
377
+ *
378
+ * Framework decision objectstack#13906 decision 1 option A — *a thing that could
379
+ * not be READ is not a thing that is ABSENT* — therefore binds this branch: it
380
+ * may only assert what is true in BOTH worlds. It names the possibilities and
381
+ * commits to neither, because committing is exactly the claim nobody established.
382
+ *
383
+ * ⛔ Do not "restore" the shorter sentence by inferring failure from emptiness in
384
+ * either direction — emptiness is what both worlds produce, so it can never tell
385
+ * them apart. Saying more than this needs the failure FACT plumbed in from the
386
+ * adapter (objectui#8151's surface), not a better guess here.
210
387
  */
211
388
  export function elementDataSourceViewNotFoundMessage(object, view, knownViews) {
212
389
  const known = knownViews.length
213
390
  ? `Known views: ${[...knownViews].sort().join(', ')}.`
214
- : 'This object has no saved views.';
391
+ : 'No saved views are known for this object. It may have none, or they could not be read.';
215
392
  return `dataSource.view "${view}" was not found on object "${object}". ${known}`;
216
393
  }
394
+ /* ------------------------------------------------------------------ *
395
+ * The DECLARATION half — one copy, next to the semantics (objectui#6678)
396
+ * ------------------------------------------------------------------ */
397
+ /**
398
+ * The spec key this module resolves, spelled once.
399
+ *
400
+ * Every consumer that needs the NAME (the registry injection, its gates, the
401
+ * pins) reads it from here rather than repeating the literal, so "which key is
402
+ * this?" has exactly one answer in the tree.
403
+ */
404
+ export const ELEMENT_DATA_SOURCE_KEY = 'dataSource';
405
+ /**
406
+ * The authoring-surface declaration of {@link ELEMENT_DATA_SOURCE_KEY} — the one
407
+ * copy, emitted into every registration that wraps the runtime gate.
408
+ *
409
+ * ## Why it lives here and is injected rather than written per block
410
+ *
411
+ * `PageComponentSchema.dataSource` is READ by `ElementDataSourceGate` in
412
+ * `@object-ui/react` on behalf of every object-bound block that wraps itself in
413
+ * it — nine of them at the time of writing. It was declared by NONE: `validateTree`
414
+ * skips `BASE_PROPS` and otherwise looks a prop up in the component's `inputs`,
415
+ * and `dataSource` was in neither. So the html tier reported the one spelling
416
+ * that resolves a saved view — the only one that works — with the identical
417
+ * `unknown-prop` warning it gives the spellings that do nothing, on the tier
418
+ * meant to accept AI-authored pages, where the diagnostic IS the contract
419
+ * (objectui#6678, the reporter of objectui#6598 gave up on exactly this).
420
+ *
421
+ * The maintainer ruling of 2026-08-29 took option B **in the injection form**:
422
+ * emitted mechanically at the wrapping seam, so a block declares the key from
423
+ * the same place that reads it. Nine hand-kept copies is the shape that ruling
424
+ * refused — they drift, and a tenth block would simply forget. Adding the key to
425
+ * `sdui-parser`'s `BASE_PROPS` (option A) was refused too: that set mirrors
426
+ * `BaseSchema`, and silencing `dataSource` on blocks that do NOT read it
427
+ * (`flex`, `card`) would make the diagnostic lie in the other direction.
428
+ *
429
+ * ## Why `object`, and not a bare `'object'` kind
430
+ *
431
+ * The `binding: 'object'` marker is what tells a consumer this input names an
432
+ * OBJECT — `validateTree` records it as a binding site, and the designer offers
433
+ * an object picker rather than a free-text blob. The `type` stays the coarse
434
+ * `'object'` kind because the value is a record; the two words are unrelated and
435
+ * both are correct here.
436
+ *
437
+ * ## Typed as the framework's injected input (`9e37d9b39`)
438
+ *
439
+ * `binding` is not a `ComponentInput` member. The maintainer ruling of
440
+ * 2026-09-07 answered the card's product question — may an ordinary
441
+ * registration declare a binding input? — with no, so the ONE writer of the
442
+ * key is this constant, typed by `InjectedComponentInput` from
443
+ * `@object-ui/types`: a `ComponentInput` plus the marker. The hand-written
444
+ * inline type literal that used to sit here was the tell that the key had no
445
+ * authored home, and the splice in `Registry.register` reached the `inputs`
446
+ * array through an `as ComponentMeta` cast. Both are gone: the shape is now
447
+ * checked here and at the splice, and a registration that writes `binding`
448
+ * itself is an excess-property `tsc` error.
449
+ */
450
+ export const ELEMENT_DATA_SOURCE_INPUT = {
451
+ name: ELEMENT_DATA_SOURCE_KEY,
452
+ type: 'object',
453
+ binding: 'object',
454
+ description: 'Per-element data binding: { object, view, filter, sort, limit }. Overrides the ' +
455
+ 'page-level object context for this block; `view` names a saved view of `object`, ' +
456
+ 'and `filter` is ANDed with the view’s own rather than replacing it.',
457
+ };
458
+ /**
459
+ * The renderers that wrap the runtime gate.
460
+ *
461
+ * A `WeakSet` rather than a property on the component: marking must not mutate a
462
+ * `React.forwardRef` exotic object or show up when a test snapshots a
463
+ * registration, and an unregistered renderer must stay collectable.
464
+ */
465
+ const elementDataSourceBlocks = new WeakSet();
466
+ /**
467
+ * Declare that `renderer` wraps `ElementDataSourceGate`, so its registrations
468
+ * publish {@link ELEMENT_DATA_SOURCE_INPUT}.
469
+ *
470
+ * This is the SEAM, and it is deliberately the only way in: `Registry.register`
471
+ * injects the declaration for exactly the renderers marked here, so the key is
472
+ * published by whatever consumes the gate and by nothing else — which is what
473
+ * keeps the `flex` / `card` direction of the ruling true.
474
+ * `scripts/check-element-data-source-declaration.mjs` fails any source that
475
+ * consumes the gate without passing through it, so a new block cannot forget.
476
+ *
477
+ * ## Two import paths, ONE name and ONE function
478
+ *
479
+ * `@object-ui/react` re-exports this beside `ElementDataSourceGate` so the seam is
480
+ * findable next to the thing it is about. That export is for DISCOVERABILITY:
481
+ * every CALL SITE imports from here, and
482
+ * `scripts/check-element-data-source-declaration.mjs` enforces it.
483
+ *
484
+ * The rule is measured, not stylistic. A registration runs at MODULE SCOPE, and
485
+ * `element:record_picker`'s registration in `@object-ui/components` is the
486
+ * worked example. 101 suites in this repo partially
487
+ * mock `@object-ui/react` by hand-listing the exports they return, and a
488
+ * module-scope read of a name those lists do not carry throws at COLLECTION
489
+ * time — the importing test file fails before it runs a single assertion.
490
+ * Measured: 17 files, all four CI shards, zero failed assertions among them.
491
+ * Nothing mocks `@object-ui/core`, and `@object-ui/components` already imports
492
+ * `ComponentRegistry` from it at module scope, so this path adds no coupling
493
+ * that was not already there.
494
+ */
495
+ export function elementDataSourceBlock(renderer) {
496
+ if (renderer && (typeof renderer === 'object' || typeof renderer === 'function')) {
497
+ elementDataSourceBlocks.add(renderer);
498
+ }
499
+ return renderer;
500
+ }
501
+ /** Whether `renderer` was marked by {@link elementDataSourceBlock}. */
502
+ export function isElementDataSourceBlock(renderer) {
503
+ return (!!renderer
504
+ && (typeof renderer === 'object' || typeof renderer === 'function')
505
+ && elementDataSourceBlocks.has(renderer));
506
+ }
@@ -1,12 +1,17 @@
1
1
  /**
2
2
  * @object-ui/core - DataScope Module
3
3
  *
4
- * Runtime data scope management for row-level security and
5
- * reactive data state within the UI component tree.
4
+ * Resolution of the data a view or a page element reads: the spec's
5
+ * `ViewData` union (`ViewDataProvider`) and the per-element
6
+ * `ElementDataSource` binding.
7
+ *
8
+ * Row-level security is not evaluated here. The platform declares it once, as
9
+ * the CEL predicate of a `@objectstack/spec` row-level security policy, and
10
+ * enforces it on the server; objectui#7750 retired the client-side row-level
11
+ * filter evaluator this module used to export.
6
12
  *
7
13
  * @module data-scope
8
14
  * @packageDocumentation
9
15
  */
10
- export { DataScopeManager, defaultDataScopeManager, type RowLevelFilter, type DataScopeConfig, } from './DataScopeManager.js';
11
16
  export { ViewDataProvider, type ViewDataConfig, type DataFetcher, type ResolvedData, } from './ViewDataProvider.js';
12
- export { collectSavedViews, composeElementDataSource, elementDataSourceViewNotFoundMessage, isElementDataSourceConfig, resolveSavedView, type ComposedElementDataSource, type ElementDataSourceConfig, type ElementDataSourceSort, type ElementSavedView, } from './element-data-source.js';
17
+ export { collectSavedViews, composeElementDataSource, ELEMENT_DATA_SOURCE_INPUT, ELEMENT_DATA_SOURCE_KEY, elementDataSourceBlock, elementDataSourceRefusedLimitMessage, elementDataSourceViewNotFoundMessage, isElementDataSourceBlock, isElementDataSourceConfig, resolveSavedView, type ComposedElementDataSource, type ElementDataSourceConfig, type ElementDataSourceSort, type ElementSavedView, } from './element-data-source.js';
@@ -1,12 +1,17 @@
1
1
  /**
2
2
  * @object-ui/core - DataScope Module
3
3
  *
4
- * Runtime data scope management for row-level security and
5
- * reactive data state within the UI component tree.
4
+ * Resolution of the data a view or a page element reads: the spec's
5
+ * `ViewData` union (`ViewDataProvider`) and the per-element
6
+ * `ElementDataSource` binding.
7
+ *
8
+ * Row-level security is not evaluated here. The platform declares it once, as
9
+ * the CEL predicate of a `@objectstack/spec` row-level security policy, and
10
+ * enforces it on the server; objectui#7750 retired the client-side row-level
11
+ * filter evaluator this module used to export.
6
12
  *
7
13
  * @module data-scope
8
14
  * @packageDocumentation
9
15
  */
10
- export { DataScopeManager, defaultDataScopeManager, } from './DataScopeManager.js';
11
16
  export { ViewDataProvider, } from './ViewDataProvider.js';
12
- export { collectSavedViews, composeElementDataSource, elementDataSourceViewNotFoundMessage, isElementDataSourceConfig, resolveSavedView, } from './element-data-source.js';
17
+ export { collectSavedViews, composeElementDataSource, ELEMENT_DATA_SOURCE_INPUT, ELEMENT_DATA_SOURCE_KEY, elementDataSourceBlock, elementDataSourceRefusedLimitMessage, elementDataSourceViewNotFoundMessage, isElementDataSourceBlock, isElementDataSourceConfig, resolveSavedView, } from './element-data-source.js';
@@ -35,6 +35,47 @@ export interface EvaluationOptions {
35
35
  * @default true
36
36
  */
37
37
  sanitize?: boolean;
38
+ /**
39
+ * Fault passback: called with the failure reason when this evaluation could
40
+ * not be performed, at the moment the evaluator ALREADY knows it faulted.
41
+ *
42
+ * ## Why a passback and not a second evaluation (objectui#6038)
43
+ *
44
+ * The only fault-detection channel this class used to offer a fail-soft
45
+ * caller was `throwOnError`, and on the CEL branch `evaluateCelCondition`
46
+ * implements that by evaluating TWICE (once with each fallback — a value
47
+ * that tracks the fallback both times is a fault). A caller that wants to
48
+ * *observe* a fault while keeping the fail-soft verdict therefore had to pay
49
+ * for a second engine call per predicate per node per render. That price is
50
+ * exactly why the node gate in `SchemaRenderer` bought its diagnostic with a
51
+ * `__DEV__` gate and shipped production silent.
52
+ *
53
+ * This option costs nothing: every fault site below is already inside a
54
+ * `catch`, or already holds the engine's own failure reason. The verdict is
55
+ * untouched on every path — `onFault` is invoked for its side effect and its
56
+ * return value is ignored.
57
+ *
58
+ * ## Supplying it TRANSFERS reporting to the caller
59
+ *
60
+ * The built-in `console.warn`s on these paths are suppressed while it is
61
+ * set, so one fault stays one line — the caller's, which can name the node
62
+ * the predicate belongs to. This mirrors, one layer up, the contract
63
+ * `FieldPredicateDiagnostic` (`fieldRules.ts`) already documents for the
64
+ * canonical CEL engine: `warn: false` plus an `onFault` passback, so
65
+ * silencing the generic line never discards the description of *why* the
66
+ * predicate failed. On the CEL branch this option is forwarded to exactly
67
+ * that seam rather than reimplementing it.
68
+ *
69
+ * Independent of {@link throwOnError}, which converts a fault into a throw
70
+ * and is the fail-CLOSED contract; this one keeps the historical fail-soft
71
+ * answer and merely says so out loud. `throwOnError` still wins where both
72
+ * are set: the throw happens first and is the caller's own signal.
73
+ *
74
+ * Must not throw — it is invoked outside the evaluation guard, so an
75
+ * exception here propagates to the caller rather than being reported as an
76
+ * evaluation fault.
77
+ */
78
+ onFault?: (reason: string) => void;
38
79
  }
39
80
  /**
40
81
  * Expression evaluator for dynamic UI expressions
@@ -96,6 +137,51 @@ export declare class ExpressionEvaluator {
96
137
  * syntax) throws; a genuine `false` never throws.
97
138
  */
98
139
  private evaluateCelCondition;
140
+ /**
141
+ * Answer a BLANK gate predicate — the ONE diagnosed guard of
142
+ * {@link evaluateCondition}, on both of its routes (objectui#8069 put it on
143
+ * the `{ dialect: 'cel' }` route; objectui#11262 routes the legacy path's
144
+ * blank here too, so the two are one report rather than two).
145
+ *
146
+ * The verdict is `true` — no predicate → visible/enabled, exactly as before
147
+ * (objectui#3850 / #3960's verdict) — in EVERY mode, `throwOnError` included,
148
+ * since the author wrote nothing that could fault. What it is not is SILENT
149
+ * (ADR-0137 D4: a blank gate predicate is "diagnosed, never a silent
150
+ * `true`"). The report is `evalFieldPredicate`'s own `[blank]` one — the
151
+ * channel every other predicate fault already uses — so a caller that passed
152
+ * `onFault` receives it there (and prints its own, node-named line), and
153
+ * every other caller gets the deduped built-in warning. No engine call is
154
+ * made: a blank predicate never reaches the engine.
155
+ *
156
+ * The locator names a CEL gate on both routes: a blank has no syntax to put
157
+ * it in either dialect, and a bare-string predicate is CEL by the spec's
158
+ * contract. One locator is also what keeps `''` and `{ dialect: 'cel',
159
+ * source: '' }` on one dedupe key.
160
+ */
161
+ private answerBlankGate;
162
+ /**
163
+ * Answer a gate that is DECLARED but cannot be evaluated (objectui#11358) —
164
+ * see `isUnevaluablePredicate` (`./unevaluablePredicate.ts`) for the shapes.
165
+ *
166
+ * It is a fault, not a blank: {@link answerBlankGate} answers `true` in every
167
+ * mode because a blank says nothing, while this value says "there is a gate"
168
+ * and the client cannot read it. So the modes split exactly as they do for a
169
+ * predicate that faults with text in it:
170
+ *
171
+ * - `throwOnError` → THROW. The caller opted into fail-closed semantics,
172
+ * and its catch both hides / disables and reports, naming the control —
173
+ * the same contract `evaluateCelCondition` keeps (no warning AND a throw
174
+ * for one fault, objectstack-ai/objectstack#5149).
175
+ * - otherwise → reported once through `evalFieldPredicate`'s own report
176
+ * (the `[unevaluable]` reason, beside `[blank]`), or handed to the
177
+ * caller's `onFault`, and answered with the fail-soft `true`. On a
178
+ * `disabled` / `hidden` leg that `true` is the CLOSED side (greyed out /
179
+ * hidden), on `visible` / `enabled` / `condition` the open one — each
180
+ * key's existing fault direction, unchanged.
181
+ *
182
+ * No engine call is made: there is no `source` to hand it.
183
+ */
184
+ private answerUnevaluableGate;
99
185
  /**
100
186
  * Update the context with new data
101
187
  */