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