@object-ui/core 17.6.0 → 17.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/CHANGELOG.md +6610 -0
  2. package/README.md +101 -14
  3. package/dist/actions/ActionEngine.d.ts +4 -2
  4. package/dist/actions/ActionEngine.js +13 -8
  5. package/dist/actions/ActionRunner.d.ts +288 -27
  6. package/dist/actions/ActionRunner.js +443 -63
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/actionKeys.d.ts +9 -7
  9. package/dist/actions/actionKeys.js +57 -14
  10. package/dist/actions/actionResponse.js +2 -2
  11. package/dist/actions/index.d.ts +1 -0
  12. package/dist/actions/index.js +1 -0
  13. package/dist/actions/recordDelete.d.ts +118 -0
  14. package/dist/actions/recordDelete.js +112 -0
  15. package/dist/adapters/ValueDataSource.d.ts +6 -1
  16. package/dist/adapters/ValueDataSource.js +1090 -98
  17. package/dist/adapters/batchTransaction.d.ts +1 -1
  18. package/dist/adapters/batchTransaction.js +1 -1
  19. package/dist/builder/schema-builder.d.ts +38 -81
  20. package/dist/builder/schema-builder.js +33 -152
  21. package/dist/data-scope/ViewDataProvider.d.ts +1 -1
  22. package/dist/data-scope/ViewDataProvider.js +21 -1
  23. package/dist/data-scope/element-data-source.d.ts +261 -7
  24. package/dist/data-scope/element-data-source.js +342 -52
  25. package/dist/data-scope/index.d.ts +9 -4
  26. package/dist/data-scope/index.js +9 -4
  27. package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
  28. package/dist/evaluator/ExpressionEvaluator.js +184 -12
  29. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  30. package/dist/evaluator/FormulaFunctions.js +166 -27
  31. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  32. package/dist/evaluator/declaredPredicate.js +117 -16
  33. package/dist/evaluator/fieldRules.d.ts +88 -4
  34. package/dist/evaluator/fieldRules.js +203 -31
  35. package/dist/evaluator/index.d.ts +1 -0
  36. package/dist/evaluator/index.js +1 -0
  37. package/dist/evaluator/listConditional.d.ts +103 -34
  38. package/dist/evaluator/listConditional.js +127 -50
  39. package/dist/evaluator/optionRules.d.ts +77 -6
  40. package/dist/evaluator/optionRules.js +87 -10
  41. package/dist/evaluator/predicateInput.d.ts +15 -4
  42. package/dist/evaluator/predicateInput.js +35 -2
  43. package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
  44. package/dist/evaluator/rowPredicateCanon.js +65 -115
  45. package/dist/evaluator/subjectPermissions.d.ts +76 -0
  46. package/dist/evaluator/subjectPermissions.js +44 -0
  47. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  48. package/dist/evaluator/unevaluablePredicate.js +70 -0
  49. package/dist/index.d.ts +14 -0
  50. package/dist/index.js +80 -2
  51. package/dist/protocols/index.d.ts +0 -1
  52. package/dist/protocols/index.js +9 -1
  53. package/dist/registry/Registry.d.ts +358 -31
  54. package/dist/registry/Registry.js +263 -11
  55. package/dist/registry/WidgetRegistry.js +7 -3
  56. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  57. package/dist/registry/html-tier-intrinsics.js +110 -0
  58. package/dist/registry/public-blocks.d.ts +8 -0
  59. package/dist/registry/public-blocks.js +57 -0
  60. package/dist/theme/ThemeEngine.js +1 -1
  61. package/dist/utils/chart-category-key.d.ts +82 -0
  62. package/dist/utils/chart-category-key.js +84 -0
  63. package/dist/utils/chart-measure-key.d.ts +64 -0
  64. package/dist/utils/chart-measure-key.js +65 -0
  65. package/dist/utils/chart-presentation.d.ts +95 -112
  66. package/dist/utils/chart-presentation.js +169 -104
  67. package/dist/utils/chart-series.d.ts +131 -5
  68. package/dist/utils/chart-series.js +98 -16
  69. package/dist/utils/column-sortability.d.ts +157 -0
  70. package/dist/utils/column-sortability.js +217 -0
  71. package/dist/utils/dashboard-filters.d.ts +38 -6
  72. package/dist/utils/dashboard-filters.js +107 -61
  73. package/dist/utils/dataset-format.d.ts +211 -30
  74. package/dist/utils/dataset-format.js +389 -46
  75. package/dist/utils/date-display.d.ts +354 -0
  76. package/dist/utils/date-display.js +463 -0
  77. package/dist/utils/date-macros.js +7 -0
  78. package/dist/utils/drill-down.d.ts +78 -0
  79. package/dist/utils/drill-down.js +93 -0
  80. package/dist/utils/emptiness.d.ts +96 -0
  81. package/dist/utils/emptiness.js +101 -0
  82. package/dist/utils/expand-fields.d.ts +76 -6
  83. package/dist/utils/expand-fields.js +76 -6
  84. package/dist/utils/extract-records.d.ts +65 -5
  85. package/dist/utils/extract-records.js +65 -8
  86. package/dist/utils/filter-converter.d.ts +152 -4
  87. package/dist/utils/filter-converter.js +1092 -22
  88. package/dist/utils/filter-tokens.d.ts +49 -5
  89. package/dist/utils/filter-tokens.js +105 -33
  90. package/dist/utils/grouping-fields.d.ts +61 -0
  91. package/dist/utils/grouping-fields.js +84 -0
  92. package/dist/utils/managedBy.d.ts +3 -3
  93. package/dist/utils/managedBy.js +5 -5
  94. package/dist/utils/native-date-value.d.ts +48 -0
  95. package/dist/utils/native-date-value.js +167 -0
  96. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  97. package/dist/utils/non-grid-row-ceiling.js +141 -0
  98. package/dist/utils/normalize-list-view.d.ts +121 -1
  99. package/dist/utils/normalize-list-view.js +186 -6
  100. package/dist/utils/number-display.d.ts +63 -14
  101. package/dist/utils/number-display.js +34 -12
  102. package/dist/utils/object-aggregate-query.d.ts +113 -0
  103. package/dist/utils/object-aggregate-query.js +50 -0
  104. package/dist/utils/parent-scope.d.ts +123 -0
  105. package/dist/utils/parent-scope.js +140 -0
  106. package/dist/utils/predicate-fields.d.ts +45 -12
  107. package/dist/utils/predicate-fields.js +137 -12
  108. package/dist/utils/record-color.d.ts +75 -0
  109. package/dist/utils/record-color.js +65 -0
  110. package/dist/utils/record-source.d.ts +298 -0
  111. package/dist/utils/record-source.js +331 -0
  112. package/dist/utils/record-title.d.ts +148 -8
  113. package/dist/utils/record-title.js +220 -26
  114. package/dist/utils/reference-keys.d.ts +21 -22
  115. package/dist/utils/reference-keys.js +532 -29
  116. package/dist/utils/sort-query.d.ts +115 -15
  117. package/dist/utils/sort-query.js +240 -13
  118. package/dist/utils/text-comparand.d.ts +92 -0
  119. package/dist/utils/text-comparand.js +99 -0
  120. package/dist/utils/without-denied-fields.d.ts +49 -0
  121. package/dist/utils/without-denied-fields.js +52 -0
  122. package/dist/validation/index.d.ts +8 -4
  123. package/dist/validation/index.js +8 -4
  124. package/dist/validation/schema-validator.d.ts +1 -1
  125. package/dist/validation/schema-validator.js +200 -82
  126. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  127. package/dist/validation/validators/object-validation-engine.js +14 -3
  128. package/package.json +8 -9
  129. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  130. package/dist/data-scope/DataScopeManager.js +0 -229
  131. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  132. package/dist/protocols/ResponsiveProtocol.js +0 -158
  133. package/dist/validation/validation-engine.d.ts +0 -88
  134. package/dist/validation/validation-engine.js +0 -434
@@ -5,7 +5,81 @@
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 { ELEMENT_DATA_SOURCE_INPUT, isElementDataSourceBlock, } from '../data-scope/element-data-source.js';
8
9
  import { PUBLIC_BLOCKS } from './public-blocks.js';
10
+ import { HTML_TIER_INTRINSICS } from './html-tier-intrinsics.js';
11
+ /**
12
+ * The full type a pending lazy stub DECLARES for a bare key.
13
+ *
14
+ * `registerLazy` computes `namespace:type` and stores the stub under both that
15
+ * key and the bare one; nothing on the entry itself records which bare key it
16
+ * claims, so the claim has to be recomputed from the entry's own meta. Spelled
17
+ * once here because both doors' collision guards need it and a second copy
18
+ * would be a second thing to keep in step with `registerLazy`'s own line.
19
+ */
20
+ function lazyStubFullType(type, entry) {
21
+ return entry.meta?.namespace ? `${entry.meta.namespace}:${type}` : type;
22
+ }
23
+ /**
24
+ * Emit the spec's `dataSource` input for a registration whose renderer wraps
25
+ * `ElementDataSourceGate` (objectui#6678).
26
+ *
27
+ * ## The one place, and why it is this one
28
+ *
29
+ * The maintainer ruling of 2026-08-29 adopted option B **in the injection
30
+ * form**: the declaration is emitted mechanically at the wrapping seam so every
31
+ * gate-wrapping registration declares the key from the same place that reads it
32
+ * — one mechanism rather than nine hand-kept copies across nine packages, which
33
+ * drift and which a tenth block would simply forget. `register()` is where every
34
+ * one of those registrations passes through, so it is where the emission lands;
35
+ * the DECLARATION itself is `ELEMENT_DATA_SOURCE_INPUT`, which lives beside the
36
+ * binding's own semantics in `data-scope/element-data-source.ts`.
37
+ *
38
+ * ## What it must NOT do, which is half the ruling
39
+ *
40
+ * Widening this to every registration is option A wearing a different hat: it
41
+ * would silence `dataSource` on `flex` and `card`, which do not read it, and the
42
+ * diagnostic would lie in the other direction instead of the one it lied in
43
+ * before. So the condition is the marker and nothing else — no heuristic over
44
+ * `objectName`, no "looks object-bound".
45
+ *
46
+ * ## Idempotent, and it never overwrites
47
+ *
48
+ * A registration that declares `dataSource` ITSELF keeps its own entry: the
49
+ * emission fills a gap, it does not own the key. (Nothing declares it today —
50
+ * that is the defect — but a block whose binding needs a narrower description
51
+ * later must be able to say so without fighting this function.) Re-registering
52
+ * the same component, which the registry allows and tests do constantly, is
53
+ * likewise a no-op rather than a growing `inputs` array.
54
+ *
55
+ * ## Typed end to end — the cast is gone (`9e37d9b39`)
56
+ *
57
+ * The spliced element is an `InjectedComponentInput` (`@object-ui/types`): a
58
+ * `ComponentInput` plus the framework-set `binding` marker, so it is a plain
59
+ * subtype of the array's element type and the return value type-checks as a
60
+ * `ComponentMeta` with no assertion. This used to read `as ComponentMeta`,
61
+ * because `ELEMENT_DATA_SOURCE_INPUT` carried a hand-written inline type and
62
+ * `binding` had no declared home — and a cast at the one place the key is
63
+ * written is exactly what would have hidden any later drift between the
64
+ * constant and the type. The absence is held by `eslint.config.js`, which
65
+ * scopes `@typescript-eslint/consistent-type-assertions` with
66
+ * `assertionStyle: 'never'` to THIS FILE (objectui#8316) — an AST rule,
67
+ * because `tsc --noEmit` stays at exit 0 with the cast re-added and the
68
+ * source-text pin that used to stand here was green for `<ComponentMeta>{…}`,
69
+ * the same assertion in the other spelling.
70
+ * `packages/types/src/__tests__/injected-component-input-6950.test.ts` still
71
+ * pins the positive half, that the local below is ANNOTATED. Keep it typed: if
72
+ * the shape ever stops fitting, that line is where the compiler should say so.
73
+ */
74
+ export function withElementDataSourceInput(component, meta) {
75
+ if (!isElementDataSourceBlock(component))
76
+ return meta;
77
+ const inputs = meta?.inputs ?? [];
78
+ if (inputs.some((input) => input?.name === ELEMENT_DATA_SOURCE_INPUT.name))
79
+ return meta;
80
+ const injected = { ...ELEMENT_DATA_SOURCE_INPUT };
81
+ return { ...(meta ?? {}), inputs: [...inputs, injected] };
82
+ }
9
83
  export class Registry {
10
84
  constructor() {
11
85
  Object.defineProperty(this, "components", {
@@ -65,6 +139,9 @@ export class Registry {
65
139
  */
66
140
  register(type, component, meta) {
67
141
  const fullType = meta?.namespace ? `${meta.namespace}:${type}` : type;
142
+ // The `dataSource` declaration is EMITTED here, not written by the blocks
143
+ // (objectui#6678). See `withElementDataSourceInput`.
144
+ const resolvedMeta = withElementDataSourceInput(component, meta);
68
145
  // Warn if registering without namespace (deprecated pattern)
69
146
  if (!meta?.namespace) {
70
147
  console.warn(`Registering component "${type}" without a namespace is deprecated. ` +
@@ -74,7 +151,7 @@ export class Registry {
74
151
  ` registry.register('${type}', MyComponent);\n\n` +
75
152
  ` // After:\n` +
76
153
  ` registry.register('${type}', MyComponent, { namespace: 'my-plugin' });\n\n` +
77
- ` See: https://github.com/objectstack-ai/objectui/blob/main/MIGRATION_GUIDE.md`);
154
+ ` See: https://www.objectui.org/docs/guide/plugin-development#namespaced-registration`);
78
155
  }
79
156
  if (this.components.has(fullType)) {
80
157
  // console.warn(`Component type "${fullType}" is already registered. Overwriting.`);
@@ -82,7 +159,7 @@ export class Registry {
82
159
  this.components.set(fullType, {
83
160
  type: fullType,
84
161
  component,
85
- ...meta
162
+ ...resolvedMeta
86
163
  });
87
164
  // Also register without namespace for backward compatibility
88
165
  // This allows "button" to work even when registered as "ui:button"
@@ -101,24 +178,94 @@ export class Registry {
101
178
  `If this is intentional keep going; otherwise register "${fullType}" with ` +
102
179
  `{ skipFallback: true } so it doesn't claim the bare "${type}" key.`);
103
180
  }
181
+ else if (!existing) {
182
+ // The CROSS-TABLE half of the same guard (objectui#9821), and the half
183
+ // the contest objectui#9533 measured actually went through. The check
184
+ // above reads `components` only, so a bare key held by a pending STUB
185
+ // is invisible to it: the console declared bare `dashboard` for
186
+ // `plugin-dashboard:dashboard` at boot, this door took the same bare key
187
+ // for `view:dashboard` when the chunk landed, and because `components`
188
+ // still held nothing under `dashboard` at that moment, NOTHING warned.
189
+ // Measured, not reasoned: replaying that card's three real claimants
190
+ // against a real registry emitted zero warnings in BOTH orders.
191
+ //
192
+ // ⚠️ The guard has to fire HERE and not only in `registerLazy`, because
193
+ // a contest that is only reported in one registration order is reported
194
+ // in the order this repository does not boot in. The `report-` and
195
+ // `timeline-bare-key-ownership` pins replay both orders for exactly
196
+ // that reason.
197
+ //
198
+ // ⭐ The remedy this warning names CHANGED with objectui#9839, and it
199
+ // changed because the behaviour behind it did. It used to send the
200
+ // author away from `skipFallback: true` — correctly at the time, since
201
+ // this door cleared the bare stub outside the fallback branch, so the
202
+ // opt-out left the bare key resolving to nothing at all. The delete is
203
+ // now inside the branch, so declining the bare key leaves it with its
204
+ // owner and the opt-out settles this contest exactly as it does on the
205
+ // lazy door. Both doors therefore prescribe the same thing now, which
206
+ // `each door prescribes the remedy that is true for it` pins.
207
+ const stub = this.lazyEntries.get(type);
208
+ const stubType = stub ? lazyStubFullType(type, stub) : undefined;
209
+ if (stubType && stubType !== fullType) {
210
+ console.warn(`Component "${type}" bare-name fallback is being overwritten by "${fullType}", ` +
211
+ `which a pending lazy stub already claims for "${stubType}". Which one an ` +
212
+ `authored "${type}" node resolves to depends on whether that chunk has loaded. ` +
213
+ `If this is intentional keep going; otherwise register "${fullType}" with ` +
214
+ `{ skipFallback: true } so it doesn't claim the bare "${type}" key — that ` +
215
+ `opt-out settles it, because the bare "${type}" key is then left with ` +
216
+ `"${stubType}".`);
217
+ }
218
+ }
104
219
  this.components.set(type, {
105
220
  type: fullType, // Keep reference to namespaced type
106
221
  component,
107
- ...meta
222
+ ...resolvedMeta
108
223
  });
224
+ // The bare key now resolves to THIS registration, so the bare stub is
225
+ // ours to clear. It is cleared here — inside the branch, under the same
226
+ // predicate that took the key — and deliberately not below (objectui#9839).
227
+ this.lazyEntries.delete(type);
109
228
  }
110
- // A real component is now available — clear any matching lazy stub so we
111
- // don't keep holding the loader reference, and notify subscribers.
229
+ // A real component is now available — clear the stub for the key this
230
+ // registration actually claimed, so we don't keep holding the loader
231
+ // reference, and notify subscribers.
232
+ //
233
+ // ⭐ `fullType` only. With no namespace `fullType === type`, so a bare
234
+ // registration still clears the bare stub — it claimed the bare key. What
235
+ // this line must NOT do is clear a bare stub the registration DECLINED:
236
+ // `skipFallback: true` skips the branch above, so `components` is never
237
+ // written under the bare key, and deleting the stub here took the bare key
238
+ // away from its owner and gave it to nobody — the stub was gone and this
239
+ // registration had refused to replace it (objectui#9839). A registration
240
+ // that opts out of the bare key opts out of clearing it too.
112
241
  this.lazyEntries.delete(fullType);
113
- this.lazyEntries.delete(type);
114
242
  this.notify();
115
243
  }
116
244
  /**
117
245
  * Remove a previously registered component. Mirrors {@link register} by
118
246
  * clearing both the namespaced key and the bare-name fallback (when the
119
- * fallback still points at this registration), plus any matching lazy stub.
247
+ * fallback still points at this registration), plus any matching lazy stub —
248
+ * where "matching" means the same thing on both tables: a stub that still
249
+ * belongs to THIS registration (objectui#9839).
120
250
  * Notifies subscribers only when something was actually removed.
121
251
  *
252
+ * ⭐ Why "matching" cannot mean "a stub under either key". This function
253
+ * already answers that question for the bare `components` key and answers it
254
+ * "ownership" — it will not drop bare `dashboard` when it resolves to
255
+ * `plugin-dashboard:dashboard`. Reading the next line as an unconditional
256
+ * delete makes the SAME call, on the SAME bare key, with the SAME owner,
257
+ * take the opposite decision purely because the owner happens to be sitting
258
+ * in the other table at that instant — and which table it is sitting in is
259
+ * exactly the "has the chunk loaded yet" race that objectui#9533 and
260
+ * objectui#9821 are about. A policy that flips on chunk-load timing is not a
261
+ * policy, so the stub gets the ownership test too.
262
+ *
263
+ * ⚠️ The BARE form (`unregister(type)` with no namespace) is unchanged and
264
+ * is still the unconditional one, on both tables: `fullType === type` there,
265
+ * so the deletes below address the bare key directly. That is the "force"
266
+ * form teardown sites pair with the namespaced call, and taking it away
267
+ * would break them.
268
+ *
122
269
  * Mainly used by tests that install a stub renderer and need to restore the
123
270
  * prior registry state on teardown, since the registry is a process-level
124
271
  * singleton shared across test files.
@@ -133,7 +280,13 @@ export class Registry {
133
280
  this.components.delete(type);
134
281
  }
135
282
  this.lazyEntries.delete(fullType);
136
- this.lazyEntries.delete(type);
283
+ // The same ownership test, on the other table.
284
+ if (namespace) {
285
+ const bareStub = this.lazyEntries.get(type);
286
+ if (bareStub && lazyStubFullType(type, bareStub) === fullType) {
287
+ this.lazyEntries.delete(type);
288
+ }
289
+ }
137
290
  if (removed)
138
291
  this.notify();
139
292
  return removed;
@@ -155,6 +308,46 @@ export class Registry {
155
308
  const entry = { loader, meta };
156
309
  this.lazyEntries.set(fullType, entry);
157
310
  if (meta?.namespace && !meta?.skipFallback) {
311
+ // Collision guard (objectui#9821). This door took the same bare-name
312
+ // fallback branch as `register` with no check at all, so a stub could
313
+ // take the bare key off another declaration in total silence — and the
314
+ // silence is why the class stayed invisible: two of the three claimants
315
+ // in the contest objectui#9533 filed came in through here.
316
+ //
317
+ // ⭐ The predicate keys on the FULL TYPE and reads BOTH tables, and both
318
+ // halves of that were decided by the census rather than chosen:
319
+ //
320
+ // - BOTH tables, because the contention is cross-table. Copying the
321
+ // guard above verbatim would compare `lazyEntries` against itself,
322
+ // and the one contested bare key on this tree is a stub in this table
323
+ // against a loaded registration in the other one. A same-table check
324
+ // would not have caught the card that produced this one.
325
+ // - FULL TYPE and not loader identity, because the ordinary correct
326
+ // shape here is a stub re-declared with a DIFFERENT loader closure
327
+ // for the SAME full type: two console files drive the same plugin
328
+ // set, and nine of this tree's bare keys are registered twice that
329
+ // way. Keying on the loader would warn on all nine, every boot.
330
+ //
331
+ // Census taken on the declared population of this repository (the same
332
+ // one `check-registry-bare-name-collisions.mjs` reads): of 31 bare keys
333
+ // a stub claims, 30 name exactly one full type across both tables — the
334
+ // stub-then-real lifecycle this must stay silent about — and one is a
335
+ // genuine contest.
336
+ const loaded = this.components.get(type);
337
+ const priorStub = loaded ? undefined : this.lazyEntries.get(type);
338
+ const claimedBy = loaded
339
+ ? loaded.type
340
+ : priorStub
341
+ ? lazyStubFullType(type, priorStub)
342
+ : undefined;
343
+ if (claimedBy && claimedBy !== fullType) {
344
+ console.warn(`Lazy component "${type}" bare-name fallback is being overwritten by "${fullType}", ` +
345
+ `which ${loaded ? 'a loaded registration' : 'another pending stub'} already claims ` +
346
+ `for "${claimedBy}". If this is intentional keep going; otherwise register ` +
347
+ `"${fullType}" with { skipFallback: true } so it doesn't claim the bare "${type}" ` +
348
+ `key — that opt-out settles it, because the stub then claims only ` +
349
+ `"${fullType}".`);
350
+ }
158
351
  this.lazyEntries.set(type, entry);
159
352
  }
160
353
  // Bump the version but do NOT notify: the set of KNOWN types grew, so
@@ -329,6 +522,47 @@ export class Registry {
329
522
  const key = namespace ? `${namespace}:${type}` : type;
330
523
  return this.components.get(key) ?? this.lazyEntries.get(key)?.meta;
331
524
  }
525
+ /**
526
+ * The deprecation `type` declares FOR `surface` — or `undefined` when it
527
+ * declares none there (objectui#6674).
528
+ *
529
+ * ## The question this makes askable
530
+ *
531
+ * "Is this type deprecated?" — which, before the {@link ComponentDeprecation}
532
+ * declaration existed, nothing could ask. The only two statements of a
533
+ * deprecation were a `console.warn` string literal in a renderer and the word
534
+ * "(Deprecated)" inside a human `label`; no gate, test or type can consult
535
+ * either. Both gates that touch component types ask whether a type RESOLVES,
536
+ * and a deprecated type resolves.
537
+ *
538
+ * ## Why the caller must name a surface
539
+ *
540
+ * Because the honest answer differs by surface, and a reader that dropped the
541
+ * distinction would let every caller re-derive it — which is how the same
542
+ * exemption ends up written N times and wrong in N-1 of them. `span` is
543
+ * deprecated for JSON-authored pages and is vocabulary of the `kind:'html'`
544
+ * tier (objectui#4000). Asking `deprecationFor('span', 'html')` therefore
545
+ * correctly answers `undefined` even when the type declares a `'json'`
546
+ * deprecation, while `deprecationFor('div', 'html')` answers the declaration,
547
+ * because `div` names both surfaces (objectui#10757). A gate over html-tier
548
+ * sources gets the right answer for either without knowing the rulings: the
549
+ * console's html compile reads exactly this to decide what it refuses.
550
+ *
551
+ * Returning the DECLARATION rather than a boolean is deliberate: the caller
552
+ * that has to report the finding also needs `replacement` to say what to
553
+ * author instead, and a boolean would send it back to the console string.
554
+ *
555
+ * Resolution is {@link getMeta}'s, so both spellings of a namespaced
556
+ * registration answer alike (`div` and `ui:div`), a `skipFallback` type
557
+ * answers only under its namespaced key, and a pending `registerLazy` stub
558
+ * answers from the meta it was registered with.
559
+ */
560
+ deprecationFor(type, surface, namespace) {
561
+ const declared = this.getMeta(type, namespace)?.deprecated;
562
+ if (!declared)
563
+ return undefined;
564
+ return declared.surfaces.includes(surface) ? declared : undefined;
565
+ }
332
566
  /**
333
567
  * Get all registered component configurations.
334
568
  *
@@ -364,9 +598,18 @@ export class Registry {
364
598
  return { ...entry.meta, type: canonical, lazy: true };
365
599
  }
366
600
  /**
367
- * Get the curated PUBLIC-tier component configs (ADR-0080) — those registered
368
- * with `tier: 'public'`. This is the contract/AI-vocabulary surface, a subset
369
- * of the full rendering capability returned by {@link getAllConfigs}.
601
+ * Get the CONTRACT-surface component configs (ADR-0080) — what the published
602
+ * `sdui.manifest.json` serialises. A subset of the full rendering capability
603
+ * returned by {@link getAllConfigs}, made of two tiers:
604
+ *
605
+ * - the curated PUBLIC tier — `PUBLIC_BLOCKS` plus registrations that opted
606
+ * in with `tier: 'public'` — the JSON-surface AI-authoring vocabulary;
607
+ * - the html tier's registered intrinsic elements — `HTML_TIER_INTRINSICS`,
608
+ * each entry stamped `tier: 'html'` (objectui#10735). A `kind:'html'` page
609
+ * may author these tags and the gate that reads the manifest must accept
610
+ * them; they are NOT part of the curated vocabulary, so a consumer that
611
+ * means that vocabulary alone — the `kind:'react'` JSX scope, the block
612
+ * list, a census of curated blocks — filters on the stamp.
370
613
  *
371
614
  * Includes blocks that are only lazily registered so far; those come back
372
615
  * with `lazy: true` and no `component` (see {@link PublicComponentConfig}).
@@ -398,6 +641,15 @@ export class Registry {
398
641
  if (entry.meta?.tier === 'public' && !key.includes(':'))
399
642
  add(key, this.getContractConfig(key));
400
643
  }
644
+ // … and the html tier's registered intrinsic elements, LAST and STAMPED:
645
+ // the dedupe above means a tag on both rosters comes out curated and
646
+ // unstamped, which is why the two rosters are pinned disjoint. The stamp is
647
+ // a projection, never written back onto the registration (objectui#10735).
648
+ for (const tag of HTML_TIER_INTRINSICS) {
649
+ const cfg = this.getContractConfig(tag);
650
+ if (cfg)
651
+ add(tag, { ...cfg, tier: 'html' });
652
+ }
401
653
  return out;
402
654
  }
403
655
  /**
@@ -147,15 +147,19 @@ export class WidgetRegistry {
147
147
  label: manifest.label,
148
148
  icon: manifest.icon,
149
149
  category: manifest.category,
150
+ // `WidgetInput.label` / `defaultValue` / `advanced` are NOT copied
151
+ // across: they are ADR-0049 retirement tombstones on BOTH faces now
152
+ // (`ComponentInput` by objectui#7493 / objectui#7781,
153
+ // `WidgetInput` by objectui#7911) — the manifest serializer never
154
+ // forwarded them and no consumer of `ComponentMeta.inputs` read them,
155
+ // so this copy was the one non-test write of the three keys and it
156
+ // fed nothing. Restoring a line here would assign from a `never`.
150
157
  inputs: manifest.inputs?.map((input) => ({
151
158
  name: input.name,
152
159
  type: input.type,
153
- label: input.label,
154
- defaultValue: input.defaultValue,
155
160
  required: input.required,
156
161
  enum: input.options,
157
162
  description: input.description,
158
- advanced: input.advanced,
159
163
  })),
160
164
  defaultProps: manifest.defaultProps,
161
165
  isContainer: manifest.isContainer,
@@ -0,0 +1,90 @@
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
+ * ADR-0081 §2 — the html tier's registered intrinsic-element roster, as the
10
+ * published manifest declares it (objectui#10735, executing the maintainer's
11
+ * ruling A on objectstack#20112).
12
+ *
13
+ * A `kind:'html'` page is author-written HTML expressed as constrained JSX,
14
+ * parsed (never executed) into the SDUI tree. Its everyday tags — headings,
15
+ * paragraphs, links, lists, images, emphasis, the sectioning tags — each
16
+ * resolve to a renderer: `renderers/basic/html-elements.tsx` registers the
17
+ * flow/inline set from its `TAGS` loop, `renderers/layout/semantic.tsx` the
18
+ * seven sectioning tags, and `span` / `table` / `label` are registered by
19
+ * their own modules. The console's html-tier compile whitelists
20
+ * `ComponentRegistry.getKnownTypes()` minus the types whose registration is
21
+ * deprecated on the `'html'` surface (objectui#10757), so those pages render
22
+ * today.
23
+ *
24
+ * The PUBLISHED contract did not say so. `sdui.manifest.json` is
25
+ * `getPublicConfigs()` serialised, and that read was the curated JSON-surface
26
+ * vocabulary (`PUBLIC_BLOCKS`) alone — so the objectstack CLI gate, which reads
27
+ * the manifest as the html tier's tag whitelist, refused every plain HTML tag as
28
+ * `forbidden-tag` / `unknown-component` on pages the renderer accepts. The
29
+ * ruling: the manifest declares the html tier's registered intrinsic set
30
+ * exactly as the registry declares it, with each tag's registered `inputs` and
31
+ * child slot.
32
+ *
33
+ * ## Why a SECOND roster, and a marker, rather than entries in `PUBLIC_BLOCKS`
34
+ *
35
+ * The manifest is one flat `components` map and it feeds two readers with two
36
+ * meanings: the objectstack gate reads its keys as the html tier's whitelist;
37
+ * objectui reads the same set as the curated AI-authoring vocabulary — the
38
+ * `kind:'react'` JSX scope (`renderers/layout/react-page.tsx`), the generated
39
+ * block list, the console's contract census. Listing `h1` or `p` in
40
+ * `PUBLIC_BLOCKS` would have widened that curated vocabulary silently, which
41
+ * the ruling forbids. So the tier is declared here, on its own list, and every
42
+ * entry it contributes to `getPublicConfigs()` is stamped `tier: 'html'`;
43
+ * `manifestFromConfigs` carries the stamp into the manifest as the component's
44
+ * `tier`, and a consumer that means the CURATED vocabulary filters on it. A
45
+ * consumer that ignores the key — the objectstack gate does — sees the tags as
46
+ * legal, which is the ruling's whole intent for that reader.
47
+ *
48
+ * ## What is deliberately NOT here
49
+ *
50
+ * - `div` — deprecated on the JSON surface in favour of `box` (objectui#3965,
51
+ * PR objectui#6878) and kept out of the published contract by the same
52
+ * ruling that admits the rest: the gate keeps refusing `<div>` on an html
53
+ * page. The renderer still registers it, and its declared deprecation names
54
+ * both surfaces, `json` and `html` (objectui#10757): the console's html
55
+ * compile refuses it too, naming `box`, so the renderer and the gate agree.
56
+ * - `kbd` — a `ui` component under an HTML tag name (renders `keys` /
57
+ * `label`), not named by the ruling. Admitting it is additive and cheap on
58
+ * a named need; retracting a declared tag is a narrowing.
59
+ * - `form`, `input`, `textarea`, `select`, `switch`, `progress`, `dialog`,
60
+ * `summary`, `object`, `view` — JSON-surface components (or `field:` /
61
+ * plugin fallbacks) that happen to squat on HTML tag names; the tier
62
+ * excludes form controls by design (html-elements.tsx's own exclusion
63
+ * list) and none of the rest is an element passthrough.
64
+ * - `button`, `text`, `image`, `html` — HTML tag names that are ALREADY
65
+ * curated in `PUBLIC_BLOCKS`; they reach the manifest from there, unmarked.
66
+ *
67
+ * `code` was on that list when the roster landed: the ruling named it
68
+ * "registered elsewhere", but the only bare `code` registration was the
69
+ * `field:code` widget's namespace fallback — a code editor reading `value`, no
70
+ * child slot — so an html author's `<code>inline</code>` drew the editor and lost
71
+ * its text. objectui#10756 registers `code` as a sanitised passthrough in
72
+ * `html-elements.tsx`'s `TAGS` and has the widget stand down from the bare key
73
+ * (`FIELD_TYPES_SKIP_FALLBACK` in `@object-ui/fields`; it stays `field:code`),
74
+ * so the tag is declared here exactly as its siblings are.
75
+ *
76
+ * The console's `html-tier-manifest` test derives the population of
77
+ * HTML-named registrations from `@types/react`'s `IntrinsicElements` and
78
+ * holds every member to one of: this roster, `PUBLIC_BLOCKS`, or its exclusion
79
+ * ledger with a reason — so a registration that newly squats on a tag name
80
+ * lands there by absence.
81
+ *
82
+ * A tag listed here that is not registered is skipped, exactly as an
83
+ * unregistered `PUBLIC_BLOCKS` entry is (aspirational-safe). ⛔ A tag may not be
84
+ * on both rosters: the curated read wins the dedupe, so the html marker would
85
+ * silently not apply; `__tests__/html-tier-intrinsics.test.ts` pins the
86
+ * disjointness.
87
+ */
88
+ export declare const HTML_TIER_INTRINSICS: readonly string[];
89
+ /** Fast membership set built from {@link HTML_TIER_INTRINSICS}. */
90
+ export declare const HTML_TIER_INTRINSIC_SET: ReadonlySet<string>;
@@ -0,0 +1,110 @@
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
+ * ADR-0081 §2 — the html tier's registered intrinsic-element roster, as the
10
+ * published manifest declares it (objectui#10735, executing the maintainer's
11
+ * ruling A on objectstack#20112).
12
+ *
13
+ * A `kind:'html'` page is author-written HTML expressed as constrained JSX,
14
+ * parsed (never executed) into the SDUI tree. Its everyday tags — headings,
15
+ * paragraphs, links, lists, images, emphasis, the sectioning tags — each
16
+ * resolve to a renderer: `renderers/basic/html-elements.tsx` registers the
17
+ * flow/inline set from its `TAGS` loop, `renderers/layout/semantic.tsx` the
18
+ * seven sectioning tags, and `span` / `table` / `label` are registered by
19
+ * their own modules. The console's html-tier compile whitelists
20
+ * `ComponentRegistry.getKnownTypes()` minus the types whose registration is
21
+ * deprecated on the `'html'` surface (objectui#10757), so those pages render
22
+ * today.
23
+ *
24
+ * The PUBLISHED contract did not say so. `sdui.manifest.json` is
25
+ * `getPublicConfigs()` serialised, and that read was the curated JSON-surface
26
+ * vocabulary (`PUBLIC_BLOCKS`) alone — so the objectstack CLI gate, which reads
27
+ * the manifest as the html tier's tag whitelist, refused every plain HTML tag as
28
+ * `forbidden-tag` / `unknown-component` on pages the renderer accepts. The
29
+ * ruling: the manifest declares the html tier's registered intrinsic set
30
+ * exactly as the registry declares it, with each tag's registered `inputs` and
31
+ * child slot.
32
+ *
33
+ * ## Why a SECOND roster, and a marker, rather than entries in `PUBLIC_BLOCKS`
34
+ *
35
+ * The manifest is one flat `components` map and it feeds two readers with two
36
+ * meanings: the objectstack gate reads its keys as the html tier's whitelist;
37
+ * objectui reads the same set as the curated AI-authoring vocabulary — the
38
+ * `kind:'react'` JSX scope (`renderers/layout/react-page.tsx`), the generated
39
+ * block list, the console's contract census. Listing `h1` or `p` in
40
+ * `PUBLIC_BLOCKS` would have widened that curated vocabulary silently, which
41
+ * the ruling forbids. So the tier is declared here, on its own list, and every
42
+ * entry it contributes to `getPublicConfigs()` is stamped `tier: 'html'`;
43
+ * `manifestFromConfigs` carries the stamp into the manifest as the component's
44
+ * `tier`, and a consumer that means the CURATED vocabulary filters on it. A
45
+ * consumer that ignores the key — the objectstack gate does — sees the tags as
46
+ * legal, which is the ruling's whole intent for that reader.
47
+ *
48
+ * ## What is deliberately NOT here
49
+ *
50
+ * - `div` — deprecated on the JSON surface in favour of `box` (objectui#3965,
51
+ * PR objectui#6878) and kept out of the published contract by the same
52
+ * ruling that admits the rest: the gate keeps refusing `<div>` on an html
53
+ * page. The renderer still registers it, and its declared deprecation names
54
+ * both surfaces, `json` and `html` (objectui#10757): the console's html
55
+ * compile refuses it too, naming `box`, so the renderer and the gate agree.
56
+ * - `kbd` — a `ui` component under an HTML tag name (renders `keys` /
57
+ * `label`), not named by the ruling. Admitting it is additive and cheap on
58
+ * a named need; retracting a declared tag is a narrowing.
59
+ * - `form`, `input`, `textarea`, `select`, `switch`, `progress`, `dialog`,
60
+ * `summary`, `object`, `view` — JSON-surface components (or `field:` /
61
+ * plugin fallbacks) that happen to squat on HTML tag names; the tier
62
+ * excludes form controls by design (html-elements.tsx's own exclusion
63
+ * list) and none of the rest is an element passthrough.
64
+ * - `button`, `text`, `image`, `html` — HTML tag names that are ALREADY
65
+ * curated in `PUBLIC_BLOCKS`; they reach the manifest from there, unmarked.
66
+ *
67
+ * `code` was on that list when the roster landed: the ruling named it
68
+ * "registered elsewhere", but the only bare `code` registration was the
69
+ * `field:code` widget's namespace fallback — a code editor reading `value`, no
70
+ * child slot — so an html author's `<code>inline</code>` drew the editor and lost
71
+ * its text. objectui#10756 registers `code` as a sanitised passthrough in
72
+ * `html-elements.tsx`'s `TAGS` and has the widget stand down from the bare key
73
+ * (`FIELD_TYPES_SKIP_FALLBACK` in `@object-ui/fields`; it stays `field:code`),
74
+ * so the tag is declared here exactly as its siblings are.
75
+ *
76
+ * The console's `html-tier-manifest` test derives the population of
77
+ * HTML-named registrations from `@types/react`'s `IntrinsicElements` and
78
+ * holds every member to one of: this roster, `PUBLIC_BLOCKS`, or its exclusion
79
+ * ledger with a reason — so a registration that newly squats on a tag name
80
+ * lands there by absence.
81
+ *
82
+ * A tag listed here that is not registered is skipped, exactly as an
83
+ * unregistered `PUBLIC_BLOCKS` entry is (aspirational-safe). ⛔ A tag may not be
84
+ * on both rosters: the curated read wins the dedupe, so the html marker would
85
+ * silently not apply; `__tests__/html-tier-intrinsics.test.ts` pins the
86
+ * disjointness.
87
+ */
88
+ export const HTML_TIER_INTRINSICS = [
89
+ // ── `renderers/basic/html-elements.tsx` `TAGS` — the safe flow/inline set ──
90
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
91
+ 'p', 'a', 'blockquote', 'pre', 'code',
92
+ 'strong', 'em', 'b', 'i', 'u', 'small', 'mark', 'sub', 'sup', 'del', 'ins', 'abbr',
93
+ 'ul', 'ol', 'li', 'dl', 'dt', 'dd',
94
+ 'figure', 'figcaption', 'img', 'hr', 'br', 'time', 'address', 'cite', 'q',
95
+ // ── registered elsewhere, each with its own module ────────────────────────
96
+ // `renderers/basic/span.tsx` — deprecated on the JSON surface, permanent
97
+ // vocabulary of the html tier (objectui#4000); the declaration travels with
98
+ // the registration, so nothing here restates it.
99
+ 'span',
100
+ // `renderers/complex/table.tsx` — the data-driven table (`columns` / `data`),
101
+ // declared exactly as registered: no child slot, so a `<table>` authored with
102
+ // element children draws `not-a-container` rather than rendering them.
103
+ 'table',
104
+ // `renderers/form/label.tsx` — reads `text`, declared exactly as registered.
105
+ 'label',
106
+ // ── `renderers/layout/semantic.tsx` — the seven sectioning tags ──────────
107
+ 'aside', 'main', 'header', 'nav', 'footer', 'section', 'article',
108
+ ];
109
+ /** Fast membership set built from {@link HTML_TIER_INTRINSICS}. */
110
+ export const HTML_TIER_INTRINSIC_SET = new Set(HTML_TIER_INTRINSICS);
@@ -21,6 +21,14 @@
21
21
  *
22
22
  * This is a single, reviewable source of truth for the public surface — prefer
23
23
  * editing this list over scattering `tier` flags across registration sites.
24
+ *
25
+ * ⛔ The html tier's intrinsic elements (`h1`, `p`, `a`, the sectioning tags, …)
26
+ * do NOT belong here. They reach the published manifest through the second
27
+ * roster beside this one, `HTML_TIER_INTRINSICS`, stamped `tier: 'html'`, so
28
+ * that the manifest's other reader — the objectstack gate that whitelists a
29
+ * `kind:'html'` page's tags — accepts them without this curated JSON-surface
30
+ * vocabulary growing (objectui#10735). Listing one here would widen the
31
+ * vocabulary silently; `html-tier-intrinsics.test.ts` pins the two disjoint.
24
32
  */
25
33
  export declare const PUBLIC_BLOCKS: readonly string[];
26
34
  /** Fast membership set built from {@link PUBLIC_BLOCKS}. */