@object-ui/core 17.6.0 → 17.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/CHANGELOG.md +6610 -0
  2. package/README.md +101 -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 +288 -27
  6. package/dist/actions/ActionRunner.js +443 -63
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/actionKeys.d.ts +9 -7
  9. package/dist/actions/actionKeys.js +57 -14
  10. package/dist/actions/actionResponse.js +2 -2
  11. package/dist/actions/index.d.ts +1 -0
  12. package/dist/actions/index.js +1 -0
  13. package/dist/actions/recordDelete.d.ts +118 -0
  14. package/dist/actions/recordDelete.js +112 -0
  15. package/dist/adapters/ValueDataSource.d.ts +6 -1
  16. package/dist/adapters/ValueDataSource.js +1090 -98
  17. package/dist/adapters/batchTransaction.d.ts +1 -1
  18. package/dist/adapters/batchTransaction.js +1 -1
  19. package/dist/builder/schema-builder.d.ts +38 -81
  20. package/dist/builder/schema-builder.js +33 -152
  21. package/dist/data-scope/ViewDataProvider.d.ts +1 -1
  22. package/dist/data-scope/ViewDataProvider.js +21 -1
  23. package/dist/data-scope/element-data-source.d.ts +261 -7
  24. package/dist/data-scope/element-data-source.js +342 -52
  25. package/dist/data-scope/index.d.ts +9 -4
  26. package/dist/data-scope/index.js +9 -4
  27. package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
  28. package/dist/evaluator/ExpressionEvaluator.js +184 -12
  29. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  30. package/dist/evaluator/FormulaFunctions.js +166 -27
  31. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  32. package/dist/evaluator/declaredPredicate.js +117 -16
  33. package/dist/evaluator/fieldRules.d.ts +88 -4
  34. package/dist/evaluator/fieldRules.js +203 -31
  35. package/dist/evaluator/index.d.ts +1 -0
  36. package/dist/evaluator/index.js +1 -0
  37. package/dist/evaluator/listConditional.d.ts +103 -34
  38. package/dist/evaluator/listConditional.js +127 -50
  39. package/dist/evaluator/optionRules.d.ts +77 -6
  40. package/dist/evaluator/optionRules.js +87 -10
  41. package/dist/evaluator/predicateInput.d.ts +15 -4
  42. package/dist/evaluator/predicateInput.js +35 -2
  43. package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
  44. package/dist/evaluator/rowPredicateCanon.js +65 -115
  45. package/dist/evaluator/subjectPermissions.d.ts +76 -0
  46. package/dist/evaluator/subjectPermissions.js +44 -0
  47. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  48. package/dist/evaluator/unevaluablePredicate.js +70 -0
  49. package/dist/index.d.ts +14 -0
  50. package/dist/index.js +80 -2
  51. package/dist/protocols/index.d.ts +0 -1
  52. package/dist/protocols/index.js +9 -1
  53. package/dist/registry/Registry.d.ts +358 -31
  54. package/dist/registry/Registry.js +263 -11
  55. package/dist/registry/WidgetRegistry.js +7 -3
  56. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  57. package/dist/registry/html-tier-intrinsics.js +110 -0
  58. package/dist/registry/public-blocks.d.ts +8 -0
  59. package/dist/registry/public-blocks.js +57 -0
  60. package/dist/theme/ThemeEngine.js +1 -1
  61. package/dist/utils/chart-category-key.d.ts +82 -0
  62. package/dist/utils/chart-category-key.js +84 -0
  63. package/dist/utils/chart-measure-key.d.ts +64 -0
  64. package/dist/utils/chart-measure-key.js +65 -0
  65. package/dist/utils/chart-presentation.d.ts +95 -112
  66. package/dist/utils/chart-presentation.js +169 -104
  67. package/dist/utils/chart-series.d.ts +131 -5
  68. package/dist/utils/chart-series.js +98 -16
  69. package/dist/utils/column-sortability.d.ts +157 -0
  70. package/dist/utils/column-sortability.js +217 -0
  71. package/dist/utils/dashboard-filters.d.ts +38 -6
  72. package/dist/utils/dashboard-filters.js +107 -61
  73. package/dist/utils/dataset-format.d.ts +211 -30
  74. package/dist/utils/dataset-format.js +389 -46
  75. package/dist/utils/date-display.d.ts +354 -0
  76. package/dist/utils/date-display.js +463 -0
  77. package/dist/utils/date-macros.js +7 -0
  78. package/dist/utils/drill-down.d.ts +78 -0
  79. package/dist/utils/drill-down.js +93 -0
  80. package/dist/utils/emptiness.d.ts +96 -0
  81. package/dist/utils/emptiness.js +101 -0
  82. package/dist/utils/expand-fields.d.ts +76 -6
  83. package/dist/utils/expand-fields.js +76 -6
  84. package/dist/utils/extract-records.d.ts +65 -5
  85. package/dist/utils/extract-records.js +65 -8
  86. package/dist/utils/filter-converter.d.ts +152 -4
  87. package/dist/utils/filter-converter.js +1092 -22
  88. package/dist/utils/filter-tokens.d.ts +49 -5
  89. package/dist/utils/filter-tokens.js +105 -33
  90. package/dist/utils/grouping-fields.d.ts +61 -0
  91. package/dist/utils/grouping-fields.js +84 -0
  92. package/dist/utils/managedBy.d.ts +3 -3
  93. package/dist/utils/managedBy.js +5 -5
  94. package/dist/utils/native-date-value.d.ts +48 -0
  95. package/dist/utils/native-date-value.js +167 -0
  96. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  97. package/dist/utils/non-grid-row-ceiling.js +141 -0
  98. package/dist/utils/normalize-list-view.d.ts +121 -1
  99. package/dist/utils/normalize-list-view.js +186 -6
  100. package/dist/utils/number-display.d.ts +63 -14
  101. package/dist/utils/number-display.js +34 -12
  102. package/dist/utils/object-aggregate-query.d.ts +113 -0
  103. package/dist/utils/object-aggregate-query.js +50 -0
  104. package/dist/utils/parent-scope.d.ts +123 -0
  105. package/dist/utils/parent-scope.js +140 -0
  106. package/dist/utils/predicate-fields.d.ts +45 -12
  107. package/dist/utils/predicate-fields.js +137 -12
  108. package/dist/utils/record-color.d.ts +75 -0
  109. package/dist/utils/record-color.js +65 -0
  110. package/dist/utils/record-source.d.ts +298 -0
  111. package/dist/utils/record-source.js +331 -0
  112. package/dist/utils/record-title.d.ts +148 -8
  113. package/dist/utils/record-title.js +220 -26
  114. package/dist/utils/reference-keys.d.ts +21 -22
  115. package/dist/utils/reference-keys.js +532 -29
  116. package/dist/utils/sort-query.d.ts +115 -15
  117. package/dist/utils/sort-query.js +240 -13
  118. package/dist/utils/text-comparand.d.ts +92 -0
  119. package/dist/utils/text-comparand.js +99 -0
  120. package/dist/utils/without-denied-fields.d.ts +49 -0
  121. package/dist/utils/without-denied-fields.js +52 -0
  122. package/dist/validation/index.d.ts +8 -4
  123. package/dist/validation/index.js +8 -4
  124. package/dist/validation/schema-validator.d.ts +1 -1
  125. package/dist/validation/schema-validator.js +200 -82
  126. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  127. package/dist/validation/validators/object-validation-engine.js +14 -3
  128. package/package.json +8 -9
  129. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  130. package/dist/data-scope/DataScopeManager.js +0 -229
  131. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  132. package/dist/protocols/ResponsiveProtocol.js +0 -158
  133. package/dist/validation/validation-engine.d.ts +0 -88
  134. package/dist/validation/validation-engine.js +0 -434
@@ -6,6 +6,7 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  import { evalFieldPredicate } from './fieldRules.js';
9
+ import { isEmptyValue } from '../utils/emptiness.js';
9
10
  /**
10
11
  * Normalize a field's `dependsOn` to the list of sibling field names it reacts
11
12
  * to. The lookup-only `{field,param}` form contributes just its `field`.
@@ -18,10 +19,12 @@ export function resolveDependsOnFields(dependsOn) {
18
19
  .map((d) => (typeof d === 'string' ? d : d && typeof d === 'object' ? d.field : null))
19
20
  .filter((f) => typeof f === 'string' && f.length > 0);
20
21
  }
21
- /** A value counts as "empty" (dependency unmet) when nullish, blank, or an empty array. */
22
- function isEmptyValue(v) {
23
- return v === undefined || v === null || v === '' || (Array.isArray(v) && v.length === 0);
24
- }
22
+ // A dependency counts as UNMET on exactly the shared floor — `null`,
23
+ // `undefined`, the empty string, the empty array — and this module is where
24
+ // those four members were first written down. objectui#8496 promoted them out
25
+ // of here into `utils/emptiness.ts` (byte-for-byte the same four) so the four
26
+ // other surfaces that had each re-spelled them could stop. No extension and no
27
+ // declension: a gated option list asks the floor and nothing more.
25
28
  /**
26
29
  * True when at least one `dependsOn` field is empty in the record — the option
27
30
  * list is gated and the widget should prompt for the parent rather than show an
@@ -45,6 +48,71 @@ export function resolveVisibleOptions(options, record, scope) {
45
48
  context: `visibleWhen of option '${String(o.value)}'`,
46
49
  }));
47
50
  }
51
+ /**
52
+ * The text an option widget SHOWS for one option: its `label`, or its `value`
53
+ * when the label is blank (`20b507aff`).
54
+ *
55
+ * ## Why a blank label reaches a renderer at all — it is a LEGAL document
56
+ *
57
+ * This is deliberately NOT the lenient renderer fallback AGENTS.md #0.1
58
+ * forbids. That rule governs metadata the contract REFUSES ("if the metadata
59
+ * is off-spec, fix it at the producer"); an empty label is metadata the
60
+ * contract ACCEPTS. Measured on the installed `@objectstack/spec` 17.6.0,
61
+ * `SelectOptionSchema`:
62
+ *
63
+ * { value: 'low', label: 'Low' } -> ACCEPT (lit control)
64
+ * { value: 'low', label: '' } -> ACCEPT <- the key reading
65
+ * { value: 'low' } -> REJECT invalid_type at [label]
66
+ *
67
+ * `@object-ui/types` mirrors that reading in prose on `SelectOptionBase`:
68
+ * "An empty string is a valid label; an ABSENT one is not." So the producer
69
+ * is right to emit `label: ''` — the field designer's `patchOptions` does,
70
+ * under the ruling `f0f774b0d` landed, and a pin
71
+ * (`ObjectFieldInspector.optionLabel.test.tsx`) fails if it ever invents
72
+ * content to fill the hole instead. A document every layer calls legal has to
73
+ * render as something a person can click.
74
+ *
75
+ * ## What the contract does NOT say, and who had already answered it
76
+ *
77
+ * The spec states no display default for a legal-but-blank label, so nothing
78
+ * upstream decides this. objectui had nevertheless already decided it — on
79
+ * HALF its read sites. Measured across the four option widgets before this
80
+ * helper existed, with `{ value: 'low', label: '' }` authored and
81
+ * `{ value: 'high', label: 'High' }` beside it as the lit control:
82
+ *
83
+ * | widget | readonly path | interactive path |
84
+ * | select | "low" | "" |
85
+ * | multiselect | "low" | "" |
86
+ * | radio | "low" | "" |
87
+ * | checkboxes | "low" | "" |
88
+ *
89
+ * Four summary paths spelled `opt?.label || value` and fell back; the four
90
+ * interactive paths spelled `{opt.label}` and rendered nothing. So the same
91
+ * option read "low" in a read-only form and blank in the editable one — the
92
+ * bug was never "tolerate or refuse", it was a widget family disagreeing with
93
+ * itself, and the blank half is the one a person has to click.
94
+ *
95
+ * This function is that decision written ONCE, the same "one definition, N
96
+ * surfaces" move as {@link CASCADE_OPTION_WIDGET_TYPES} below. All eight sites
97
+ * call it, so the two halves cannot drift apart again.
98
+ *
99
+ * ## Boundaries
100
+ *
101
+ * - Blank means TRIM-empty, so a whitespace-only label falls back too: it is
102
+ * the same invisible row to the person looking at it, and it is the
103
+ * spelling the producer's own reader already uses to call an authored
104
+ * `value` blank (`classifyOption`, `o.value.trim() === ''`).
105
+ * - `label` is typed required, but this reads defensively because renderers
106
+ * are handed unvalidated runtime metadata: a non-string label must fall back
107
+ * rather than throw where `{opt.label}` merely rendered nothing.
108
+ * - This changes DISPLAY only. It writes nothing, and it is not a licence to
109
+ * author blank labels — whether the contract should refuse them outright is
110
+ * a `packages/spec` question, not a renderer one.
111
+ */
112
+ export function optionDisplayLabel(option) {
113
+ const { label } = option;
114
+ return typeof label === 'string' && label.trim() !== '' ? label : String(option.value);
115
+ }
48
116
  /**
49
117
  * Whether a scalar field value is still valid given the currently-visible
50
118
  * options — used to decide a cascade clear (parent changed, child's old choice
@@ -112,12 +180,21 @@ export function resolveCascadingOptions(rawOptions, record, dependsOn, scope) {
112
180
  *
113
181
  * `dependentValues` is also the channel two widget-hint pickers read a specific
114
182
  * SIBLING KEY from (`filter-condition` reads `object_name`, `recipient-picker`
115
- * reads `recipient_type`), and the lookup family filters its query by it. All
116
- * three surfaces above agree today in NOT feeding those from action / bulk
117
- * dialog params, but that is a different wiring question from the one ruled
118
- * here and it has never been decided either way. Stated here so the three
119
- * copies of the statement become one — the boundary itself stays open:
120
- * objectui#4771.
183
+ * reads `recipient_type`), and the lookup family filters its query by it —
184
+ * neither of which is an option list, so ⛔ no member is ever added to THIS set
185
+ * to serve them. Each surface decides that separately, by OR-ing its own rule
186
+ * beside this one, and they do not agree:
187
+ *
188
+ * - the object form has fed both families all along — `needsDataSourceWiring`,
189
+ * its own line beside the one that reads this set;
190
+ * - the single-record action dialog feeds the lookup family since
191
+ * objectui#8672 ruling A — `paramNeedsDependentValues()`, same shape;
192
+ * - the bulk action dialog feeds neither, and nothing has ruled on it.
193
+ *
194
+ * ⇒ objectui#4771 is the open boundary for the widget-hint pickers everywhere,
195
+ * and for the lookup family on the bulk dialog. Stated here so the surfaces'
196
+ * copies of the statement become one; ⛔ do not read a member of this set as
197
+ * the answer to "does widget X get the record on surface Y".
121
198
  *
122
199
  * ## Normalization stays with the consumer
123
200
  *
@@ -9,7 +9,9 @@
9
9
  * The normalized shape {@link ExpressionEvaluator.evaluateCondition} consumes:
10
10
  * a boolean (short-circuits), a `${…}` template string (legacy JS path), a
11
11
  * `{ dialect: 'cel', source }` envelope (canonical `@objectstack/formula`
12
- * path), or `undefined` for "no predicate declared".
12
+ * path), `undefined` for "no predicate declared", or a `cel` envelope with NO
13
+ * `source` for a gate that is declared but cannot be evaluated — which the
14
+ * evaluator treats as a fault, never as "absent" (objectui#11358).
13
15
  *
14
16
  * ## Why not `PredicateInput` (objectstack#4115 / objectui#3074)
15
17
  *
@@ -24,7 +26,8 @@
24
26
  * admits `boolean` and the `${…}` template spelling (neither of which the spec
25
27
  * models as a predicate), narrows `dialect` to `cel` alone (every other dialect
26
28
  * has already been flattened onto the legacy path by `toPredicateInput`),
27
- * requires a non-empty `source`, and carries no `ast` / `meta`. The two unions
29
+ * requires a non-empty `source` (or none at all, for the declared-but-
30
+ * unevaluable state), and carries no `ast` / `meta`. The two unions
28
31
  * are not mutually assignable in either direction.
29
32
  *
30
33
  * The rename is pinned from both sides by the tripwire in
@@ -36,6 +39,9 @@
36
39
  export type EvaluatorPredicateInput = string | boolean | {
37
40
  dialect: 'cel';
38
41
  source: string;
42
+ } | {
43
+ readonly dialect: 'cel';
44
+ readonly source?: undefined;
39
45
  } | undefined;
40
46
  /**
41
47
  * Normalize a schema-supplied predicate (`visible` / `enabled` / `disabled` /
@@ -52,8 +58,13 @@ export type EvaluatorPredicateInput = string | boolean | {
52
58
  * authored predicate, including bare strings) → a `cel` dialect keeps
53
59
  * its envelope; every other dialect is unwrapped and wrapped as
54
60
  * `${source}`.
55
- * - `null` / `undefined` / empty / anything else → `undefined`
56
- * (default visible/enabled).
61
+ * - `null` / `undefined` / `''` / an envelope whose `source` is `''` →
62
+ * `undefined` (absent: default visible/enabled).
63
+ * - anything else that is present — an envelope with no string `source`
64
+ * (`{ dialect: 'cel', ast }`), a number, `{}`, an array → the frozen
65
+ * `{ dialect: 'cel' }` with no `source`: declared, and a FAULT to the
66
+ * evaluator (objectui#11358 — see `DECLARED_UNEVALUABLE` above). This used
67
+ * to fold into `undefined` with the absent shapes.
57
68
  *
58
69
  * ## Why the `cel` envelope must survive normalization (#2661 / #3314)
59
70
  *
@@ -5,6 +5,28 @@
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
+ import { isUnevaluablePredicate } from './unevaluablePredicate.js';
9
+ /**
10
+ * The normalized answer for a gate that is DECLARED but has nothing the client
11
+ * can evaluate (objectui#11358) — the fourth member of
12
+ * {@link EvaluatorPredicateInput}, a `cel` envelope with no `source`.
13
+ *
14
+ * It exists so the normalizer can tell three states apart instead of two.
15
+ * `undefined` is "absent" and evaluates to `true` (visible / enabled) by the
16
+ * evaluator's one documented default; this value is "declared and faulting"
17
+ * and `ExpressionEvaluator.evaluateCondition` treats it as a FAULT — reported,
18
+ * thrown to a `throwOnError` caller, and answered with the fail-soft `true`
19
+ * otherwise — so every reader gets the fail direction its key already has for
20
+ * a predicate that cannot be evaluated. Folding it into `undefined`, as this
21
+ * function did, made an `ast`-only envelope, `0`, `{}` or an array read as
22
+ * "no gate" everywhere, in silence (ADR-0137 D4).
23
+ *
24
+ * Shape, not a marker: it says what the evaluator receives — a CEL predicate
25
+ * (a bare predicate is CEL by the spec's contract) whose `source` is absent —
26
+ * and carries nothing of the original value, so no reader can guess what an
27
+ * `ast` meant. Frozen and shared, so a reader's memo sees one value per state.
28
+ */
29
+ const DECLARED_UNEVALUABLE = Object.freeze({ dialect: 'cel' });
8
30
  /**
9
31
  * Wrap a BARE expression as a `${…}` template, and leave a string that already
10
32
  * carries template syntax alone (objectui#3871 — rationale in
@@ -29,8 +51,13 @@ function wrapIfBare(expression) {
29
51
  * authored predicate, including bare strings) → a `cel` dialect keeps
30
52
  * its envelope; every other dialect is unwrapped and wrapped as
31
53
  * `${source}`.
32
- * - `null` / `undefined` / empty / anything else → `undefined`
33
- * (default visible/enabled).
54
+ * - `null` / `undefined` / `''` / an envelope whose `source` is `''` →
55
+ * `undefined` (absent: default visible/enabled).
56
+ * - anything else that is present — an envelope with no string `source`
57
+ * (`{ dialect: 'cel', ast }`), a number, `{}`, an array → the frozen
58
+ * `{ dialect: 'cel' }` with no `source`: declared, and a FAULT to the
59
+ * evaluator (objectui#11358 — see `DECLARED_UNEVALUABLE` above). This used
60
+ * to fold into `undefined` with the absent shapes.
34
61
  *
35
62
  * ## Why the `cel` envelope must survive normalization (#2661 / #3314)
36
63
  *
@@ -113,6 +140,10 @@ export function toPredicateInput(value) {
113
140
  return value;
114
141
  if (typeof value === 'string')
115
142
  return wrapIfBare(value);
143
+ // Declared, but nothing here can be evaluated (objectui#11358): kept as its
144
+ // own state rather than folded into "absent". See `DECLARED_UNEVALUABLE`.
145
+ if (isUnevaluablePredicate(value))
146
+ return DECLARED_UNEVALUABLE;
116
147
  if (typeof value === 'object' && typeof value.source === 'string') {
117
148
  const src = value.source;
118
149
  if (!src)
@@ -125,5 +156,7 @@ export function toPredicateInput(value) {
125
156
  // Every other dialect (template / unset) keeps the legacy `${…}` behavior.
126
157
  return wrapIfBare(src);
127
158
  }
159
+ // Unreachable: every present value is a boolean, a string, an envelope with a
160
+ // string `source`, or unevaluable — all answered above. Kept for the compiler.
128
161
  return undefined;
129
162
  }
@@ -26,8 +26,8 @@ export type NonCanonicalRowSpelling = {
26
26
  * predicate is already canonical (or is not this module's verdict to give).
27
27
  *
28
28
  * Deliberately conservative in both arms — every condition below can only
29
- * REMOVE a finding, never invent one, because a false deprecation warning sends
30
- * an author to rewrite a predicate that was correct:
29
+ * REMOVE a finding, never invent one, because a false finding sends an author
30
+ * to rewrite a predicate that was correct:
31
31
  *
32
32
  * - **Unparseable source** → `null`. Syntax is another gate's verdict.
33
33
  * - **Bare shorthand** is reported only when the undeclared identifier is an own
@@ -45,23 +45,12 @@ export type NonCanonicalRowSpelling = {
45
45
  * predicate manages both it is the one the author must fix to have anything
46
46
  * evaluate at all.
47
47
  *
48
- * @param source The predicate's CEL text. Callers must have already
49
- * routed legacy `${…}`-dialect strings elsewhere — in that
50
- * dialect `data.*` is the NORMAL spelling, and reporting it
51
- * here would be a false positive on every legacy predicate.
48
+ * @param source The predicate's CEL text. A legacy `${…}`-dialect string
49
+ * is not CEL and returns `null` here (unparseable); classify
50
+ * it by its own dialect's rules — on a runtime record surface
51
+ * it retired with the CEL spellings (`83fe6e741`), while on
52
+ * the schema/widget tier `data` is a different scope entirely.
52
53
  * @param row The row the predicate is bound against.
53
54
  * @param dataNamesRow Whether `data` is bound to that same row on this surface.
54
55
  */
55
56
  export declare function detectNonCanonicalRowSpelling(source: string, row: Record<string, unknown> | null | undefined, dataNamesRow: boolean): NonCanonicalRowSpelling | null;
56
- /** Reset the row-predicate spelling warn-once memo. Exported for tests. */
57
- export declare function resetRowPredicateCanonWarnings(): void;
58
- /**
59
- * Report a non-canonical row-predicate spelling once, in dev.
60
- *
61
- * Phase 1 of the objectui#5330 window: the binding is UNCHANGED and every
62
- * spelling still resolves — this only says so out loud, so the stored
63
- * population stops growing and a later survey has something to count. It is a
64
- * warning and deliberately not a refusal: turning it into one would move the
65
- * accept/reject set, which this card is explicitly not entitled to do.
66
- */
67
- export declare function warnNonCanonicalRowSpelling(source: string, row: Record<string, unknown> | null | undefined, dataNamesRow: boolean, label?: string): void;
@@ -6,70 +6,76 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  /**
9
- * The row-predicate spelling CANON, and the Phase-1 deprecation warning for the
10
- * two spellings that are not it (objectui#5330).
9
+ * The row-predicate spelling CANON: on a runtime record surface the row is bound
10
+ * as `record.*` and nothing else (objectui#5330, maintainer ruling 2026-08-20,
11
+ * option B; Phase 2 executed by `83fe6e741`).
11
12
  *
12
- * ## The canon (maintainer ruling, 2026-08-20 — option B)
13
+ * ## The canon
13
14
  *
14
15
  * A row predicate — `visible` / `disabled` / `enabled` on an action renderer, a
15
- * row scope, a `record:alert` — binds the row THREE ways today: canonical
16
- * `record.status`, bare shorthand `status`, and legacy `data.status`
17
- * (objectui#4075 / PR #4079 bound all four action renderers all three ways to
18
- * restore consistency, deliberately without deciding which of them is
19
- * CONTRACT). This module is where that decision now lives:
20
- *
21
- * > **The canon is `record.*`.** The bare shorthand and `data.*` are
22
- * > client-side tolerances in a deprecation window, kept because stored
23
- * > metadata carries them, warned about here, and removable only after a
24
- * > stored-metadata survey sizes the window (⛔ no removal before the survey).
25
- *
26
- * It mirrors the objectstack#7917 option-② precedent for the identical shape (a
27
- * renderer tolerance quietly becoming a second de-facto contract — AGENTS.md
28
- * #0.1), whose objectui half is `utils/dashboard-filters.ts`' bare-string
29
- * `options` shorthand: same three phases, same reason the warning is not
30
- * decoration (ADR-0078 — nothing silently inert; a tolerance nothing ever
31
- * reports can never be retired, because nothing would ever show that the last
32
- * document carrying it is gone).
33
- *
34
- * ## The canon states the SERVER's accept set, not this client's
35
- *
36
- * The ruling made that the dev's first measurement, because a canon that only
37
- * describes the renderer would be the very thing it exists to end. Measured
38
- * against `@objectstack/formula@17.1.0` — the engine the server evaluates with,
39
- * and the one `fieldRules.ts` already delegates to:
16
+ * row scope, a `record:alert`, a conditional-formatting `condition` — used to
17
+ * bind the row THREE ways: canonical `record.status`, bare shorthand `status`,
18
+ * and legacy `data.status` (objectui#4075 / PR #4079 bound all four action
19
+ * renderers all three ways to restore consistency, deliberately without
20
+ * deciding which of them was CONTRACT). The ruling decided it, in two phases:
21
+ *
22
+ * > **The canon is `record.*`.** Phase 1 (`d1ab06f0f`) declared it and warned
23
+ * > once, in dev, on the two other spellings. Phase 2 (`83fe6e741`, ruled
24
+ * > 2026-09-02 and amended 2026-09-05) retired them: the bare shorthand and
25
+ * > `data.*` are no longer bound on runtime record surfaces, and the Phase-1
26
+ * > warning went with them. No stored-metadata survey was run (「不考虑存量」);
27
+ * > the Phase-1 warning period was the notice.
28
+ *
29
+ * ## What a retired spelling does now: it FAULTS, as it always did on the server
30
+ *
31
+ * The canon states the SERVER's accept set. Measured against
32
+ * `@objectstack/formula@17.1.0` — the engine the server evaluates with, and the
33
+ * one `fieldRules.ts` delegates to:
40
34
  *
41
35
  * | spelling | server runtime (`buildScope` + `celEngine`) | server authoring oracle |
42
36
  * |---|---|---|
43
37
  * | `record.status` | ✅ `{ ok: true, value: true }` | ✅ accepted |
44
38
  * | bare `status` | ❌ `Unknown variable: status` | ❌ refused (`'status'`) |
45
- * | `data.status` | ❌ `Unknown variable: data` | ⚠️ **silently accepted** |
46
- *
47
- * `buildScope({ record })` mounts exactly `['record']` — `data` is never bound
48
- * and the row's fields are never flattened to top level. So **the server
49
- * accepts `record.*` and nothing else**; both other spellings fault there. The
50
- * three-way binding is a client tolerance with no server counterpart, which is
51
- * precisely why it is the client's job to warn.
52
- *
53
- * ⚠️ The `data.*` row is the dangerous one and the reason this warning exists
54
- * at all. `data` IS in `@objectstack/formula`'s `SCOPE_ROOTS`, so the server's
55
- * bare-identifier oracle waves `data.status` through — that list is a
56
- * deliberately generous "never faults" LINT BASELINE, not the runtime accept
57
- * set. A `data.*` row predicate therefore passes every authoring gate the
58
- * platform has and then binds nothing at runtime: it is not an error, it is a
59
- * constant `false`, and a `visible` that is constantly false is a button that
60
- * silently never appears. That is the #4075 fail-closed family's exact
61
- * signature, and this client is the only layer positioned to catch it.
62
- *
63
- * ## `data.*` is DEPRECATED HERE, not everywhere — the canon is layer-scoped
39
+ * | `data.status` | ❌ `Unknown variable: data` | ⚠️ silently accepted |
40
+ *
41
+ * `buildScope({ record })` mounts exactly `['record']`. Since Phase 2 the client
42
+ * binds the same set (`listConditional.ts`' scope bag and `@object-ui/react`'s
43
+ * `usePredicateRecordContext`), so both retired spellings fault on the client
44
+ * with the server's verdict, and each surface applies its EXISTING fault policy:
45
+ * fail-closed `visible` on the throwing `useCondition` legs, the caller's
46
+ * `fallback` on `evalRowPredicate` / `partitionRowsByPredicate`, fail-soft on
47
+ * the non-throwing `useCondition` legs. There is no runtime detector, no
48
+ * "treat as absent" special case and no uniform override — a retired spelling
49
+ * is not a recognised-and-rejected thing, it is an unknown variable like any
50
+ * other, and the existing fault warnings are what name it.
51
+ *
52
+ * One consequence, stated so it is not read back as a bug: a host scope may
53
+ * legitimately carry its OWN `data` (a rowless dialog's, or app-shell's ambient
54
+ * `data: {}`), and it is left standing. `data.*` on a record surface then reads
55
+ * the host's object rather than the row — which is exactly what "no longer bound
56
+ * to the row" means, and, against an ambient `data: {}`, the constant-false
57
+ * signature the Phase-1 warning text already described for the server.
58
+ *
59
+ * ## `data.*` is retired HERE, not everywhere — the canon is layer-scoped
64
60
  *
65
61
  * `data` is the CANONICAL root one layer over, in a metadata-editing form (the
66
62
  * row under edit): objectstack's `CANONICAL_ROOT_BY_LAYER` reads
67
63
  * `{ runtime: 'record', metadata: 'data' }` (ADR-0089 D3), and objectui's own
68
- * `app-shell` metadata-admin `SchemaForm` binds `{ data: row }` on purpose. So
69
- * `data.*` is not a legacy alias to be deprecated platform-wide — it is a
70
- * WRONG-LAYER paste on a runtime record surface, and only that is what this
71
- * module reports. Stating the deprecation unqualified would contradict
72
- * ADR-0089 D3 and break the metadata-editing layer's own contract.
64
+ * `app-shell` metadata-admin `SchemaForm` binds `{ data: row }` on purpose,
65
+ * through its own evaluator (`views/metadata-admin/predicate.ts`) — never
66
+ * through `evalRowPredicate` or `usePredicateRecordContext`. So `data.*` is not
67
+ * retired platform-wide: it is a WRONG-LAYER spelling on a runtime record
68
+ * surface, and that is the only thing the detector below reports.
69
+ *
70
+ * ## What is left in this module, and why
71
+ *
72
+ * {@link detectNonCanonicalRowSpelling} is the OFFLINE instrument: it classifies
73
+ * an authored predicate's spelling against a row without evaluating it, so a
74
+ * sweep over stored or in-repo metadata (the `702c48a62` corpus sweep's
75
+ * recipe) can find the documents that still need rewriting. It is
76
+ * exported for that purpose and nothing on the hot path calls it — the runtime
77
+ * warning half Phase 1 built on it (`warnNonCanonicalRowSpelling`,
78
+ * `resetRowPredicateCanonWarnings`) was removed with the bindings.
73
79
  *
74
80
  * ## Why the detection reuses the server's oracle instead of a regex
75
81
  *
@@ -94,8 +100,8 @@ const METADATA_LAYER_ROOT = 'data';
94
100
  * predicate is already canonical (or is not this module's verdict to give).
95
101
  *
96
102
  * Deliberately conservative in both arms — every condition below can only
97
- * REMOVE a finding, never invent one, because a false deprecation warning sends
98
- * an author to rewrite a predicate that was correct:
103
+ * REMOVE a finding, never invent one, because a false finding sends an author
104
+ * to rewrite a predicate that was correct:
99
105
  *
100
106
  * - **Unparseable source** → `null`. Syntax is another gate's verdict.
101
107
  * - **Bare shorthand** is reported only when the undeclared identifier is an own
@@ -113,10 +119,11 @@ const METADATA_LAYER_ROOT = 'data';
113
119
  * predicate manages both it is the one the author must fix to have anything
114
120
  * evaluate at all.
115
121
  *
116
- * @param source The predicate's CEL text. Callers must have already
117
- * routed legacy `${…}`-dialect strings elsewhere — in that
118
- * dialect `data.*` is the NORMAL spelling, and reporting it
119
- * here would be a false positive on every legacy predicate.
122
+ * @param source The predicate's CEL text. A legacy `${…}`-dialect string
123
+ * is not CEL and returns `null` here (unparseable); classify
124
+ * it by its own dialect's rules — on a runtime record surface
125
+ * it retired with the CEL spellings (`83fe6e741`), while on
126
+ * the schema/widget tier `data` is a different scope entirely.
120
127
  * @param row The row the predicate is bound against.
121
128
  * @param dataNamesRow Whether `data` is bound to that same row on this surface.
122
129
  */
@@ -149,60 +156,3 @@ export function detectNonCanonicalRowSpelling(source, row, dataNamesRow) {
149
156
  }
150
157
  return null;
151
158
  }
152
- /**
153
- * Dev-mode gate, matching `utils/dashboard-filters.ts` and `actions/actionKeys.ts`
154
- * — a deprecation warning that floods a production console is a warning that
155
- * gets muted.
156
- */
157
- const isDev = () => globalThis.process?.env
158
- ?.NODE_ENV !== 'production';
159
- /**
160
- * Warn-once memo, keyed by the `(label, predicate source)` pair — the same
161
- * identity `warnEvalError` uses, and JSON-encoded for the same reason: the
162
- * separator that boundary once used was a raw U+0000, which made the file
163
- * carrying it binary to grep (objectstack#5450). Keying on the source alone
164
- * would report the first surface carrying a shorthand `status` predicate and
165
- * stay silent about every other one; the label is what sends the author to the
166
- * right screen.
167
- *
168
- * Module scope, not per-call: these predicates are re-evaluated on every row of
169
- * every render, so per-call state would warn once per frame — the flood the
170
- * dedupe exists to prevent.
171
- */
172
- const warnedSpellings = new Set();
173
- /** Reset the row-predicate spelling warn-once memo. Exported for tests. */
174
- export function resetRowPredicateCanonWarnings() {
175
- warnedSpellings.clear();
176
- }
177
- /**
178
- * Report a non-canonical row-predicate spelling once, in dev.
179
- *
180
- * Phase 1 of the objectui#5330 window: the binding is UNCHANGED and every
181
- * spelling still resolves — this only says so out loud, so the stored
182
- * population stops growing and a later survey has something to count. It is a
183
- * warning and deliberately not a refusal: turning it into one would move the
184
- * accept/reject set, which this card is explicitly not entitled to do.
185
- */
186
- export function warnNonCanonicalRowSpelling(source, row, dataNamesRow, label) {
187
- if (!isDev())
188
- return;
189
- const finding = detectNonCanonicalRowSpelling(source, row, dataNamesRow);
190
- if (!finding)
191
- return;
192
- const key = JSON.stringify([label ?? '', source, finding.kind]);
193
- if (warnedSpellings.has(key))
194
- return;
195
- warnedSpellings.add(key);
196
- const where = label ? ` (${label})` : '';
197
- const detail = finding.kind === 'bare-shorthand'
198
- ? `it references the bare field ${JSON.stringify(finding.identifier)}; ` +
199
- `the server refuses that spelling outright ("Unknown variable: ${finding.identifier}")`
200
- : 'it is rooted at `data.`, which is the metadata-editing-form root — on a ' +
201
- 'record surface the server binds no `data` at all, so the predicate is a ' +
202
- 'constant false there rather than an error';
203
- console.warn(`[object-ui] A row predicate${where} uses a DEPRECATED spelling: ` +
204
- `${JSON.stringify(source)} — ${detail}. The canon is \`record.*\` ` +
205
- `(objectui#5330, ruled 2026-08-20): write \`${finding.canonical}\`. ` +
206
- 'This still evaluates here for now; the tolerance retires after a ' +
207
- 'stored-metadata survey.');
208
- }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * The acting subject's effective object permissions, carried to the one place
10
+ * this repo hands a predicate to the CEL engine (objectui#4421).
11
+ *
12
+ * ## What it is for
13
+ *
14
+ * `@objectstack/formula` registers `current_user.can(object, verb)` as a
15
+ * receiver method on the acting subject, and answers it from
16
+ * `EvalContext.permissions` — the verbatim `objects` map of the
17
+ * `/auth/me/permissions` response. That map is a separate field of the engine
18
+ * context, and the engine is explicit that it is ⛔ NOT a CEL variable: an
19
+ * authored predicate must not be able to read `permissions.crm_lead.allowEdit`
20
+ * and step around the closed verb table.
21
+ *
22
+ * This repo's evaluation seam (`evalFieldPredicate`) is not handed an engine
23
+ * context by its callers, though. Every action surface — the row menu, the
24
+ * record header, the `action:*` renderers, `DeclaredActionsBar` — hands it a
25
+ * variable BAG (the host's predicate scope, spread, merged, pushed through an
26
+ * `ExpressionContext`), and the bag's string keys all become CEL roots. So the
27
+ * permissions cannot ride on the bag under a name: any name would be a root.
28
+ *
29
+ * They ride on the SUBJECT instead, under a symbol:
30
+ *
31
+ * - The engine models the map as the subject's own answer sheet — it binds
32
+ * `permissions` only alongside a user, and `can` refuses any receiver that
33
+ * is not that user (`receiver !== subject`). The subject is the one object
34
+ * every bag already carries by reference under `current_user` (and the
35
+ * `user` / `ctx.user` / `os.user` aliases of it), through every spread and
36
+ * every `ExpressionContext` scope, so no surface needs its own copy of the
37
+ * hand-off.
38
+ * - A symbol is the one kind of key no predicate dialect can name: CEL
39
+ * identifiers and member names are strings, and so are the legacy
40
+ * evaluator's. `current_user.permissions` still faults as an unknown key.
41
+ *
42
+ * ## Absent is not empty
43
+ *
44
+ * A subject that carries NO map — the payload has not loaded, no permission
45
+ * provider is mounted, or the provider has no `/auth/me/permissions` answer at
46
+ * all — hands the engine no `permissions`, and `can` then refuses loudly
47
+ * (`ok: false`, `kind: 'runtime'`). That refusal is a FAULT, so each surface
48
+ * applies its own fault policy to it; it is never turned into a quiet `false`
49
+ * here, because the engine's contract is that an empty map means "holds
50
+ * nothing" and a caller that has no data must not claim it has that answer.
51
+ */
52
+ import type { EvalPermissions } from '@objectstack/formula';
53
+ /**
54
+ * The key the subject carries its effective object permissions under.
55
+ *
56
+ * `Symbol.for` rather than `Symbol()` so two copies of this module in one page
57
+ * (a host that bundles `@object-ui/core` twice) still agree on the key; the
58
+ * namespace keeps it from colliding with anyone else's registry entry.
59
+ */
60
+ export declare const SUBJECT_PERMISSIONS: unique symbol;
61
+ /**
62
+ * The subject to publish as `current_user`, carrying `permissions` when there
63
+ * are any.
64
+ *
65
+ * Returns a shallow copy so the caller's own user object is never mutated; the
66
+ * copy is what the whole predicate scope must use for every alias of the
67
+ * subject, because the engine compares the `can` receiver by IDENTITY. With no
68
+ * `permissions` the result carries none — a subject built from one that did is
69
+ * stripped rather than left answering from a stale map.
70
+ */
71
+ export declare function bindSubjectPermissions<T extends object>(subject: T, permissions: EvalPermissions | undefined): T;
72
+ /**
73
+ * The effective object permissions a subject carries, or `undefined` when it
74
+ * carries none (see "Absent is not empty" above).
75
+ */
76
+ export declare function subjectPermissionsOf(subject: unknown): EvalPermissions | undefined;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * The key the subject carries its effective object permissions under.
10
+ *
11
+ * `Symbol.for` rather than `Symbol()` so two copies of this module in one page
12
+ * (a host that bundles `@object-ui/core` twice) still agree on the key; the
13
+ * namespace keeps it from colliding with anyone else's registry entry.
14
+ */
15
+ export const SUBJECT_PERMISSIONS = Symbol.for('@object-ui/core:subject-permissions');
16
+ /**
17
+ * The subject to publish as `current_user`, carrying `permissions` when there
18
+ * are any.
19
+ *
20
+ * Returns a shallow copy so the caller's own user object is never mutated; the
21
+ * copy is what the whole predicate scope must use for every alias of the
22
+ * subject, because the engine compares the `can` receiver by IDENTITY. With no
23
+ * `permissions` the result carries none — a subject built from one that did is
24
+ * stripped rather than left answering from a stale map.
25
+ */
26
+ export function bindSubjectPermissions(subject, permissions) {
27
+ if (permissions === undefined) {
28
+ if (!Object.prototype.hasOwnProperty.call(subject, SUBJECT_PERMISSIONS))
29
+ return subject;
30
+ const stripped = { ...subject };
31
+ delete stripped[SUBJECT_PERMISSIONS];
32
+ return stripped;
33
+ }
34
+ return { ...subject, [SUBJECT_PERMISSIONS]: permissions };
35
+ }
36
+ /**
37
+ * The effective object permissions a subject carries, or `undefined` when it
38
+ * carries none (see "Absent is not empty" above).
39
+ */
40
+ export function subjectPermissionsOf(subject) {
41
+ if (subject === null || typeof subject !== 'object')
42
+ return undefined;
43
+ return subject[SUBJECT_PERMISSIONS];
44
+ }