@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
@@ -61,8 +61,22 @@
61
61
  * logs `treating the field as LOCKED` instead). The *default* stays
62
62
  * fail-open on purpose — flipping it is a shipped-behavior change tracked
63
63
  * separately in objectstack#5149 (appeal 1, undecided).
64
+ *
65
+ * A predicate the author DECLARED and left BLANK is loud too, since
66
+ * objectui#8069 — and it did not used to be. It is a third state, not a
67
+ * spelling of either neighbour: the key is present (so it is not "no rule")
68
+ * and nothing evaluates (so no engine fault is raised), and it used to return
69
+ * the caller's permissive fallback before any warning could fire. `''` and
70
+ * `' '` are authorable — `ExpressionWireSchema` is a bare `z.string()`, and
71
+ * `resolveFieldRuleState`'s own guard is `!= null` — so the one state an author
72
+ * reaches by *starting* a rule and not finishing it was the one state that said
73
+ * nothing at all. Both spellings now report `[blank]` through the same single
74
+ * site as every other fault; the VERDICT is unchanged for every input.
64
75
  */
65
76
  import { ExpressionEngine } from '@objectstack/formula';
77
+ import { subjectCapabilitiesOf, subjectPermissionsOf } from './subjectPermissions.js';
78
+ import { isBlankPredicateText } from './declaredPredicate.js';
79
+ import { isUnevaluablePredicate, UNEVALUABLE_PREDICATE_REASON } from './unevaluablePredicate.js';
66
80
  /** Normalize a predicate into the `Expression` shape the engine expects. */
67
81
  function toExpression(pred) {
68
82
  if (typeof pred === 'string')
@@ -70,15 +84,35 @@ function toExpression(pred) {
70
84
  return { dialect: (pred.dialect ?? 'cel'), source: pred.source };
71
85
  }
72
86
  const warnedPredicates = new Set();
87
+ /**
88
+ * The `reason` reported for a predicate the author DECLARED and left blank.
89
+ *
90
+ * Tagged like an engine reason (`[parse]`, `[type]`, `[throw]`) because it
91
+ * travels the same two channels — the built-in warning and the `onFault`
92
+ * passback — and a caller that routes on the tag must be able to tell this
93
+ * apart from a predicate that faulted with text in it: the fix for a blank one
94
+ * is to finish it or delete the key, never to debug its syntax.
95
+ */
96
+ const BLANK_PREDICATE_REASON = '[blank] the predicate is declared but empty — nothing to evaluate';
73
97
  /**
74
98
  * One-time warning for a predicate that could not be evaluated. Deduped per
75
99
  * predicate TEXT (dialect + source): a broken predicate is re-evaluated on
76
100
  * every render/keystroke, and the point is one loud line, not a scrolling
77
101
  * wall. The dedupe key is JSON-encoded — never a control-character separator
78
102
  * (objectstack#5450 made a sibling of this file binary to grep that way).
103
+ *
104
+ * ⚠️ One class joins the LOCATOR to that key: a BLANK predicate. The text is
105
+ * what identifies the authoring site for every other fault — it carries the
106
+ * typo — but every blank predicate in an app shares the key `""`, so text
107
+ * alone would let the first blank rule silence every other author's. Non-blank
108
+ * keys are unchanged (objectui#8069).
79
109
  */
80
110
  function warnPredicateFailure(expr, fallback, reason, context) {
81
- const key = JSON.stringify([expr.dialect, expr.source]);
111
+ // `Expression['source']` is optional in the spec; an absent source is blank
112
+ // by the same rule as a whitespace-only one, and lands in the same key shape.
113
+ const key = expr.source?.trim()
114
+ ? JSON.stringify([expr.dialect, expr.source])
115
+ : JSON.stringify([expr.dialect, expr.source, context ?? '']);
82
116
  if (warnedPredicates.has(key))
83
117
  return;
84
118
  warnedPredicates.add(key);
@@ -92,12 +126,54 @@ function warnPredicateFailure(expr, fallback, reason, context) {
92
126
  /**
93
127
  * Evaluate a field-rule CEL predicate against a record.
94
128
  *
129
+ * ## `fallback` is a per-CALL-SITE policy, not a default this helper owns
130
+ *
131
+ * The parameter is required and carries no default value, so every call site
132
+ * states a direction of its own — and the directions in this repo do not
133
+ * agree. Five distinct fault policies share this one helper today, which is
134
+ * readable nowhere but here (objectui#8069):
135
+ *
136
+ * 1. **Fixed permissive, paired with an equal "no rule" literal.**
137
+ * {@link resolveFieldRuleState} (`true` / `false` / `false`) and
138
+ * `resolveVisibleOptions` (`evaluator/optionRules.ts`, `true`). A fault
139
+ * produces the permissive verdict, indistinguishable from the verdict for
140
+ * "the author wrote no rule" — see the `…_FAULTED` / `…_ABSENT` note below.
141
+ * 2. **Fixed permissive, unguarded — ONE literal answers absent *and*
142
+ * faulted.** Every visibility call site outside core: the form renderer
143
+ * (`components/renderers/form/form.tsx`), console's `FormPage`, app-shell's
144
+ * `ScreenView`, `plugin-form`'s `WizardForm`. All of them pass `true`, and
145
+ * none has a separate absent branch at all.
146
+ * 3. **Caller-parameterised.** `evalRowPredicate`'s single-eval fast route
147
+ * (`evaluator/listConditional.ts`) forwards `opts.fallback`, so the
148
+ * direction is the mounting surface's to state.
149
+ * 4. **Divergence probe → fault FLAG.** `evalCel`
150
+ * (`evaluator/listConditional.ts`) calls this helper once per direction with
151
+ * `warn: false` + `onFault`: a verdict that tracks the fallback in BOTH runs
152
+ * is a fault, and the caller emits its own labelled warning and returns its
153
+ * own fallback.
154
+ * 5. **Divergence probe → THROW.** `ExpressionEvaluator.evaluateCelCondition`
155
+ * under `throwOnError` runs the same two-call trick and converts the
156
+ * disagreement into `throw new Error('CEL predicate failed to evaluate: …')`.
157
+ *
158
+ * ⚠️ The last two policies exist **because** `fallback` is free to specify: they
159
+ * detect a fault by disagreeing with themselves. Fixing a direction *inside*
160
+ * this helper would remove the mechanism they are built on, which is why
161
+ * ADR-0137 D5 keeps `fallback` free and declares the direction at the
162
+ * field-rule and gate layer instead.
163
+ *
95
164
  * @param pred The `visibleWhen` / `readonlyWhen` / `requiredWhen` predicate.
96
165
  * @param record The live form values (overlays prior persisted record).
97
- * @param fallback Value to return when the predicate is absent or fails to
98
- * evaluate. Pick the *safe* default for the caller:
99
- * `false` for readonly/required (don't lock/block on error),
100
- * `true` for visibility (don't hide on error).
166
+ * @param fallback Value to return when the predicate is ABSENT (`null` /
167
+ * `undefined`), and — separately — the value returned when a
168
+ * present predicate cannot be evaluated. ⚠️ Those are two
169
+ * questions this one parameter answers with one value; a
170
+ * caller that wants different answers must branch before the
171
+ * call, as {@link resolveFieldRuleState} does. The historic
172
+ * advice is to pick the *safe* default (`false` for
173
+ * readonly/required — don't lock/block on error; `true` for
174
+ * visibility — don't hide on error). ADR-0137 kept those
175
+ * directions for RENDER (D3) and put the answer to a broken
176
+ * field rule at SUBMIT (D2) — see {@link FieldRuleFaults}.
101
177
  * @param previous The prior persisted record, if any (for `previous.*` refs).
102
178
  * @param scope Extra top-level scope variables bound alongside `record` —
103
179
  * e.g. `{ parent }` so an inline line-item cell can reference
@@ -109,30 +185,68 @@ function warnPredicateFailure(expr, fallback, reason, context) {
109
185
  * is unaffected either way.
110
186
  */
111
187
  export function evalFieldPredicate(pred, record, fallback, previous, scope, diagnostic) {
112
- if (pred == null || (typeof pred === 'string' && !pred.trim()))
188
+ if (pred == null)
113
189
  return fallback;
114
190
  const expr = toExpression(pred);
115
- // The two fault sources — a not-ok verdict and a throw that slipped past the
116
- // engine's "never throws" contract — converge on one reason string and one
117
- // reporting site below, so the built-in warning and the `onFault` passback
118
- // can never describe the failure differently.
191
+ // The fault sources — a blank predicate, a not-ok verdict, and a throw that
192
+ // slipped past the engine's "never throws" contract — converge on one reason
193
+ // string and one reporting site below, so the built-in warning and the
194
+ // `onFault` passback can never describe the failure differently.
119
195
  let reason;
120
196
  let value = fallback;
121
- try {
122
- const res = ExpressionEngine.evaluate(expr, {
123
- record,
124
- previous,
125
- ...(scope ? { extra: scope } : {}),
126
- });
127
- // Parse error, type error, unbound identifier, engine fault … — every
128
- // not-ok verdict resolves to the fallback, but never silently (#5149).
129
- if (!res.ok)
130
- reason = `[${res.error.kind}] ${res.error.message}`;
131
- else
132
- value = res.value === true;
197
+ if (isBlankPredicateText(pred)) {
198
+ // Declared-and-blank, in EITHER spelling. Answered here rather than by the
199
+ // engine for two reasons: the bare-string spelling never reached the engine
200
+ // at all (this line used to `return fallback` before any warning could
201
+ // fire — objectui#8069), and the envelope spelling reached it and came back
202
+ // with `AST-only evaluation not yet supported; persist \`source\`` for
203
+ // `{ source: '' }`, a reason that sends the author looking for a
204
+ // serialization bug. `isBlankPredicateText` is the repo's one definition of
205
+ // this question (objectui#3960) — a fourth local `trim()` here is exactly
206
+ // the drift it was consolidated to stop.
207
+ reason = BLANK_PREDICATE_REASON;
133
208
  }
134
- catch (err) {
135
- reason = `[throw] ${err instanceof Error ? err.message : String(err)}`;
209
+ else if (isUnevaluablePredicate(pred)) {
210
+ // Declared, and nothing here can be evaluated: an envelope with no string
211
+ // `source` (an `ast`-only one), a number, `{}`, an array (objectui#11358).
212
+ // Answered here, beside the blank, for the blank's reasons: there is no
213
+ // `source` to hand the engine, and the engine's answer for one —
214
+ // `AST-only evaluation not yet supported; persist \`source\`` — was what
215
+ // a `0` reported too, which sends its author looking for a serializer.
216
+ // One reason, one tag, the same two channels.
217
+ reason = UNEVALUABLE_PREDICATE_REASON;
218
+ }
219
+ else {
220
+ try {
221
+ // objectui#4421 — the acting subject's effective object permissions, the
222
+ // one input `current_user.can(object, verb)` is answered from. They ride
223
+ // on the subject the scope already carries (`subjectPermissions.ts`),
224
+ // never under a scope key, because every key of `scope` is a CEL root
225
+ // and the engine keeps this map out of the variable namespace. A subject
226
+ // with none passes none, and `can` then refuses loudly: that fault goes
227
+ // through the caller's `fallback` like any other.
228
+ const permissions = subjectPermissionsOf(scope?.current_user);
229
+ // objectui#13006 — the subject's held system capabilities, the one input
230
+ // `current_user.hasCapability(name)` is answered from, carried the same
231
+ // way and for the same reason; absent stays absent, so the call refuses.
232
+ const systemPermissions = subjectCapabilitiesOf(scope?.current_user);
233
+ const res = ExpressionEngine.evaluate(expr, {
234
+ record,
235
+ previous,
236
+ ...(scope ? { extra: scope } : {}),
237
+ ...(permissions !== undefined ? { permissions } : {}),
238
+ ...(systemPermissions !== undefined ? { systemPermissions } : {}),
239
+ });
240
+ // Parse error, type error, unbound identifier, engine fault … — every
241
+ // not-ok verdict resolves to the fallback, but never silently (objectstack-ai/objectstack#5149).
242
+ if (!res.ok)
243
+ reason = `[${res.error.kind}] ${res.error.message}`;
244
+ else
245
+ value = res.value === true;
246
+ }
247
+ catch (err) {
248
+ reason = `[throw] ${err instanceof Error ? err.message : String(err)}`;
249
+ }
136
250
  }
137
251
  if (reason !== undefined) {
138
252
  if (diagnostic?.warn !== false) {
@@ -145,6 +259,60 @@ export function evalFieldPredicate(pred, record, fallback, previous, scope, diag
145
259
  }
146
260
  return value;
147
261
  }
262
+ /**
263
+ * The verdict {@link resolveFieldRuleState} applies when a rule's predicate
264
+ * CANNOT BE EVALUATED — parse error, unbound identifier, engine fault, or a
265
+ * predicate the author left blank.
266
+ *
267
+ * ⚠️ These answer **"what should the form do when this rule is BROKEN?"** —
268
+ * *not* "what should the form do when the author declared NO rule?" That
269
+ * second question is answered separately, by the `…_ABSENT` set below. The two
270
+ * sets hold pairwise EQUAL values today, and that equality is a copy, not an
271
+ * implication: each `rules.<key> != null` ternary answers the absent case with
272
+ * its own literal and hands `evalFieldPredicate` the same literal as the fault
273
+ * fallback beside it, so every permissive value is written twice and the "it
274
+ * broke" answer was chosen by aligning it with the "it is absent" answer next
275
+ * to it. Nothing was *inherited* from a shared default — `evalFieldPredicate`'s
276
+ * `fallback` parameter is required and has none (objectui#8069 measured both
277
+ * halves).
278
+ *
279
+ * All three directions are the PERMISSIVE one, so a single mistyped column in
280
+ * one authored predicate yields a form that shows more, locks less and demands
281
+ * less — three faults from one typo, composing rather than cancelling.
282
+ * ADR-0137 answered that without moving any of the three: they stay the RENDER
283
+ * direction (D3 — a faulting `visibleWhen` shows the field), and the composite
284
+ * is closed at SUBMIT instead (D2), from the `faults` report
285
+ * {@link resolveFieldRuleState} returns beside the verdicts.
286
+ *
287
+ * **What the history records** (read on full, unshallowed history — a shallow
288
+ * clone answers this with one commit and no warning): the direction *was*
289
+ * reasoned about, once, when the helper landed (objectui#1578). This module's
290
+ * head and ADR-0036 both record the same rationale — the fallbacks are "chosen
291
+ * so a fault is *safe*: `true` for visibility (don't hide content on error),
292
+ * `false` for required/readonly (don't block submit or lock a field on
293
+ * error)". What no commit records is the COMPOSITION: every recorded argument
294
+ * is per-key, and the case where all three faults arrive from one typo was
295
+ * never put. objectstack#5149 then removed the SILENCE and left the direction
296
+ * explicitly undecided ("appeal 1").
297
+ *
298
+ * ⛔ These values are shipped behaviour, declared by ADR-0137 D3. Changing one
299
+ * is not a refactor — and `6a449fc49` leans on the `visibleWhen` half staying
300
+ * fail-open (a broken predicate must never silently null a stored column).
301
+ */
302
+ const VISIBLE_WHEN_FAULTED = true;
303
+ const READONLY_WHEN_FAULTED = false;
304
+ const REQUIRED_WHEN_FAULTED = false;
305
+ /**
306
+ * The verdict {@link resolveFieldRuleState} applies when the author declared NO
307
+ * rule for that key at all — the `: <literal>` arm of each `!= null` ternary.
308
+ *
309
+ * Spelled apart from the `…_FAULTED` set above because they answer a different
310
+ * question, not because they differ: changing one of THESE changes what an
311
+ * unconditional field does, which is a louder and quite separate decision.
312
+ */
313
+ const VISIBLE_WHEN_ABSENT = true;
314
+ const READONLY_WHEN_ABSENT = false;
315
+ const REQUIRED_WHEN_ABSENT = false;
148
316
  /**
149
317
  * Resolve the effective `{ visible, readonly, required }` state for a field
150
318
  * given its conditional rules and the live record. Each `*When` rule, when
@@ -174,18 +342,27 @@ export function evalFieldPredicate(pred, record, fallback, previous, scope, diag
174
342
  * `"field 'amount'"`. The warning then reads
175
343
  * `visibleWhen of field 'amount' …`; without it, the rule
176
344
  * kind alone is reported.
345
+ * @returns The three verdicts, plus `faults` — see {@link FieldRuleFaults} for
346
+ * what a submit path reads from it and what it leaves out.
177
347
  */
178
348
  export function resolveFieldRuleState(rules, record, statics, previous, scope, fieldContext) {
349
+ const faults = {};
350
+ // `onFault` fires beside the built-in warning, not instead of it (`warn`
351
+ // stays on), so recording the report costs no diagnostic and no second
352
+ // engine call.
179
353
  const diag = (rule) => ({
180
354
  context: fieldContext ? `${rule} of ${fieldContext}` : rule,
355
+ onFault: (reason) => {
356
+ faults[rule] = reason;
357
+ },
181
358
  });
182
359
  const visible = rules.visibleWhen != null
183
- ? evalFieldPredicate(rules.visibleWhen, record, true, previous, scope, diag('visibleWhen'))
184
- : true;
360
+ ? evalFieldPredicate(rules.visibleWhen, record, VISIBLE_WHEN_FAULTED, previous, scope, diag('visibleWhen'))
361
+ : VISIBLE_WHEN_ABSENT;
185
362
  const readonly = statics.readonly === true ||
186
363
  (rules.readonlyWhen != null
187
- ? evalFieldPredicate(rules.readonlyWhen, record, false, previous, scope, diag('readonlyWhen'))
188
- : false);
364
+ ? evalFieldPredicate(rules.readonlyWhen, record, READONLY_WHEN_FAULTED, previous, scope, diag('readonlyWhen'))
365
+ : READONLY_WHEN_ABSENT);
189
366
  // Short-circuited, not evaluated-and-discarded: the verdict cannot depend on
190
367
  // the predicate, so running it would only spend an engine call per field per
191
368
  // render. A broken `requiredWhen` still warns wherever it decides something
@@ -195,7 +372,7 @@ export function resolveFieldRuleState(rules, record, statics, previous, scope, f
195
372
  ? false
196
373
  : statics.required === true ||
197
374
  (rules.requiredWhen != null
198
- ? evalFieldPredicate(rules.requiredWhen, record, false, previous, scope, diag('requiredWhen'))
199
- : false);
200
- return { visible, readonly, required };
375
+ ? evalFieldPredicate(rules.requiredWhen, record, REQUIRED_WHEN_FAULTED, previous, scope, diag('requiredWhen'))
376
+ : REQUIRED_WHEN_ABSENT);
377
+ return { visible, readonly, required, faults };
201
378
  }
@@ -10,8 +10,10 @@ export * from './ExpressionEvaluator.js';
10
10
  export * from './predicateInput.js';
11
11
  export * from './declaredPredicate.js';
12
12
  export * from './fieldRules.js';
13
+ export * from './subjectPermissions.js';
13
14
  export * from './rowPredicateCanon.js';
14
15
  export * from './listConditional.js';
16
+ export * from './deadlineSettled.js';
15
17
  export * from './optionRules.js';
16
18
  export * from './optionLint.js';
17
19
  export * from './ExpressionCache.js';
@@ -10,8 +10,10 @@ export * from './ExpressionEvaluator.js';
10
10
  export * from './predicateInput.js';
11
11
  export * from './declaredPredicate.js';
12
12
  export * from './fieldRules.js';
13
+ export * from './subjectPermissions.js';
13
14
  export * from './rowPredicateCanon.js';
14
15
  export * from './listConditional.js';
16
+ export * from './deadlineSettled.js';
15
17
  export * from './optionRules.js';
16
18
  export * from './optionLint.js';
17
19
  export * from './ExpressionCache.js';
@@ -44,8 +44,10 @@ export interface RowPredicateOptions {
44
44
  * row-action `visible`/`disabled`; `false` (no style) for formatting. */
45
45
  fallback?: boolean;
46
46
  /** Extra top-level scope merged alongside the row — e.g. the global predicate
47
- * scope (`features` / `user` / `app`) a host shell provides. The row wins on
48
- * collision: a `record` or `data` key here never shadows the row. */
47
+ * scope (`features` / `user`) a host shell provides. The row wins on
48
+ * collision: a `record` key here never shadows the row. Every OTHER key —
49
+ * a host's own `data` included — reaches the predicate as the host's own
50
+ * (`83fe6e741`: `data` no longer names the row on a record surface). */
49
51
  scope?: Record<string, unknown>;
50
52
  /** When true, log a one-time warning if a *present* predicate faults. */
51
53
  warnOnError?: boolean;
@@ -53,16 +55,16 @@ export interface RowPredicateOptions {
53
55
  label?: string;
54
56
  /**
55
57
  * This surface has **no row of its own** — bind NOTHING for the row instead
56
- * of binding an empty one, so a `record` / `data` the HOST SCOPE carries
57
- * survives to the predicate (objectui#4640).
58
+ * of binding an empty one, so a `record` the HOST SCOPE carries survives to
59
+ * the predicate (objectui#4640).
58
60
  *
59
61
  * The default (`false`) is this function's whole subject rule: the row is
60
- * pinned over the scope, on both dialect paths, so `record` / `data` always
61
- * name THIS row even when the host scope carries keys of those names
62
+ * pinned over the scope as `record`, on both dialect paths, so `record`
63
+ * always names THIS row even when the host scope carries a key of that name
62
64
  * (objectui#3796). That is right for every row surface — and exactly wrong
63
65
  * for a surface that has no row, because the pin then writes an EMPTY
64
- * `record` / `data` over the host's real ones and every `record.*` predicate
65
- * faults with "No such key". "This surface has no row of its own" and "this
66
+ * `record` over the host's real one and every `record.*` predicate faults
67
+ * with "No such key". "This surface has no row of its own" and "this
66
68
  * surface's row is empty" are different facts; only the latter is entitled to
67
69
  * shadow the scope. Same distinction, same words, as `usePredicateRecordContext`
68
70
  * in `@object-ui/react` (objectui#4075) — which is the BINDING half of this
@@ -86,38 +88,105 @@ export interface RowPredicateOptions {
86
88
  }
87
89
  /**
88
90
  * Evaluate a single boolean predicate against a row record on the canonical CEL
89
- * engine (with a legacy-dialect fallback — see the module note). The row's
90
- * fields are bound three ways so every authoring convention resolves:
91
- * `record.status`, bare `status` (row-action shorthand), and `data.status`.
92
- *
93
- * ⚠️ Those three are NOT peers, and this doc comment used to read as though
94
- * they were. **The canon is `record.*`** (maintainer ruling 2026-08-20 on
95
- * objectui#5330, option B); the other two are client tolerances in a
96
- * deprecation window, kept because stored metadata carries them and warned
97
- * about — from the CEL path below — by `warnNonCanonicalRowSpelling`. The
98
- * server accepts `record.*` and NOTHING else: measured on
99
- * `@objectstack/formula@17.1.0`, `buildScope({ record })` mounts exactly
100
- * `['record']`, so a bare field faults `Unknown variable: status` there and a
101
- * `data.*` predicate faults `Unknown variable: data`. See
102
- * {@link ./rowPredicateCanon.ts} for the full measurement, for why `data.*` is
103
- * the dangerous one (it is silently ACCEPTED by the server's authoring oracle
104
- * and still binds nothing at runtime), and for why the deprecation is scoped to
105
- * this runtime layer rather than declared platform-wide (`data` is the
106
- * canonical root of a metadata-editing form — ADR-0089 D3).
107
- *
108
- * ⛔ No spelling is removed from the binding, and none may be before a
109
- * stored-metadata survey sizes the window — that is part of the same ruling.
110
- *
111
- * The optional `scope` (host predicate scope) is bound
112
- * alongside so `features.*` / `user.*` predicates keep working — but the row is
113
- * the subject: `record` and `data` always name THIS row, on both dialect paths,
114
- * even when the host scope carries keys of those names (objectui#3796).
91
+ * engine (with a legacy-dialect fallback — see the module note). The row is
92
+ * bound ONE way: as `record.*` — **the canon** (maintainer ruling 2026-08-20 on
93
+ * objectui#5330, option B; Phase 2 executed by `83fe6e741`).
94
+ *
95
+ * Until Phase 2 the row was also bound as bare fields (`status`, the row-action
96
+ * shorthand) and as `data.*`, and Phase 1 (`d1ab06f0f`) warned once per
97
+ * non-canonical spelling. Both bindings and that warning are gone. A bare-field
98
+ * or `data.*` predicate on a record surface now FAULTS here exactly as it always
99
+ * did on the server — measured on `@objectstack/formula@17.1.0`,
100
+ * `buildScope({ record })` mounts exactly `['record']`, so a bare field faults
101
+ * `Unknown variable: status` and `data.*` faults `Unknown variable: data` — and
102
+ * takes this function's EXISTING fault policy: the caller's `fallback`, reported
103
+ * once by `warnEvalError` (when `warnOnError` is set) or by the canonical
104
+ * helper's own one-time warning, either of which names the unknown variable.
105
+ * Nothing detects a retired spelling on this path; it is simply unbound. The
106
+ * same holds on the legacy `${…}` path below — one scope shape per surface,
107
+ * both dialects — so `${data.x}` / `${x}` strings on a row surface fault there
108
+ * too, and land in the same fallback / warning.
109
+ *
110
+ * The retirement is scoped to the RUNTIME RECORD layer. `data` is the canonical
111
+ * root one layer over, in a metadata-editing form (ADR-0089 D3,
112
+ * `CANONICAL_ROOT_BY_LAYER` = `{ runtime: 'record', metadata: 'data' }`), and
113
+ * that layer evaluates through its own entry (`app-shell`'s metadata-admin
114
+ * `predicate.ts`), never through this one. See {@link ./rowPredicateCanon.ts}
115
+ * for the canon statement, the server measurement and the offline detector.
116
+ *
117
+ * The optional `scope` (host predicate scope) is bound alongside so
118
+ * `features.*` / `user.*` predicates keep working — but the row is the subject:
119
+ * `record` always names THIS row, on both dialect paths, even when the host
120
+ * scope carries a key of that name (objectui#3796). Every OTHER host key —
121
+ * a host's own `data` included — reaches the predicate as the host's own.
115
122
  *
116
123
  * A caller with no row at all passes {@link RowPredicateOptions.rowless}, and
117
124
  * then nothing is bound over the scope — see that option for why an absent row
118
125
  * must not be spelled as an empty one.
119
126
  */
120
127
  export declare function evalRowPredicate(pred: FieldRulePredicate | undefined | null, row: Record<string, unknown> | null | undefined, opts?: RowPredicateOptions): boolean;
128
+ /** The two halves of a {@link partitionRowsByPredicate} verdict over a selection. */
129
+ export interface RowPartition<TRow> {
130
+ /** Records whose predicate passed — the ones an operation may act on. */
131
+ eligible: TRow[];
132
+ /**
133
+ * How many records were filtered out. A caller that shrinks a user's
134
+ * selection owes them this number: a run over fewer records than they picked
135
+ * must say so rather than quietly shrinking.
136
+ */
137
+ skipped: number;
138
+ }
139
+ /**
140
+ * Split a set of records by one per-record predicate — the SET-shaped
141
+ * counterpart of {@link evalRowPredicate}, and the reason a bulk gate never
142
+ * needs a hook.
143
+ *
144
+ * A bulk affordance spans N records, so its gate is a **loop**, and React
145
+ * forbids calling `useRowPredicate` (or any hook) per iteration. Every bulk
146
+ * surface therefore reaches for `evalRowPredicate` directly; this wrapper is
147
+ * that loop written once, with the three cases each caller was otherwise
148
+ * re-deriving:
149
+ *
150
+ * - **absent** (`null` / `undefined` / `''`) — nothing is gated, so `rows` is
151
+ * returned BY REFERENCE and `skipped` is 0. The common case allocates
152
+ * nothing and a caller's downstream memo still holds. Whitespace-only text
153
+ * and an envelope whose `source` is blank are NOT absent here: they are
154
+ * evaluated, and fail closed like any other predicate that cannot run.
155
+ * - **boolean** — a verdict, not an expression, short-circuited exactly as
156
+ * `useCondition` / `useRowPredicate` do. Handing `true` to the engine
157
+ * produces `{ dialect: 'cel', source: undefined }`, which faults and — on
158
+ * this fail-closed path — would disqualify every record, so the most
159
+ * explicit way to say "always" would exclude everyone (objectui#3492).
160
+ * - **expression** — evaluated once per record with that record in scope,
161
+ * **fail-closed**: a predicate that faults must not hand the caller records
162
+ * it was written to exclude. `warnOnError` (default `true`) makes the
163
+ * resulting exclusion diagnosable once per predicate instead of silent.
164
+ *
165
+ * The fail-closed default is what separates this from a lenient record-free
166
+ * evaluation, and the difference is not mere strictness: evaluated with NO
167
+ * record bound, `record.status != 'paid'` returns `true` for every row —
168
+ * including the ones it was written to exclude — so an authored gate is not
169
+ * weakened, it is inverted for half its inputs (objectui#3067).
170
+ *
171
+ * ## Who calls it, and who decides "is a gate declared?" (objectui#11322)
172
+ *
173
+ * This is a shared partitioner, and its opening test is not the action
174
+ * family's "is a gate declared?" question. Its callers:
175
+ *
176
+ * - the bulk ACTION fold, plugin-grid's `partitionBulkRows` (a bulk def's
177
+ * `visible`). The action family decides "declared?" BEFORE calling here,
178
+ * through `hasDeclaredVisibilityGate`, and hands an undeclared gate over as
179
+ * `undefined`. So a whitespace-only `visible` or a blank-`source` envelope
180
+ * never reaches the test below from that caller.
181
+ * - the built-in Delete's `userActions.delete.visibleWhen`, a field-rule key,
182
+ * from plugin-grid's `ObjectGrid` and from plugin-list's `ListView`
183
+ * (`bulkDeleteEligibility`). The test below is THEIR answer: a blank one
184
+ * that is not `''` is evaluated and excludes every record.
185
+ *
186
+ * A caller that needs the action family's answer asks it first; it does not
187
+ * write a second blank test in front of this one.
188
+ */
189
+ export declare function partitionRowsByPredicate<TRow extends Record<string, unknown>>(pred: FieldRulePredicate | boolean | undefined | null, rows: readonly TRow[], opts?: Omit<RowPredicateOptions, 'fallback' | 'rowless'>): RowPartition<TRow>;
121
190
  /** A conditional-formatting rule in any of the three historical shapes. */
122
191
  export type ConditionalFormattingRuleLike = {
123
192
  /** Spec shape: a CEL predicate (`ExpressionInputSchema`). */