@object-ui/react 17.6.0 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +4263 -0
  2. package/README.md +383 -42
  3. package/dist/SchemaRenderer.d.ts +64 -7
  4. package/dist/SchemaRenderer.js +1326 -106
  5. package/dist/context/ActionContext.js +40 -1
  6. package/dist/context/AppShellContext.d.ts +27 -0
  7. package/dist/context/DiscussionContext.d.ts +27 -0
  8. package/dist/context/DiscussionContext.js +4 -0
  9. package/dist/context/DrillNavigationContext.d.ts +13 -0
  10. package/dist/context/InlineEditContext.d.ts +151 -1
  11. package/dist/context/InlineEditContext.js +36 -2
  12. package/dist/context/PrintPageContext.d.ts +60 -0
  13. package/dist/context/PrintPageContext.js +32 -0
  14. package/dist/context/RecordContext.d.ts +48 -5
  15. package/dist/context/RecordContext.js +10 -1
  16. package/dist/context/RelatedRecordActionsContext.d.ts +72 -2
  17. package/dist/context/RelatedRecordActionsContext.js +4 -0
  18. package/dist/context/SchemaRendererContext.d.ts +61 -6
  19. package/dist/context/SchemaRendererContext.js +39 -6
  20. package/dist/context/ThemeContext.js +1 -1
  21. package/dist/context/actionRunnerTranslator.d.ts +27 -0
  22. package/dist/context/actionRunnerTranslator.js +37 -0
  23. package/dist/context/index.d.ts +1 -0
  24. package/dist/context/index.js +1 -0
  25. package/dist/element-data-source/ElementApiDataSourcePanel.d.ts +28 -0
  26. package/dist/element-data-source/ElementApiDataSourcePanel.js +10 -0
  27. package/dist/element-data-source/ElementDataSourceGate.d.ts +118 -3
  28. package/dist/element-data-source/ElementDataSourceGate.js +401 -19
  29. package/dist/element-data-source/useElementApiDataSource.d.ts +119 -0
  30. package/dist/element-data-source/useElementApiDataSource.js +198 -0
  31. package/dist/hooks/index.d.ts +3 -1
  32. package/dist/hooks/index.js +10 -1
  33. package/dist/hooks/useActionEngine.js +46 -3
  34. package/dist/hooks/useActionRunner.js +4 -0
  35. package/dist/hooks/useActionTextLocalizer.d.ts +35 -9
  36. package/dist/hooks/useActionTextLocalizer.js +70 -14
  37. package/dist/hooks/useConfigBagEvaluator.d.ts +31 -0
  38. package/dist/hooks/useConfigBagEvaluator.js +40 -0
  39. package/dist/hooks/useDatasetDimensionLabels.js +25 -10
  40. package/dist/hooks/useDensityMode.d.ts +8 -2
  41. package/dist/hooks/useDensityMode.js +8 -2
  42. package/dist/hooks/useDiscovery.d.ts +1 -10
  43. package/dist/hooks/useETagCache.js +25 -4
  44. package/dist/hooks/useElementDataSource.d.ts +15 -2
  45. package/dist/hooks/useElementDataSource.js +24 -7
  46. package/dist/hooks/useExpression.d.ts +52 -59
  47. package/dist/hooks/useExpression.js +58 -90
  48. package/dist/hooks/useFilterScope.d.ts +22 -2
  49. package/dist/hooks/useFilterScope.js +29 -4
  50. package/dist/hooks/useGlobalUndo.d.ts +10 -0
  51. package/dist/hooks/useGlobalUndo.js +112 -46
  52. package/dist/hooks/useNavigationOverlay.d.ts +76 -11
  53. package/dist/hooks/useNavigationOverlay.js +119 -9
  54. package/dist/hooks/useOffline.js +65 -11
  55. package/dist/hooks/usePageAssignment.d.ts +33 -7
  56. package/dist/hooks/usePageAssignment.js +85 -21
  57. package/dist/hooks/useRecordSearch.d.ts +34 -1
  58. package/dist/hooks/useRecordSearch.js +42 -7
  59. package/dist/hooks/useResolvedFilter.d.ts +46 -0
  60. package/dist/hooks/useResolvedFilter.js +130 -0
  61. package/dist/hooks/useSchemaPersistence.d.ts +7 -0
  62. package/dist/hooks/useSchemaPersistence.js +144 -5
  63. package/dist/hooks/useSettledSchema.d.ts +123 -0
  64. package/dist/hooks/useSettledSchema.js +186 -0
  65. package/dist/hooks/useViewData.d.ts +10 -3
  66. package/dist/hooks/useViewData.js +5 -14
  67. package/dist/index.d.ts +9 -1
  68. package/dist/index.js +48 -1
  69. package/dist/schema-input.d.ts +38 -8
  70. package/dist/schema-input.js +39 -7
  71. package/dist/utils/configBag.d.ts +68 -0
  72. package/dist/utils/configBag.js +70 -0
  73. package/dist/utils/error-message.d.ts +1 -1
  74. package/dist/utils/error-message.js +1 -1
  75. package/dist/utils/inlineAria.d.ts +61 -0
  76. package/dist/utils/inlineAria.js +70 -0
  77. package/dist/utils/localeMap.d.ts +27 -0
  78. package/dist/utils/localeMap.js +106 -0
  79. package/dist/utils/nonGridRowCeiling.d.ts +44 -0
  80. package/dist/utils/nonGridRowCeiling.js +78 -0
  81. package/dist/utils/paramsBag.d.ts +28 -0
  82. package/dist/utils/paramsBag.js +146 -0
  83. package/dist/utils/propsBagDiagnostic.d.ts +196 -0
  84. package/dist/utils/propsBagDiagnostic.js +422 -0
  85. package/dist/utils/refusedDataPropDiagnostic.d.ts +44 -0
  86. package/dist/utils/refusedDataPropDiagnostic.js +94 -0
  87. package/dist/utils/unevaluatedExpression.d.ts +44 -1
  88. package/dist/utils/unevaluatedExpression.js +92 -11
  89. package/dist/utils/visibilityDiagnostic.d.ts +282 -21
  90. package/dist/utils/visibilityDiagnostic.js +321 -46
  91. package/package.json +10 -10
  92. package/dist/hooks/useClientNotifications.d.ts +0 -47
  93. package/dist/hooks/useClientNotifications.js +0 -162
  94. package/dist/spec-bridge/SpecBridge.d.ts +0 -28
  95. package/dist/spec-bridge/SpecBridge.js +0 -56
  96. package/dist/spec-bridge/bridges/form-view.d.ts +0 -158
  97. package/dist/spec-bridge/bridges/form-view.js +0 -170
  98. package/dist/spec-bridge/bridges/list-view.d.ts +0 -20
  99. package/dist/spec-bridge/bridges/list-view.js +0 -268
  100. package/dist/spec-bridge/index.d.ts +0 -11
  101. package/dist/spec-bridge/index.js +0 -10
  102. package/dist/spec-bridge/types.d.ts +0 -55
  103. package/dist/spec-bridge/types.js +0 -8
@@ -5,6 +5,9 @@
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 { isConfigBag } from './configBag.js';
9
+ import { isInlineLocaleMap, mapLocaleMapMembers } from './localeMap.js';
10
+ import { PARAMS_KEY, isParamsBag, mapParamsLeaves } from './paramsBag.js';
8
11
  /**
9
12
  * Dev-build diagnostic: an UNEVALUATED template expression reached the DOM.
10
13
  *
@@ -55,6 +58,21 @@
55
58
  * diagnostic would be louder than the contract it speaks for. Deepening
56
59
  * evaluation is a separate decision, and this scan follows it the day it is
57
60
  * taken, not before.
61
+ *
62
+ * That decision has been taken for exactly ONE key: `params` (objectui#7867,
63
+ * ruling A). Every string leaf of a `params` bag is evaluated, at any depth,
64
+ * on every channel — so this scan walks the same leaves, through the same
65
+ * walker (`paramsBag.ts`, which the evaluation memo also uses), and a template
66
+ * that still cannot resolve is reported by its path (`params.target.id`,
67
+ * `properties.params.ids[0]`). Radius equal to evaluation by construction,
68
+ * not by two copies agreeing; every other key stays shallow on both sides.
69
+ *
70
+ * And for exactly ONE value shape, one level deep: an inline locale map
71
+ * (objectui#12936). Each member is evaluated like a plain string on the same
72
+ * key, so each member is scanned like one, through the same recogniser and
73
+ * walker (`localeMap.ts`), and a member whose expression threw is reported by
74
+ * its locale (`properties.title.zh-CN`). Any other object stays unscanned, as
75
+ * it stays unevaluated.
58
76
  */
59
77
  /** What counts as an expression — the evaluator's own definition. */
60
78
  const EXPRESSION_PATTERN = /\$\{([^}]+)\}/g;
@@ -78,18 +96,42 @@ export function findExpressionSources(value) {
78
96
  }
79
97
  return found;
80
98
  }
99
+ function pushIfUnevaluated(key, channel, value, into) {
100
+ const expressions = findExpressionSources(value);
101
+ if (expressions.length > 0) {
102
+ into.push({ key, channel, value, expressions });
103
+ }
104
+ }
81
105
  function scanBag(bag, channel, into, skip) {
82
- if (!bag || typeof bag !== 'object' || Array.isArray(bag))
106
+ if (!isConfigBag(bag))
83
107
  return;
84
108
  for (const [key, value] of Object.entries(bag)) {
85
- if (typeof value !== 'string')
86
- continue;
87
109
  if (skip?.(key, value))
88
110
  continue;
89
- const expressions = findExpressionSources(value);
90
- if (expressions.length > 0) {
91
- into.push({ key, channel, value, expressions });
111
+ // objectui#7867 — `params` is evaluated leaf-deep, so it is scanned
112
+ // leaf-deep, by the evaluator's own walker; the finding's `key` is the
113
+ // leaf's path (`params.target.id`). The visitor hands every leaf back
114
+ // unchanged, so the walk allocates nothing.
115
+ if (key === PARAMS_KEY && isParamsBag(value)) {
116
+ mapParamsLeaves(value, (leaf, path) => {
117
+ pushIfUnevaluated(path, channel, leaf, into);
118
+ return leaf;
119
+ });
120
+ continue;
121
+ }
122
+ // objectui#12936 — an inline locale map's members are evaluated like plain
123
+ // strings, so they are scanned like them, through the evaluator's own
124
+ // recogniser and walker. The visitor returns each member unchanged.
125
+ if (isInlineLocaleMap(value)) {
126
+ mapLocaleMapMembers(value, (member, locale) => {
127
+ pushIfUnevaluated(key + '.' + locale, channel, member, into);
128
+ return member;
129
+ });
130
+ continue;
92
131
  }
132
+ if (typeof value !== 'string')
133
+ continue;
134
+ pushIfUnevaluated(key, channel, value, into);
93
135
  }
94
136
  }
95
137
  /**
@@ -114,9 +156,7 @@ export function collectUnevaluatedExpressions(spread, properties, props) {
114
156
  scanBag(properties, 'properties', findings);
115
157
  // The hoist copies every `properties.*` value onto the node's top level, so
116
158
  // the same residue is visible twice. Report the authored spelling only.
117
- const hoisted = properties && typeof properties === 'object' && !Array.isArray(properties)
118
- ? properties
119
- : undefined;
159
+ const hoisted = isConfigBag(properties) ? properties : undefined;
120
160
  scanBag(spread, 'schema', findings, (key, value) => hoisted?.[key] === value);
121
161
  scanBag(props, 'props', findings);
122
162
  return findings;
@@ -140,6 +180,44 @@ function locate(finding) {
140
180
  /**
141
181
  * Build the message. Separate from the emit so a test can assert the words a
142
182
  * developer is going to read, not merely that something was logged.
183
+ *
184
+ * ## Why `properties.*` is in the channel list (objectui#7849)
185
+ *
186
+ * It was missing, and the omission CONTRADICTED the sibling diagnostic in
187
+ * `propsBagDiagnostic.ts`, which tells an author who wrote the same key one
188
+ * envelope out: *"`props` is NOT hoisted onto the node — only `properties.*`
189
+ * is … Write them under `properties` instead."* An author whose node trips
190
+ * both messages was told to use `properties` by one and, by the other, that
191
+ * the only channels that work are `content` or host-side resolution.
192
+ *
193
+ * The enumeration was the wrong half. `properties` is not a channel on its way
194
+ * out and there is no retirement on record for it: `@objectstack/spec@17.2.0`
195
+ * calls the vocabulary carried there *"ALIVE — this is not dead surface to
196
+ * retire under ADR-0049"*, keeps `PageComponentSchema.properties` as the open
197
+ * carrier ON PURPOSE, gates it at the authoring door
198
+ * (`validate-component-props.ts`), and tombstones no part of it via
199
+ * `retiredKey()`. In THIS repo `props` — not `properties` — is the spelling
200
+ * annotated as the legacy alias (`SchemaRenderer.tsx`: "the legacy `props`
201
+ * bag, minus every key the canonical `properties` bag also …").
202
+ *
203
+ * Measured on this branch's base through the real renderers, not inferred:
204
+ * `badge` with `properties.label = '${data.status}'` renders `completed` and
205
+ * applies the variant classes; `card` with a conditional
206
+ * `properties.className` renders `class="… border-red-500"`; the same keys
207
+ * under `props` render an EMPTY badge body and no `border-red-500`.
208
+ *
209
+ * The words are borrowed from the sibling message on purpose — one hoist, one
210
+ * vocabulary, so the two diagnostics cannot drift apart again. Pinned by
211
+ * `__tests__/diagnosticChannelConsistency.test.ts`.
212
+ *
213
+ * ## Why `params` is in it too (objectui#7867)
214
+ *
215
+ * The same defect #7849 closed, one channel later: once every string leaf of a
216
+ * `params` bag evaluates, an enumeration that left `params` out would tell an
217
+ * author whose `params.recordId` template THREW that `params` is not a channel
218
+ * at all — and send them to move a correct key somewhere else. A finding on a
219
+ * `params` leaf means the second sentence above (the expression threw, or the
220
+ * value it produced still reads as a template), never the first.
143
221
  */
144
222
  export function formatUnevaluatedExpressionMessage(type, id, findings) {
145
223
  const node = typeof type === 'string' && type ? '"' + type + '"' : '(untyped node)';
@@ -155,8 +233,11 @@ export function formatUnevaluatedExpressionMessage(type, id, findings) {
155
233
  'to the DOM, with its source text intact. Either SchemaRenderer does not\n' +
156
234
  'evaluate this key, or the expression threw and the evaluator returned the\n' +
157
235
  'source unchanged.\n' +
158
- 'Channels that do evaluate and read back today: `content`, or resolve the\n' +
159
- 'value in the host before handing the schema to SchemaRenderer.');
236
+ 'Channels that do evaluate and read back today: `content`; `properties.*`,\n' +
237
+ 'which is evaluated and then HOISTED onto the node, so a renderer declared\n' +
238
+ 'as `({ schema })` reads it back as `schema.<key>`; every string leaf of a\n' +
239
+ '`params` bag, at any depth, node-level or under `properties`; or resolve\n' +
240
+ 'the value in the host before handing the schema to SchemaRenderer.');
160
241
  }
161
242
  /**
162
243
  * Reported nodes, so a re-render (or the post-mount forceUpdate that picks up
@@ -6,14 +6,16 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  /**
9
- * Dev-build diagnostic: a visibility predicate could not be evaluated
10
- * (objectui#5454, leg 3 of the 2026-08-21 ruling).
9
+ * Diagnostic: a node-gate predicate could not be evaluated (objectui#5454,
10
+ * leg 3 of the 2026-08-21 ruling; production coverage added by objectui#6038,
11
+ * maintainer ruling 2026-08-25 option B; the `disabled` / `disabledOn` gate
12
+ * added by objectui#6445).
11
13
  *
12
14
  * ## The defect this names
13
15
  *
14
16
  * `SchemaRenderer`'s visibility chain is FAIL-SOFT: `evaluateCondition` answers
15
- * an unevaluable predicate with `true`. On the four negated legs (`visible` /
16
- * `visibleWhen` / `visibleOn` / `visibility`) that `true` means SHOWN — so "this
17
+ * an unevaluable predicate with `true`. On the negated legs (`visible` /
18
+ * `visibleWhen` / `visibleOn`) that `true` means SHOWN — so "this
17
19
  * predicate is broken" and "this predicate said yes" reach the screen as the
18
20
  * same pixel. An author who mistyped a root, or who wrote a `record.*` gate
19
21
  * before objectui#5454 bound the row, saw their block render and had no signal
@@ -30,9 +32,11 @@
30
32
  * It changes NO verdict. `evaluateCondition` already returned `true` for every
31
33
  * unevaluable predicate on every one of its paths, and the caller reproduces
32
34
  * exactly that from its catch — including on the two NON-negated legs
33
- * (`hidden` / `hiddenOn`), where the same `true` means HIDE. Flipping fail-soft
34
- * to fail-closed is a shipped-behaviour change tracked separately upstream
35
- * (objectstack#5149, appeal 1, undecided); this is the diagnostic half only.
35
+ * (`hidden` / `hiddenOn`), where the same `true` means HIDE — and, since
36
+ * objectui#6445, on the `disabled` / `disabledOn` gate, where it means GREYED
37
+ * OUT. Flipping fail-soft to fail-closed is a shipped-behaviour change tracked
38
+ * separately upstream (objectstack#5149, appeal 1, undecided); this module is
39
+ * the diagnostic half only, on every gate wired to it.
36
40
  *
37
41
  * ## Why a separate module
38
42
  *
@@ -48,19 +52,208 @@
48
52
  * and so an app can filter it out of its console transport if it must.
49
53
  */
50
54
  export declare const UNRESOLVABLE_VISIBILITY_PREFIX = "[ObjectUI] A visibility predicate could not be evaluated";
55
+ /**
56
+ * The same opening line for the ENABLEMENT gate (`disabled` / `disabledOn`,
57
+ * objectui#6445). A sibling constant rather than a widened one: the six
58
+ * visibility legs keep the bytes they ship today (objectui#6487 pinned them),
59
+ * and calling a `disabled` predicate a "visibility predicate" would send an
60
+ * author to the wrong gate on the first line - the one line a console filter
61
+ * and a `grep` both read.
62
+ *
63
+ * Same posture as its sibling in every other respect: same reporter, same
64
+ * severity, same dedupe `Set`, same key shape, same test-only reset. Only the
65
+ * words that would be FALSE on this gate differ.
66
+ */
67
+ export declare const UNRESOLVABLE_ENABLEMENT_PREFIX = "[ObjectUI] An enablement predicate could not be evaluated";
68
+ /**
69
+ * Which SCOPE the faulting predicate was evaluated against (objectui#6487).
70
+ *
71
+ * ## Why the reporter needs to be told, rather than deducing it
72
+ *
73
+ * The closing paragraph of the message names the ROOTS an author can bind. That
74
+ * is the half of the report an author acts on: a fault is usually a mistyped or
75
+ * unbound root, and "which roots exist here" is the answer. It is therefore the
76
+ * one part of the copy that is NOT true on every surface — the callers below
77
+ * build genuinely different bags — and it was printed unconditionally in the
78
+ * node tier's spelling, so an app-shell author was sent to check `record` and
79
+ * `page.<var>`, which nothing at that tier binds.
80
+ *
81
+ * The alternative fix — generalising the copy to "whatever this surface binds"
82
+ * plus a docs link — was refused deliberately: the concrete root names are the
83
+ * whole value of the paragraph, and advice that names no roots is correct
84
+ * everywhere and useful nowhere.
85
+ *
86
+ * Deducing the tier from the `type` slot was refused too. `type` carries the
87
+ * SCHEMA NODE TYPE at the node tier and a surface label at the other two
88
+ * (`page:tabs`, `app-shell:visible`), and `page:tabs` is a real registry key —
89
+ * so a `page:tabs` NODE faulting its own `visibleWhen` inside `SchemaRenderer`
90
+ * is indistinguishable, by `type` alone, from one of its ITEM predicates. An
91
+ * explicit argument cannot collide that way.
92
+ *
93
+ * ## The tiers, each derived from the code that builds the bag
94
+ *
95
+ * `'page-component'` — the node gate. `SchemaRenderer.tsx` builds
96
+ * `new ExpressionEvaluator({ ...predicateScope, current_user, [record], data,
97
+ * page })`, so the roots the SPEC declares for the tier (`ui/page.zod.ts`:
98
+ * *"Binds `record`, `current_user`, `page.<var>`"*) are the roots it binds.
99
+ * `page:tabs` ITEM predicates (`containers.tsx`) are this tier as well: they
100
+ * build the same three roots (plus the row spread flat, and `data` aliased to
101
+ * the row rather than to the adapter — both undeclared breadth that the spec
102
+ * does not promise and this paragraph therefore does not advertise).
103
+ *
104
+ * `'app-shell'` — the chrome gate. `ExpressionProvider.tsx` builds
105
+ * `{ current_user, user, ctx: { user }, os: { user }, features }`.
106
+ * There is no `record` and no `page` in it at all, which is the defect this
107
+ * type exists to fix. The four identity spellings are the ADR-0068 aliases and
108
+ * all four resolve; `features` is the deployment-flag root that provider's own
109
+ * docblock documents for exactly this kind of predicate.
110
+ *
111
+ * ⛔ No `app`. It was in that bag and in the paragraph below until
112
+ * objectui#8155 (ruled 2026-09-07): neither ADR-0068 nor
113
+ * `@objectstack/formula`'s `SCOPE_ROOTS` declares such a root, so
114
+ * `buildExpressionScope` stopped binding it. Naming it here would be worse
115
+ * than stale — this paragraph is printed at the exact moment a saved `app.*`
116
+ * predicate faults, so it would answer "why did my predicate not resolve?"
117
+ * with the root that is the reason.
118
+ *
119
+ * ⛔ No `data` either, since objectui#8166 (ruled 2026-09-10). It WAS bound at
120
+ * the app-shell tier and was already withheld from the paragraph below, on the
121
+ * grounds that every `ExpressionProvider` mount passes `data={{}}` — but
122
+ * `AppContent`'s own field-list evaluator passed the record under edit, so on
123
+ * that one leg an authored `data.*` resolved silently against the host's record
124
+ * instead of faulting. `buildExpressionScope` stopped binding it; a `data.*`
125
+ * predicate at this tier now reaches this reporter with the engine's
126
+ * `Unknown variable: data`, the same verdict the server gives it.
127
+ */
128
+ export type PredicateScopeTier = 'page-component' | 'app-shell';
129
+ /**
130
+ * WHICH GATE the faulting predicate was authored on (objectui#6445).
131
+ *
132
+ * ## Why the reporter has to be told, when it already prints the key
133
+ *
134
+ * `evaluateCondition` answers an unevaluable predicate with `true` on every one
135
+ * of its paths, and every caller reproduces that answer. What that `true` DOES
136
+ * to the node is not a property of the predicate - it is a property of the gate
137
+ * it was authored on, and the consequence paragraph has to say which:
138
+ *
139
+ * `'visibility'` - the visibility chain. Its `true` means SHOWN on the
140
+ * negated legs, so the safe default does NOT bite: a predicate that could not be
141
+ * evaluated reads on screen exactly like one that said yes, which is why the
142
+ * console line is the only signal an author gets.
143
+ *
144
+ * `'enablement'` - `disabled` / `disabledOn`. The SAME `true` GREYS THE CONTROL
145
+ * OUT, so here the safe default is the one that BITES. The author is looking at
146
+ * a control they can see and cannot use; telling them the gate did not bite
147
+ * would be the opposite of what is on their screen, and would send them looking
148
+ * for a rendering bug rather than at their own predicate.
149
+ *
150
+ * ## Why a parameter, and not deduced from `key`
151
+ *
152
+ * Same reason {@link PredicateScopeTier} is a parameter (read its docblock): the
153
+ * caller knows, and a deduction is a table that goes quietly wrong the moment a
154
+ * spelling arrives that it has not heard of - a new alias, or a caller outside
155
+ * this repo - which would inherit a consequence sentence written about some
156
+ * other gate. That is the exact defect class this card and objectui#6487 both
157
+ * exist to remove, so it is not worth re-introducing to save an argument.
158
+ *
159
+ * `'concealment'` - `hidden` / `hiddenOn` (objectui#6503). These are visibility
160
+ * legs, but the only two whose verdict `SchemaRenderer` does NOT negate, so the
161
+ * SAME `true` that SHOWS a node on the four negated legs REMOVES it here. They
162
+ * resolved to `'visibility'` until objectui#6503 and were therefore handed the
163
+ * one sentence that is false about them: the gate did not merely bite, it bit
164
+ * hardest of the three, and the node is not on screen to be inspected at all.
165
+ * This is the third member objectui#6445's docblock predicted, in the shape it
166
+ * predicted - a table entry and a routing decision, no signature change.
167
+ *
168
+ * ## Why a third MEMBER, and not `'visibility'` plus a `negated` flag
169
+ *
170
+ * What varies is the consequence sentence, and it varies per GATE - not along
171
+ * an independent axis a caller could set to contradict the gate it passed
172
+ * alongside. Three flat members cannot be spelled inconsistently; a
173
+ * `(kind, negated)` pair can, and `('enablement', negated: true)` would name a
174
+ * combination no renderer in this repo produces. The table stays the one place
175
+ * that decides what a fail-soft default DID.
176
+ *
177
+ * ## What widening this union costs, stated because it is a published surface
178
+ *
179
+ * `PredicateGateKind` is re-exported from `packages/react/src/index.ts` - the
180
+ * package entry, and the chain stops there (no sibling package re-exports it,
181
+ * and no file outside `packages/react` names it). A consumer switching
182
+ * EXHAUSTIVELY over the union, or holding a `Record<PredicateGateKind, ...>`,
183
+ * gains a case to answer. That is a type-level change only: no runtime
184
+ * signature moved, and every value that was accepted before still is.
185
+ *
186
+ * `'withholding'` - the app-shell's navigation `visible` gate (objectui#12700).
187
+ * A visibility predicate on a navigation entry, but one whose FAULT is answered
188
+ * fail-CLOSED: the shell withholds an entry whose gate it cannot evaluate (most
189
+ * often a `current_user.can(...)` asked before the permissions payload is
190
+ * there) instead of drawing it for everyone. So the default BITES, and the
191
+ * sentence `'visibility'` prints - "reads on screen exactly like one that said
192
+ * yes" - would be the opposite of what the member sees. Same opening line as
193
+ * `'visibility'`, for the same reason `'concealment'` keeps it.
194
+ */
195
+ export type PredicateGateKind = 'visibility' | 'enablement' | 'concealment' | 'withholding';
51
196
  /**
52
197
  * Build the message. Separate from the emit so a test can assert the words,
53
198
  * not merely that something was logged.
199
+ *
200
+ * `tier` defaults to `'page-component'` and `gate` to `'visibility'` so the
201
+ * historical five-argument call keeps printing the exact bytes it printed
202
+ * before (objectui#6487, objectui#6445) — the defaults are a compatibility
203
+ * shim for callers outside this repo, not something this repo leans on: every
204
+ * in-repo call site passes both explicitly, which is what makes each one a
205
+ * stated decision rather than an inherited one.
54
206
  */
55
- export declare function formatUnresolvableVisibilityMessage(type: unknown, id: unknown, key: string, raw: unknown, reason: string): string;
207
+ export declare function formatUnresolvableVisibilityMessage(type: unknown, id: unknown, key: string, raw: unknown, reason: string, tier?: PredicateScopeTier, gate?: PredicateGateKind): string;
56
208
  /**
57
- * Dev-build only; the caller applies the gate, so this module is dead code in a
58
- * production build. `console.warn`, not `error`: the verdict is unchanged and
59
- * the page still renders, so this is a diagnostic about a predicate — not the
60
- * refusal `reportUnevaluatedExpressions` emits once raw source has reached the
61
- * DOM.
209
+ * Reports a NODE-GATE predicate that could not be evaluated — in DEVELOPMENT
210
+ * AND IN PRODUCTION since objectui#6038 (maintainer ruling 2026-08-25, option
211
+ * B: "the silence is no longer an accepted property").
212
+ *
213
+ * The visibility chain was the first gate wired to it and is still the default
214
+ * {@link PredicateGateKind}; `disabled` / `disabledOn` joined it in
215
+ * objectui#6445, which is why the name is now narrower than the function. It is
216
+ * kept anyway: this symbol is exported from the package entry and called from
217
+ * `@object-ui/app-shell` and `@object-ui/components`, so renaming it would be a
218
+ * cross-package edit that changes no behaviour, on a card scoped to one file's
219
+ * wiring. What has to stay true is the posture the name once described — ONE
220
+ * reporter, ONE dedupe `Set`, ONE severity, ONE reset — and a second reporter
221
+ * for the second gate is exactly what that rules out.
222
+ *
223
+ * `console.warn`, not `error`: the verdict is unchanged and the page still
224
+ * renders, so this is a diagnostic about a predicate — not the refusal
225
+ * `reportUnevaluatedExpressions` emits once raw source has reached the DOM.
226
+ *
227
+ * ## `err` takes a reason, not only an `Error`
228
+ *
229
+ * The dev caller catches a throw and passes the `Error`; the production caller
230
+ * is handed the evaluator's own reason STRING through `EvaluationOptions.onFault`
231
+ * (no throw is raised there, because raising one would cost a second
232
+ * evaluation). `String(err)` already covered that shape, so both callers reach
233
+ * the same `Reason:` text and the same dedupe entry — which is what makes "dev
234
+ * and production print the identical line" true rather than approximately true.
235
+ *
236
+ * ## `tier` is NOT in the dedupe key, and that is not an oversight
237
+ *
238
+ * The key stays `(type, key, predicate source)` — the rate limit objectui#6038
239
+ * pinned in both directions. Adding `tier` could only ever LOOSEN it, and it
240
+ * would take a `type` shared across two tiers to loosen anything: the app-shell
241
+ * site's `type` is the constant `'app-shell:visible'`, which no node tier can
242
+ * produce, and the two `'page-component'` callers are the same tier by
243
+ * definition. So the tier is free of the key by measurement, not by assumption.
244
+ *
245
+ * ## Neither is `gate`, and for a stronger reason (objectui#6445)
246
+ *
247
+ * `gate` is a FUNCTION of `key`, which is already in the dedupe key: the
248
+ * enablement gate is exactly `disabled` / `disabledOn` and the visibility gate
249
+ * is exactly the six legs of the visibility chain, two disjoint sets. So adding
250
+ * it could not separate two entries that `key` does not already separate, and
251
+ * the cross-gate direction is pinned rather than argued: the SAME predicate
252
+ * source authored on `disabled` and on `visibleWhen` produces TWO lines, not
253
+ * one - the new reporting site cannot be swallowed by a visibility entry, and
254
+ * cannot swallow one.
62
255
  */
63
- export declare function reportUnresolvableVisibilityPredicate(type: unknown, id: unknown, key: string, raw: unknown, err: unknown): void;
256
+ export declare function reportUnresolvableVisibilityPredicate(type: unknown, id: unknown, key: string, raw: unknown, err: unknown, tier?: PredicateScopeTier, gate?: PredicateGateKind): void;
64
257
  /**
65
258
  * Test-only reset for the dedupe above — for BOTH legs, which is the point of
66
259
  * their sharing one `Set`. The `Set` is module state: without this, the second
@@ -76,16 +269,66 @@ export declare function __resetVisibilityPredicateWarnings(): void;
76
269
  * dev-only unresolvable-predicate report". What is load-bearing there — and
77
270
  * what the dispatch restated — is the reporter's POSTURE: same module, same
78
271
  * severity (`console.warn`), same dedupe key shape, same dedupe Set, same
79
- * dev-only gate at the same call site, same test-only reset. All of that is
80
- * shared below. The first LINE is not reused, because on this path it would
272
+ * gate at the same call site, same test-only reset. All of that is shared
273
+ * below.
274
+ *
275
+ * ⚠️ The two legs stopped sharing that gate's VALUE in objectui#6038, and only
276
+ * that. The unresolvable leg now reports in production as well, because the
277
+ * 2026-08-25 ruling retired its silence; THIS leg stays dev-only under its own
278
+ * 2026-08-22 ruling, and the difference is not an oversight. A fault is a
279
+ * predicate that could not be evaluated, and shipping a live gate that has
280
+ * stopped biting is the class-1 defect production has to be able to see. This
281
+ * leg reports something else: a predicate that evaluated perfectly, against the
282
+ * wrong object. Its trigger is a LEXICAL scan of the predicate source with a
283
+ * stated false-positive residue (the deliberate-absence idioms below), which is
284
+ * a cost worth paying for an author at their keyboard and not for every user of
285
+ * every production page. The first LINE is not reused, because on this path it would
81
286
  * state something false: the predicate did not fail to evaluate. It evaluated
82
287
  * perfectly, against the wrong object, and produced a constant. Telling an
83
288
  * author "could not be evaluated" would send them hunting for a syntax error
84
289
  * that is not there.
85
290
  */
86
- export declare const ADAPTER_ONLY_DATA_PREDICATE_PREFIX = "[ObjectUI] A visibility predicate resolved `data.*` against the data-source adapter";
87
- /** Built separately from the emit, for the same reason as the message above. */
88
- export declare function formatAdapterOnlyDataMessage(type: unknown, id: unknown, key: string, raw: unknown, unresolved: string[]): string;
291
+ export declare const ADAPTER_ONLY_DATA_PREDICATE_PREFIX = "[ObjectUI] A visibility predicate read `data.*` that the bound `data` does not answer";
292
+ /**
293
+ * The `disabled` / `disabledOn` sibling of the prefix above (objectui#6504,
294
+ * maintainer ruling 2026-08-27 option A). Same reason
295
+ * {@link UNRESOLVABLE_ENABLEMENT_PREFIX} is a sibling of
296
+ * {@link UNRESOLVABLE_VISIBILITY_PREFIX} rather than a reuse: calling a
297
+ * `disabled` predicate "a visibility predicate" would send an author to the
298
+ * wrong gate on the first line — the one line a console filter and a `grep`
299
+ * both read.
300
+ */
301
+ export declare const ADAPTER_ONLY_ENABLEMENT_PREDICATE_PREFIX = "[ObjectUI] An enablement predicate read `data.*` that the bound `data` does not answer";
302
+ /**
303
+ * Which gates the objectui#5687 constant-predicate diagnostic is wired to
304
+ * (objectui#6504, maintainer ruling 2026-08-27 option A: extend to the
305
+ * enablement gate, dev-only; option C — always-on — was excluded, outside
306
+ * the #5687 precedent).
307
+ *
308
+ * A NARROWER union than {@link PredicateGateKind} deliberately, not an
309
+ * oversight: `'concealment'` (`hidden` / `hiddenOn`) is not one of them. Both
310
+ * of this diagnostic's call sites are unchanged by this card — the visibility
311
+ * one (`SchemaRenderer.tsx`'s `evaluateVisibilityPredicate`) still calls
312
+ * {@link reportAdapterOnlyDataPredicate} with no `gate` argument at all, for
313
+ * the six-leg chain exactly as objectui#5687 shipped it, so `'concealment'`
314
+ * was never a value this table had to answer for. Giving it an entry nobody
315
+ * can reach would be a decision this ruling did not make; widening this type
316
+ * later is a decision for whichever card makes that call, not a `??` for this
317
+ * table to paper over now.
318
+ */
319
+ export type AdapterOnlyPredicateGateKind = Extract<PredicateGateKind, 'visibility' | 'enablement'>;
320
+ /**
321
+ * Built separately from the emit, for the same reason as the message above.
322
+ *
323
+ * `gate` defaults to `'visibility'` so the historical four-argument call
324
+ * (objectui#5687) keeps printing the exact bytes it printed before —
325
+ * objectui#6504's own call site in `evaluateVisibilityPredicate`
326
+ * (`SchemaRenderer.tsx`) is one of those unchanged callers, passing no `gate`
327
+ * at all. `evaluateEnablementPredicate` is the one caller that states
328
+ * `'enablement'` explicitly, matching {@link formatUnresolvableVisibilityMessage}'s
329
+ * own compatibility-default pattern.
330
+ */
331
+ export declare function formatAdapterOnlyDataMessage(type: unknown, id: unknown, key: string, raw: unknown, unresolved: string[], gate?: AdapterOnlyPredicateGateKind): string;
89
332
  /**
90
333
  * Dev-build diagnostic for a node-gate predicate that reads `data.*` and gets
91
334
  * `undefined` back from the adapter (objectui#5687, maintainer ruling
@@ -111,9 +354,27 @@ export declare function formatAdapterOnlyDataMessage(type: unknown, id: unknown,
111
354
  * objectui#5454 existed to remove, one path further along.
112
355
  *
113
356
  * Sharing the module means sharing the LIFECYCLE, which is the part that has to
114
- * match: one dedupe Set, one reset, one severity, one dev-only gate. The dedupe
357
+ * match: one dedupe Set, one reset, one severity. (One gate, too, until
358
+ * objectui#6038 — see the prefix constant above for why only the sibling leg
359
+ * crossed into production.) The dedupe
115
360
  * key is tagged with this leg's name so the two diagnostics cannot silence each
116
361
  * other for the same (type, key, source) triple — they are different faults,
117
362
  * and a node that faults one way is not evidence about the other.
363
+ *
364
+ * ## `gate` (objectui#6504) — NOT in the dedupe key, for the reason
365
+ * {@link reportUnresolvableVisibilityPredicate}'s own docblock gives for the
366
+ * same omission
367
+ *
368
+ * `gate` is a FUNCTION of `key`: the enablement gate is exactly `disabled` /
369
+ * `disabledOn` and the visibility gate this leg is called for is the six-leg
370
+ * chain, two disjoint sets. Adding it to the dedupe key could not separate two
371
+ * entries `key` does not already separate — and the cross-gate direction is
372
+ * the one that would matter if it could: the SAME predicate source authored on
373
+ * `disabled` and on (say) `visibleWhen` must produce TWO lines, which it does
374
+ * today because `key` differs, with or without `gate` in the tuple.
375
+ *
376
+ * Defaults to `'visibility'` — the objectui#5687 call site
377
+ * (`evaluateVisibilityPredicate`) is unchanged by this card and still calls
378
+ * this function with five arguments.
118
379
  */
119
- export declare function reportAdapterOnlyDataPredicate(type: unknown, id: unknown, key: string, raw: unknown, boundData: unknown): void;
380
+ export declare function reportAdapterOnlyDataPredicate(type: unknown, id: unknown, key: string, raw: unknown, boundData: unknown, gate?: AdapterOnlyPredicateGateKind): void;