@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
@@ -0,0 +1,422 @@
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
+ import { isConfigBag } from './configBag.js';
9
+ /**
10
+ * Dev-build diagnostic: a `props` CONFIG BAG on a node whose renderer reads its
11
+ * config from `schema` — so the keys inside it are evaluated, spread as React
12
+ * props, and then never read (objectui#6708).
13
+ *
14
+ * ## The defect this names
15
+ *
16
+ * `SchemaRenderer` HOISTS every `properties.*` value onto the node (minus
17
+ * `HOIST_PROTECTED_KEYS`), so a key written under `properties` is a real value
18
+ * on `schema.<key>` by the time a renderer destructures it. `props` is NOT
19
+ * hoisted: it is spread as React props on the created element. A renderer
20
+ * declared as `({ schema })` — the normal shape for the component renderers —
21
+ * therefore never sees it.
22
+ *
23
+ * Measured through the real `SchemaRenderer` on `faac0d935`, one node per row,
24
+ * with a probe renderer that records both channels:
25
+ *
26
+ * { type: 'test:cap', props: { data: '${data.customers}' } }
27
+ * -> React prop `data` = the evaluated array, `schema.data` = undefined
28
+ * { type: 'test:cap', properties: { data: '${data.customers}' } }
29
+ * -> React prop `data` = the evaluated array, `schema.data` = the array
30
+ *
31
+ * Same key, same value, one envelope apart. The card's own four-leg reading of
32
+ * the same asymmetry through a real `data-table` is pinned in
33
+ * `packages/components/src/__tests__/data-table-node-data-diagnostic.test.tsx`
34
+ * (objectui#6665): the `props` leg renders `No results found`, the `properties`
35
+ * leg renders the rows.
36
+ *
37
+ * The tolerant gates accept the `props` spelling — the zod `BaseSchema` is
38
+ * `.passthrough()`, and the TS face admitted it through `[key: string]: any`
39
+ * until objectui#8347 (a typed literal refuses it now) — and `props` is documented as the annotated legacy
40
+ * alias of the config bag, so nothing between the author and the screen says a
41
+ * word. That is the success-receipt shape objectui#6575 and objectui#6665 exist
42
+ * to remove; this is the third instance, and the first at a tier that covers
43
+ * every renderer instead of one component.
44
+ *
45
+ * ## What it deliberately does NOT do
46
+ *
47
+ * It changes NO behaviour. Hoisting `props` to parity with `properties` was
48
+ * option 1 and the maintainer REFUSED it (ruling 2026-08-29, verbatim
49
+ * 「同意」 on option 2): hoisting would weld the legacy alias in as a permanent
50
+ * second spelling, the opposite of this repo's alias-retirement direction.
51
+ * Refusing the key at parse was option 3 and stays blocked on the
52
+ * `.passthrough()` ceiling (objectui#5155 / objectui#6269). So the trap stops
53
+ * being silent; it does not stop being a trap, and what every renderer receives
54
+ * is byte-for-byte what it received before — pinned directly, not asserted, in
55
+ * `SchemaRenderer.propsBagDiagnostic.test.tsx`.
56
+ *
57
+ * ## Why `console.warn`, when its neighbour in this directory uses `error`
58
+ *
59
+ * Two conventions cross here and the choice follows the SHAPE, not the tier.
60
+ * `unevaluatedExpression.ts` sits at this same tier and shouts with
61
+ * `console.error`, but its subject is different: a raw `${…}` placed VERBATIM
62
+ * in front of a user. Nothing is placed here — a value is dropped. That is
63
+ * exactly objectui#6575's and objectui#6665's subject ("you declared something
64
+ * and the renderer dropped it"), and both of those emit `console.warn`. The
65
+ * card names this the third instance of that shape, so it joins that family.
66
+ *
67
+ * ## Level and dedupe were chosen from a census, not from taste
68
+ *
69
+ * The ruling fixed the order: measure component-level `props` usage across the
70
+ * in-repo corpus FIRST, and tune level/dedupe so the diagnostic informs rather
71
+ * than floods. Measured on `faac0d935` by walking every JSON document, every
72
+ * `json` fence in every `.md`/`.mdx`, and every TypeScript object literal in
73
+ * the repo (TS compiler API) for nodes carrying both `type` and `props`:
74
+ * 39 such nodes, of which 22 are on component-renderer types — and 19 of those
75
+ * 22 are test fixtures exercising this exact shape on purpose. The authored,
76
+ * non-test corpus holds 5, none of them in application runtime metadata.
77
+ *
78
+ * So there is nothing to flood, and the level is not softened for volume. The
79
+ * dedupe is still keyed on the MESSAGE rather than on the schema object,
80
+ * because the failure the census makes plausible is a metadata GENERATOR that
81
+ * emits the same wrong envelope on many nodes: those are distinct schema
82
+ * objects, so an object-keyed `WeakSet` (the shape
83
+ * `reportUnevaluatedExpressions` uses next door) would print one line per node
84
+ * for one authoring bug. Keying on the rendered message collapses that to one
85
+ * line while still giving two genuinely different nodes two lines. Full census
86
+ * table: the objectui#6708 PR body.
87
+ */
88
+ /**
89
+ * The namespace whose renderers merge BOTH bags.
90
+ *
91
+ * Every `readProps()` in this repo that merges `{ ...schema.props,
92
+ * ...schema.properties }` belongs to a component registered with
93
+ * `namespace: 'element'` — measured by reading all five of them on
94
+ * `faac0d935`: `elements.tsx` (`element:text` / `divider` / `image` / `button`
95
+ * / `number`), `data-list.tsx` (`definition-list` / `repeater`),
96
+ * `text-input.tsx`, `record-picker.tsx` and `metadata-viewer.tsx`. For that
97
+ * family `props` is a legitimate spelling, so silence there is correct rather
98
+ * than a gap — and it is the direction the ruling pins explicitly.
99
+ */
100
+ const ELEMENT_NAMESPACE_PREFIX = 'element:';
101
+ /**
102
+ * Node types OUTSIDE the `element:` namespace that nevertheless read the raw
103
+ * `props` bag off the schema, so the diagnostic must not claim their keys were
104
+ * dropped.
105
+ *
106
+ * Derived, not guessed: a repo-wide grep for reads of `schema.props` /
107
+ * `schema?.props` on `faac0d935` returns the `element:` family above plus
108
+ * exactly these. `plugin-view`'s `SimpleViewRenderer` reads
109
+ * `schema.props?.columns` for its grid layout.
110
+ *
111
+ * ⚠️ This list is a MEASUREMENT of the current tree, and its cost is stated
112
+ * rather than hidden: `view:simple` reads exactly one key out of the bag, so a
113
+ * `props` key other than `columns` on a `view:simple` node IS dropped and is
114
+ * NOT diagnosed. Silence on a node where the envelope is partly legitimate was
115
+ * preferred to a message that asserts a drop it did not check — a diagnostic
116
+ * that overstates its own consequence teaches authors to distrust it
117
+ * (objectui#6665's `describeIgnoredBind` makes the same trade). The honest way
118
+ * to shrink this list is to stop reading the legacy bag in that renderer, which
119
+ * is a behaviour change on a published component and not this card's to make.
120
+ */
121
+ const NON_ELEMENT_PROPS_BAG_READERS = new Set(['view:simple']);
122
+ /**
123
+ * Does this node's renderer read the `props` bag as a config bag?
124
+ *
125
+ * Exported so the report and its pins cannot drift apart: one predicate, two
126
+ * readers. A non-string `type` answers `false` — an untyped node cannot be in
127
+ * a family — and the caller has already resolved a component for it, so the
128
+ * unknown-type box is not in play.
129
+ */
130
+ export function readsPropsBag(type) {
131
+ if (typeof type !== 'string' || type.length === 0)
132
+ return false;
133
+ if (type.startsWith(ELEMENT_NAMESPACE_PREFIX))
134
+ return true;
135
+ return NON_ELEMENT_PROPS_BAG_READERS.has(type);
136
+ }
137
+ /** Prefix every line starts with — the handle tests, greps and log filters hold. */
138
+ export const DROPPED_PROPS_BAG_PREFIX = '[ObjectUI] A `props` config bag was not read';
139
+ /** Where the offending node lives, for the first line of the message. */
140
+ function describeAddress(type, id) {
141
+ const node = typeof type === 'string' && type ? `\`${type}\`` : '(untyped node)';
142
+ const where = typeof id === 'string' && id ? ` (id: '${id}')` : '';
143
+ return `${node}${where}`;
144
+ }
145
+ /**
146
+ * Build the message. Separate from the emit so a test can assert the words a
147
+ * developer is going to read, not merely that something was logged.
148
+ */
149
+ export function formatDroppedPropsBagMessage(type, id, droppedKeys) {
150
+ const keyList = droppedKeys.map(k => `\`${k}\``).join(', ');
151
+ const one = droppedKeys.length === 1;
152
+ return (`${DROPPED_PROPS_BAG_PREFIX} - node ${describeAddress(type, id)}\n` +
153
+ ` ${one ? 'Key' : 'Keys'} under \`props\`: ${keyList}\n` +
154
+ '`props` is NOT hoisted onto the node — only `properties.*` is. It is spread as\n' +
155
+ `React props on the created element, so \`schema.${droppedKeys[0]}\` is undefined and a\n` +
156
+ `renderer declared as \`({ schema })\` never sees ${one ? 'this key' : 'these keys'}. Nothing throws and\n` +
157
+ 'nothing is logged by the renderer: the node renders as if the bag were empty.\n' +
158
+ ` Write ${one ? 'it' : 'them'} under \`properties\` instead (or at node level, where the node's\n` +
159
+ 'schema declares the key). The `element:*` renderers merge both bags via\n' +
160
+ '`readProps()`; every other renderer reads `schema`. (objectui#6708)');
161
+ }
162
+ /**
163
+ * Reported messages, so a re-render — or a second node carrying the same
164
+ * authoring bug — does not repeat the line. Module state, exactly like
165
+ * `visibilityDiagnostic.ts`'s `Set`, and reset the same way for tests.
166
+ */
167
+ const _warnedPropsBags = new Set();
168
+ /**
169
+ * Test-only reset for the dedupe above. Without it the second test to assert
170
+ * the same warning reads the first test's dedupe entry and sees silence — a
171
+ * green run that checked nothing.
172
+ */
173
+ export function __resetDroppedPropsBagWarnings() {
174
+ _warnedPropsBags.clear();
175
+ }
176
+ /**
177
+ * Which keys of the OUTGOING props bag are dropped by a `schema`-reading
178
+ * renderer, or `null` when there is nothing to say.
179
+ *
180
+ * Two bags are read, and needing both is a MEASUREMENT rather than caution.
181
+ *
182
+ * `outgoingPropsBag` is the bag `SchemaRenderer` actually spreads — the value
183
+ * of `propsWithoutCanonicalKeys(schema.props, schema.properties)`, passed in
184
+ * rather than re-derived. That is what makes the message honest about the two
185
+ * cases it must not confuse:
186
+ *
187
+ * - a key BOTH bags declare has already been subtracted by that function
188
+ * (objectui#5123: `properties` wins on both channels), so the author is
189
+ * getting the canonical answer and nothing was silently dropped;
190
+ * - a key only `props` declares survives into that bag, and it is exactly the
191
+ * key that reaches no `schema` reader.
192
+ *
193
+ * `authoredPropsBag` is what the AUTHOR wrote, read off the original schema
194
+ * before the evaluation memo touched it. It is needed because that memo
195
+ * rebuilds the bag with `{ ...newSchema.props }` under a bare truthiness guard,
196
+ * so a degenerate `props: 'not-a-bag'` arrives at the spread site as
197
+ * `{ '0': 'n', '1': 'o', … }` — measured, not supposed. Reading only the
198
+ * outgoing bag would therefore report nine dropped keys named `0` … `8` and
199
+ * tell the author that `schema.0` is undefined, which is true and useless. A
200
+ * string is not a config bag; there is nothing to point at, and the shape-level
201
+ * defect it represents is a different question from this one.
202
+ *
203
+ * Deliberately narrow, and each exclusion is a reading rather than an omission:
204
+ *
205
+ * - an EMPTY bag says nothing was authored and nothing was lost;
206
+ * - a non-object authored `props` is the degenerate case above;
207
+ * - a node in the {@link readsPropsBag} family is silent by the ruling.
208
+ */
209
+ export function collectDroppedPropsKeys(type, authoredPropsBag, outgoingPropsBag) {
210
+ if (readsPropsBag(type))
211
+ return null;
212
+ if (!isConfigBag(authoredPropsBag))
213
+ return null;
214
+ if (!isConfigBag(outgoingPropsBag))
215
+ return null;
216
+ const keys = Object.keys(outgoingPropsBag);
217
+ return keys.length > 0 ? keys : null;
218
+ }
219
+ /**
220
+ * Dev-build only. Reports once per distinct message via `console.warn`, and
221
+ * returns the message it emitted (or `null`) so a caller or a test can read the
222
+ * decision rather than infer it from a spy.
223
+ *
224
+ * The caller applies the production gate, so this stays a single dev-only
225
+ * branch at the call site and the whole module is dead code in a production
226
+ * build.
227
+ */
228
+ export function reportDroppedPropsBag(type, id, authoredPropsBag, outgoingPropsBag) {
229
+ const droppedKeys = collectDroppedPropsKeys(type, authoredPropsBag, outgoingPropsBag);
230
+ if (!droppedKeys)
231
+ return null;
232
+ const message = formatDroppedPropsBagMessage(type, id, droppedKeys);
233
+ if (_warnedPropsBags.has(message))
234
+ return null;
235
+ _warnedPropsBags.add(message);
236
+ console.warn(message);
237
+ return message;
238
+ }
239
+ /* -------------------------------------------------------------------------- *
240
+ * objectui#9108 - the NODE-GATE half of the same bag
241
+ * -------------------------------------------------------------------------- */
242
+ /**
243
+ * Refusal: a NODE-GATE PREDICATE was parked under `props` (objectui#9108,
244
+ * maintainer ruling 2026-09-13, verbatim 「同意」 on the `domain:spec` seat's
245
+ * recommendation - REFUSE).
246
+ *
247
+ * ## The defect this refuses
248
+ *
249
+ * Measured at node level on `1e0e46af9`, four rows, both spellings and both
250
+ * polarities:
251
+ *
252
+ * { type: 'card', props: { visible: false } } -> the node RENDERED
253
+ * { type: 'card', props: { hidden: true } } -> the node RENDERED
254
+ * { type: 'card', properties: { visible: false } } -> correctly hidden
255
+ * { type: 'card', properties: { hidden: true } } -> correctly hidden
256
+ *
257
+ * `SchemaRenderer` hoists `properties.*` onto the node; NOTHING copies
258
+ * `props.*`. Both node gates read the post-hoist node, so a predicate that
259
+ * arrived under the alias is never one of the keys either gate can see. It is
260
+ * fail-OPEN and silent by construction - a gate that never bit renders exactly
261
+ * like a gate that said yes - so no user, no screenshot and no snapshot can
262
+ * find it. Only counting can.
263
+ *
264
+ * ## Why REFUSE and not HONOUR - this is a ruling, not a preference
265
+ *
266
+ * The opposite arm was implemented and CLOSED (PR objectui#9144). The ruling's
267
+ * decisive axis: honouring the alias would have made
268
+ * *"8 predicate keys work while the rest stayed silently dropped - and partly
269
+ * working is harder to learn from than not working"*, because the author
270
+ * generalises "`props` is fine" and collides again on the next key with the
271
+ * cause now further away. The three measured readings behind it, each with a
272
+ * control that fires: `@objectstack/spec` already refuses `props` by name
273
+ * (`unrecognized_keys ["props"]`); `skills/objectui/rules/protocol.md` teaches
274
+ * it as an ERROR rather than as a gap; and the producer census found ZERO
275
+ * authored `props` bags across `examples/**` and `content/docs/**` (controls:
276
+ * `visibleWhen` 9 files, `type` 488 files), so there is no capability to cut.
277
+ *
278
+ * ## What it deliberately does NOT do
279
+ *
280
+ * It changes NO verdict and NO rendered byte. Every gate answers exactly what
281
+ * it answered before, nothing is hoisted, `schema.<KEY>` stays undefined for a
282
+ * renderer declared as `({ schema })`, and the bag reaches the element with the
283
+ * same keys it always did. The trap stops being SILENT; it does not stop being
284
+ * a trap, which is the same posture objectui#6708 took one card earlier and the
285
+ * only posture a diagnostic can take without becoming the honour arm the ruling
286
+ * refused.
287
+ *
288
+ * ## Why `console.error`, and why NOT dev-only
289
+ *
290
+ * Two established postures in this tree, and this leg needs a leg of each:
291
+ *
292
+ * - SEVERITY from `unevaluatedExpression.ts`, which is this repo's REFUSAL
293
+ * tier. Its neighbour {@link reportDroppedPropsBag} warns because it
294
+ * reports a value that was dropped; the ruling's word here is *refused*,
295
+ * and the sibling gate diagnostic states the split explicitly ("not the
296
+ * refusal `reportUnevaluatedExpressions` emits").
297
+ * - ALWAYS-ON from `visibilityDiagnostic.ts`'s unresolvable-predicate leg,
298
+ * which the maintainer took out of `__DEV__` in objectui#6038 (ruling
299
+ * 2026-08-25, option B: "the silence is no longer an accepted property")
300
+ * on the ground that a node gate which has stopped biting in production is
301
+ * a class-1 defect that must not be able to sit live and undiscovered.
302
+ * This is that class in its widest form - the gate never bit at all, on
303
+ * every render - and the ruling's own confidence gap is about exactly the
304
+ * population a `__DEV__` gate would silence: *"The producer census covers
305
+ * this repository's `examples/**`, `content/docs/**` and skills corpus.
306
+ * It does NOT measure authored metadata in production. If stored documents
307
+ * out there do carry `props`, those authors are already suffering the
308
+ * silent drop today."*
309
+ *
310
+ * The rate limit that makes an always-on console line affordable is the same
311
+ * one objectui#6038 required: the dedupe below is keyed on the MESSAGE, so the
312
+ * ceiling is one line per distinct authoring bug for the lifetime of the page,
313
+ * not one line per render and not one per node.
314
+ */
315
+ export const REFUSED_PROPS_PREDICATE_PREFIX = '[ObjectUI] A node-gate predicate under `props` is REFUSED';
316
+ /**
317
+ * Node-gate predicate keys the BAG-READING family can still honour itself, so
318
+ * refusing them there would cry wolf.
319
+ *
320
+ * Measured on this tree, not guessed: across every renderer that reads a config
321
+ * bag ({@link readsPropsBag} - the five `element:*` `readProps()` sites plus
322
+ * `view:simple`), the only node-gate predicate key any of them reads out of the
323
+ * bag is `disabled`, in `elements.tsx` (`element:button`:
324
+ * `disabled={props.disabled || running}`) and `text-input.tsx`
325
+ * (`element:text-input`). No visibility-chain key is read from a bag by any
326
+ * renderer in this repo, and neither is `disabledOn`.
327
+ *
328
+ * So `{ type: 'element:button', props: { disabled: true } }` really does
329
+ * produce a disabled button - the node GATE did not bite, but the author got
330
+ * the effect they asked for, and a refusal there would send them looking for a
331
+ * defect that is not on their screen. Every other row stays refused on every
332
+ * family, including the `element:*` visibility rows, which nothing honours.
333
+ */
334
+ const BAG_HONOURED_GATE_KEYS = new Set(['disabled']);
335
+ /**
336
+ * Build the refusal message. Separate from the emit so a test can assert the
337
+ * words a developer is going to read, not merely that something was logged.
338
+ *
339
+ * Carries the MIGRATION LINE the ruling requires by name - *"the implementing
340
+ * round should pair the refusal with a named migration line in the diagnostic,
341
+ * not let half the keys quietly start working"*. It names the keys it refused
342
+ * and the two spellings that do work, and it names nothing else: there is no
343
+ * per-key canonical-alias table here, because inventing one would be a second
344
+ * declaration of ADR-0089's normalization competing with the spec's own.
345
+ */
346
+ export function formatRefusedPropsPredicateMessage(type, id, refusedKeys) {
347
+ const keyList = refusedKeys.map(k => `\`${k}\``).join(', ');
348
+ const one = refusedKeys.length === 1;
349
+ return (`${REFUSED_PROPS_PREDICATE_PREFIX} - node ${describeAddress(type, id)}\n` +
350
+ ` ${one ? 'Key' : 'Keys'} parked under \`props\`: ${keyList}\n` +
351
+ '`props` is NOT an authoring surface for the node gates. `SchemaRenderer`\n' +
352
+ 'hoists `properties.*` onto the node and reads its visibility / enablement\n' +
353
+ 'gates from there; nothing copies `props.*`, so this predicate is never a key\n' +
354
+ `either gate can see. ${one ? 'It gates' : 'They gate'} nothing - the node renders and stays enabled\n` +
355
+ 'exactly as if the predicate said yes, which is why nothing on screen says so.\n' +
356
+ ` MIGRATION: move ${one ? 'it' : 'them'} out of \`props\` - onto the node itself\n` +
357
+ '(`{ "type": "card", "visibleWhen": ... }`, the spelling the spec declares), or\n' +
358
+ 'into the `properties` bag, which IS hoisted. The worked pair is in\n' +
359
+ '`skills/objectui/rules/protocol.md`. (objectui#9108)');
360
+ }
361
+ /**
362
+ * Which node-gate predicate keys of the PARKED `props` bag are refused, or
363
+ * `null` when there is nothing to say.
364
+ *
365
+ * `nodeGateKeys` is passed IN rather than declared here: `SchemaRenderer` owns
366
+ * the two chain declarations and derives this union from them, so a leg added
367
+ * to either chain is covered by the same edit that adds it and this module
368
+ * never grows a twin list to drift from them (AGENTS.md #9).
369
+ *
370
+ * `parkedPropsBag` is the value of
371
+ * `propsWithoutCanonicalKeys(schema.props, schema.properties)` - the same
372
+ * subtraction the outgoing bag and the objectui#6708 diagnostic already use, so
373
+ * the two cases stay un-confused exactly as they are there: a key BOTH bags
374
+ * declare has already been subtracted (objectui#5123: `properties` wins, the
375
+ * author is getting the canonical answer and nothing is parked), while a key
376
+ * only `props` declares survives into that bag and is precisely the one no gate
377
+ * can see. A degenerate `props` contributes no keys through that same function,
378
+ * so `props: 'not-a-bag'` refuses nothing here - its defect is a different
379
+ * question (objectui#6752).
380
+ */
381
+ export function collectRefusedPropsPredicateKeys(type, nodeGateKeys, parkedPropsBag) {
382
+ if (!isConfigBag(parkedPropsBag))
383
+ return null;
384
+ const bagReader = readsPropsBag(type);
385
+ const refused = Object.keys(parkedPropsBag).filter(key => nodeGateKeys.has(key) && !(bagReader && BAG_HONOURED_GATE_KEYS.has(key)));
386
+ return refused.length > 0 ? refused : null;
387
+ }
388
+ /**
389
+ * Reported messages, so a re-render - or a second node carrying the same
390
+ * authoring bug - does not repeat the line. Module state, exactly like
391
+ * {@link reportDroppedPropsBag}'s `Set` next door, and reset the same way for
392
+ * tests.
393
+ */
394
+ const _refusedPropsPredicates = new Set();
395
+ /**
396
+ * Test-only reset for the dedupe above. Without it the second test to assert
397
+ * the same refusal reads the first test's dedupe entry and sees silence - a
398
+ * green run that checked nothing.
399
+ */
400
+ export function __resetRefusedPropsPredicateWarnings() {
401
+ _refusedPropsPredicates.clear();
402
+ }
403
+ /**
404
+ * Reports a node-gate predicate parked under `props`, in DEVELOPMENT AND IN
405
+ * PRODUCTION - read {@link REFUSED_PROPS_PREDICATE_PREFIX}'s docblock for why
406
+ * this leg carries neither the `__DEV__` gate nor the `console.warn` severity
407
+ * of its neighbour in this module.
408
+ *
409
+ * Returns the message it emitted (or `null`) so a caller or a test can read the
410
+ * decision rather than infer it from a spy.
411
+ */
412
+ export function reportRefusedPropsPredicate(type, id, nodeGateKeys, parkedPropsBag) {
413
+ const refusedKeys = collectRefusedPropsPredicateKeys(type, nodeGateKeys, parkedPropsBag);
414
+ if (!refusedKeys)
415
+ return null;
416
+ const message = formatRefusedPropsPredicateMessage(type, id, refusedKeys);
417
+ if (_refusedPropsPredicates.has(message))
418
+ return null;
419
+ _refusedPropsPredicates.add(message);
420
+ console.error(message);
421
+ return message;
422
+ }
@@ -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
+ * Test-only reset for the dedupe above. Without it the second test to assert
10
+ * the same warning reads the first test's dedupe entry and sees silence — a
11
+ * green run that checked nothing.
12
+ *
13
+ * ⚠️ The dangerous shape is the ABSENCE assertion, and it is why this export is
14
+ * a correctness affordance rather than a convenience: a test that asserts this
15
+ * warning did NOT fire for a `type#id` an earlier test already warned for is
16
+ * green whatever the renderer does, and it reports that green as coverage
17
+ * (objectui#9959). `SchemaRenderer.refusedDataPropWarnReset-9959.test.tsx`
18
+ * renders one `type#id` across separate `it()` blocks and goes red when this
19
+ * body stops clearing.
20
+ */
21
+ export declare function __resetRefusedDataPropWarnings(): void;
22
+ /**
23
+ * Say why an authored `data` key did not reach the block (objectui#9571,
24
+ * ruling objectui#8348 Q2-C, decision batch #136 item 3, maintainer 「同意」).
25
+ *
26
+ * ## Why this diagnostic is part of the change and not decoration
27
+ *
28
+ * ⛔ MEASURED, and it corrects the card's own premise: the ladder emits NO
29
+ * runtime signal when it refuses an off-arm `data`. `resolveRecordSourceConfig`
30
+ * returns `null` and falls through silently, and `validateSchema` — the
31
+ * `__DEV__` pass in `SchemaRenderer.tsx` — never reads `data` against the
32
+ * block's row at all. The refusal is loud at AUTHORING time (`os validate`, the
33
+ * save gate and the zod row all reject a bare array) and mute at render time.
34
+ *
35
+ * So without this line, retiring the props carrier is exactly the failure shape
36
+ * AGENTS.md #0.1 and `ObjectGrid`'s own column diagnostic exist to prevent:
37
+ * renderer and author disagree, and the author gets a success receipt — a page
38
+ * whose rows were on screen yesterday is blank today, with nothing anywhere
39
+ * naming the key that was dropped or the spelling that would work.
40
+ *
41
+ * Read-only and `__DEV__`-only: it reports the decision made at the call site
42
+ * and changes nothing about what is rendered.
43
+ */
44
+ export declare function reportRefusedDataPropSpread(type: string, id: string | undefined): void;
@@ -0,0 +1,94 @@
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
+ * Dev-build diagnostic: an authored `data` key that did NOT reach the block as
10
+ * a React prop, because that block's published `data` row is the `ViewData`
11
+ * OBJECT arm (objectui#9571, extended to the legacy `props` alias bag by
12
+ * objectui#9758).
13
+ *
14
+ * ## Why the diagnostic lives HERE and not in `SchemaRenderer.tsx` (objectui#9959)
15
+ *
16
+ * It was born as module state inside `SchemaRenderer.tsx`, next to the two call
17
+ * sites that emit it. Every other dev diagnostic in this directory —
18
+ * `propsBagDiagnostic.ts`'s two and `visibilityDiagnostic.ts`'s one — pairs its
19
+ * dedupe `Set` with an exported test-only reset, and each of those resets says
20
+ * in its own docblock what it exists to prevent. This one had no reset, and it
21
+ * could not simply grow one where it stood: `index.ts` re-exports
22
+ * `SchemaRenderer.js` with a star, so a `__reset` added there would have been a
23
+ * new member of this package's PUBLISHED surface — a contract change, for a
24
+ * test affordance. Moving the `Set`, the emit and the reset into this module
25
+ * puts all three where their siblings already live and adds nothing to the
26
+ * published entry, which is the whole shape of the remedy.
27
+ *
28
+ * ## The caller owns the production gate
29
+ *
30
+ * Same contract as `reportDroppedPropsBag` next door: `SchemaRenderer` applies
31
+ * `__DEV__` at the call site, so this stays one dev-only branch there and the
32
+ * whole module is dead code in a production build.
33
+ */
34
+ /**
35
+ * Warn ONCE per distinct refused node, not once per render (objectui#9571).
36
+ *
37
+ * The strip at the call site runs on every render of the node, and a warning
38
+ * that floods the console is a warning that gets muted — the same warn-once
39
+ * discipline `ObjectMap`'s legacy-config notice and the visibility-predicate
40
+ * diagnostics already use. Keyed on the node's type plus its id, which is the
41
+ * pair an author can act on; a node with no `id` is keyed on its type alone and
42
+ * so warns once per type, which is the honest ceiling for something that cannot
43
+ * be told apart.
44
+ */
45
+ const _warnedRefusedDataPropNodes = new Set();
46
+ /**
47
+ * Test-only reset for the dedupe above. Without it the second test to assert
48
+ * the same warning reads the first test's dedupe entry and sees silence — a
49
+ * green run that checked nothing.
50
+ *
51
+ * ⚠️ The dangerous shape is the ABSENCE assertion, and it is why this export is
52
+ * a correctness affordance rather than a convenience: a test that asserts this
53
+ * warning did NOT fire for a `type#id` an earlier test already warned for is
54
+ * green whatever the renderer does, and it reports that green as coverage
55
+ * (objectui#9959). `SchemaRenderer.refusedDataPropWarnReset-9959.test.tsx`
56
+ * renders one `type#id` across separate `it()` blocks and goes red when this
57
+ * body stops clearing.
58
+ */
59
+ export function __resetRefusedDataPropWarnings() {
60
+ _warnedRefusedDataPropNodes.clear();
61
+ }
62
+ /**
63
+ * Say why an authored `data` key did not reach the block (objectui#9571,
64
+ * ruling objectui#8348 Q2-C, decision batch #136 item 3, maintainer 「同意」).
65
+ *
66
+ * ## Why this diagnostic is part of the change and not decoration
67
+ *
68
+ * ⛔ MEASURED, and it corrects the card's own premise: the ladder emits NO
69
+ * runtime signal when it refuses an off-arm `data`. `resolveRecordSourceConfig`
70
+ * returns `null` and falls through silently, and `validateSchema` — the
71
+ * `__DEV__` pass in `SchemaRenderer.tsx` — never reads `data` against the
72
+ * block's row at all. The refusal is loud at AUTHORING time (`os validate`, the
73
+ * save gate and the zod row all reject a bare array) and mute at render time.
74
+ *
75
+ * So without this line, retiring the props carrier is exactly the failure shape
76
+ * AGENTS.md #0.1 and `ObjectGrid`'s own column diagnostic exist to prevent:
77
+ * renderer and author disagree, and the author gets a success receipt — a page
78
+ * whose rows were on screen yesterday is blank today, with nothing anywhere
79
+ * naming the key that was dropped or the spelling that would work.
80
+ *
81
+ * Read-only and `__DEV__`-only: it reports the decision made at the call site
82
+ * and changes nothing about what is rendered.
83
+ */
84
+ export function reportRefusedDataPropSpread(type, id) {
85
+ const key = id === undefined ? type : `${type}#${id}`;
86
+ if (_warnedRefusedDataPropNodes.has(key))
87
+ return;
88
+ _warnedRefusedDataPropNodes.add(key);
89
+ console.warn(`[ObjectUI] SchemaRenderer: the authored \`data\` key on <${type}${id === undefined ? '' : ` id="${id}"`}> was NOT passed to the component as a React prop. This block's published ` +
90
+ '`data` row is the `ViewData` OBJECT arm, so `data` is read from the schema ' +
91
+ 'and judged by that row (objectui#8348). Inline rows go at ' +
92
+ '`data: { provider: "value", items: [...] }`; a bare array under `data` is ' +
93
+ 'refused. A HOST passing rows down as a React `data` prop is unaffected.');
94
+ }
@@ -16,7 +16,12 @@ export declare function findExpressionSources(value: string): string[];
16
16
  /** Which authored channel a residual expression was found on. */
17
17
  export type UnevaluatedChannel = 'schema' | 'properties' | 'props';
18
18
  export interface UnevaluatedExpressionFinding {
19
- /** The key holding the raw source. */
19
+ /**
20
+ * The key holding the raw source — or, for a leaf of a `params` bag, that
21
+ * leaf's path from the bag (`params.target.id`, `params.ids[0]`), or, for a
22
+ * member of an inline locale map, the key and the member's locale
23
+ * (`title.zh-CN`).
24
+ */
20
25
  key: string;
21
26
  /** The authored channel the key was read from. */
22
27
  channel: UnevaluatedChannel;
@@ -51,6 +56,44 @@ export declare const UNEVALUATED_EXPRESSION_PREFIX = "[ObjectUI] Unevaluated exp
51
56
  /**
52
57
  * Build the message. Separate from the emit so a test can assert the words a
53
58
  * developer is going to read, not merely that something was logged.
59
+ *
60
+ * ## Why `properties.*` is in the channel list (objectui#7849)
61
+ *
62
+ * It was missing, and the omission CONTRADICTED the sibling diagnostic in
63
+ * `propsBagDiagnostic.ts`, which tells an author who wrote the same key one
64
+ * envelope out: *"`props` is NOT hoisted onto the node — only `properties.*`
65
+ * is … Write them under `properties` instead."* An author whose node trips
66
+ * both messages was told to use `properties` by one and, by the other, that
67
+ * the only channels that work are `content` or host-side resolution.
68
+ *
69
+ * The enumeration was the wrong half. `properties` is not a channel on its way
70
+ * out and there is no retirement on record for it: `@objectstack/spec@17.2.0`
71
+ * calls the vocabulary carried there *"ALIVE — this is not dead surface to
72
+ * retire under ADR-0049"*, keeps `PageComponentSchema.properties` as the open
73
+ * carrier ON PURPOSE, gates it at the authoring door
74
+ * (`validate-component-props.ts`), and tombstones no part of it via
75
+ * `retiredKey()`. In THIS repo `props` — not `properties` — is the spelling
76
+ * annotated as the legacy alias (`SchemaRenderer.tsx`: "the legacy `props`
77
+ * bag, minus every key the canonical `properties` bag also …").
78
+ *
79
+ * Measured on this branch's base through the real renderers, not inferred:
80
+ * `badge` with `properties.label = '${data.status}'` renders `completed` and
81
+ * applies the variant classes; `card` with a conditional
82
+ * `properties.className` renders `class="… border-red-500"`; the same keys
83
+ * under `props` render an EMPTY badge body and no `border-red-500`.
84
+ *
85
+ * The words are borrowed from the sibling message on purpose — one hoist, one
86
+ * vocabulary, so the two diagnostics cannot drift apart again. Pinned by
87
+ * `__tests__/diagnosticChannelConsistency.test.ts`.
88
+ *
89
+ * ## Why `params` is in it too (objectui#7867)
90
+ *
91
+ * The same defect #7849 closed, one channel later: once every string leaf of a
92
+ * `params` bag evaluates, an enumeration that left `params` out would tell an
93
+ * author whose `params.recordId` template THREW that `params` is not a channel
94
+ * at all — and send them to move a correct key somewhere else. A finding on a
95
+ * `params` leaf means the second sentence above (the expression threw, or the
96
+ * value it produced still reads as a template), never the first.
54
97
  */
55
98
  export declare function formatUnevaluatedExpressionMessage(type: unknown, id: unknown, findings: UnevaluatedExpressionFinding[]): string;
56
99
  /**