@object-ui/core 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 (147) hide show
  1. package/CHANGELOG.md +7260 -0
  2. package/README.md +228 -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 +473 -69
  6. package/dist/actions/ActionRunner.js +792 -125
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/UndoManager.d.ts +8 -13
  9. package/dist/actions/actionKeys.d.ts +9 -7
  10. package/dist/actions/actionKeys.js +67 -14
  11. package/dist/actions/actionResponse.js +2 -2
  12. package/dist/actions/index.d.ts +2 -0
  13. package/dist/actions/index.js +2 -0
  14. package/dist/actions/navigationTarget.d.ts +41 -0
  15. package/dist/actions/navigationTarget.js +20 -0
  16. package/dist/actions/recordDelete.d.ts +154 -0
  17. package/dist/actions/recordDelete.js +110 -0
  18. package/dist/adapters/ApiDataSource.d.ts +8 -4
  19. package/dist/adapters/ApiDataSource.js +19 -7
  20. package/dist/adapters/ValueDataSource.d.ts +6 -1
  21. package/dist/adapters/ValueDataSource.js +1090 -98
  22. package/dist/adapters/batchTransaction.d.ts +1 -1
  23. package/dist/adapters/batchTransaction.js +1 -1
  24. package/dist/adapters/resolveDataSource.d.ts +9 -6
  25. package/dist/adapters/resolveDataSource.js +9 -12
  26. package/dist/builder/schema-builder.d.ts +38 -81
  27. package/dist/builder/schema-builder.js +33 -152
  28. package/dist/data-scope/ViewDataProvider.d.ts +8 -25
  29. package/dist/data-scope/ViewDataProvider.js +27 -54
  30. package/dist/data-scope/element-data-source.d.ts +261 -7
  31. package/dist/data-scope/element-data-source.js +342 -52
  32. package/dist/data-scope/index.d.ts +9 -4
  33. package/dist/data-scope/index.js +9 -4
  34. package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
  35. package/dist/evaluator/ExpressionEvaluator.js +184 -12
  36. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  37. package/dist/evaluator/FormulaFunctions.js +166 -27
  38. package/dist/evaluator/deadlineSettled.d.ts +40 -0
  39. package/dist/evaluator/deadlineSettled.js +45 -0
  40. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  41. package/dist/evaluator/declaredPredicate.js +117 -16
  42. package/dist/evaluator/fieldRules.d.ts +88 -4
  43. package/dist/evaluator/fieldRules.js +208 -31
  44. package/dist/evaluator/index.d.ts +2 -0
  45. package/dist/evaluator/index.js +2 -0
  46. package/dist/evaluator/listConditional.d.ts +103 -34
  47. package/dist/evaluator/listConditional.js +127 -50
  48. package/dist/evaluator/optionRules.d.ts +77 -6
  49. package/dist/evaluator/optionRules.js +87 -10
  50. package/dist/evaluator/predicateInput.d.ts +15 -4
  51. package/dist/evaluator/predicateInput.js +35 -2
  52. package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
  53. package/dist/evaluator/rowPredicateCanon.js +65 -115
  54. package/dist/evaluator/subjectPermissions.d.ts +105 -0
  55. package/dist/evaluator/subjectPermissions.js +86 -0
  56. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  57. package/dist/evaluator/unevaluablePredicate.js +70 -0
  58. package/dist/index.d.ts +15 -0
  59. package/dist/index.js +83 -2
  60. package/dist/protocols/index.d.ts +0 -1
  61. package/dist/protocols/index.js +9 -1
  62. package/dist/registry/Registry.d.ts +382 -34
  63. package/dist/registry/Registry.js +247 -12
  64. package/dist/registry/WidgetRegistry.js +7 -3
  65. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  66. package/dist/registry/html-tier-intrinsics.js +110 -0
  67. package/dist/registry/public-blocks.d.ts +8 -0
  68. package/dist/registry/public-blocks.js +57 -0
  69. package/dist/theme/ThemeEngine.js +1 -1
  70. package/dist/utils/affordanceGrants.d.ts +371 -0
  71. package/dist/utils/affordanceGrants.js +252 -0
  72. package/dist/utils/chart-category-key.d.ts +82 -0
  73. package/dist/utils/chart-category-key.js +84 -0
  74. package/dist/utils/chart-measure-key.d.ts +64 -0
  75. package/dist/utils/chart-measure-key.js +65 -0
  76. package/dist/utils/chart-presentation.d.ts +95 -112
  77. package/dist/utils/chart-presentation.js +169 -104
  78. package/dist/utils/chart-series.d.ts +165 -5
  79. package/dist/utils/chart-series.js +123 -18
  80. package/dist/utils/column-sortability.d.ts +157 -0
  81. package/dist/utils/column-sortability.js +217 -0
  82. package/dist/utils/dashboard-filters.d.ts +38 -6
  83. package/dist/utils/dashboard-filters.js +107 -61
  84. package/dist/utils/dataset-format.d.ts +211 -30
  85. package/dist/utils/dataset-format.js +389 -46
  86. package/dist/utils/date-display.d.ts +346 -0
  87. package/dist/utils/date-display.js +644 -0
  88. package/dist/utils/date-macros.js +7 -0
  89. package/dist/utils/drill-down.d.ts +78 -0
  90. package/dist/utils/drill-down.js +93 -0
  91. package/dist/utils/emptiness.d.ts +96 -0
  92. package/dist/utils/emptiness.js +101 -0
  93. package/dist/utils/expand-fields.d.ts +81 -9
  94. package/dist/utils/expand-fields.js +81 -9
  95. package/dist/utils/extract-records.d.ts +65 -5
  96. package/dist/utils/extract-records.js +65 -8
  97. package/dist/utils/filter-converter.d.ts +158 -6
  98. package/dist/utils/filter-converter.js +1098 -24
  99. package/dist/utils/filter-tokens.d.ts +49 -5
  100. package/dist/utils/filter-tokens.js +105 -33
  101. package/dist/utils/grouping-fields.d.ts +61 -0
  102. package/dist/utils/grouping-fields.js +84 -0
  103. package/dist/utils/managedBy.d.ts +3 -3
  104. package/dist/utils/managedBy.js +5 -5
  105. package/dist/utils/native-date-value.d.ts +48 -0
  106. package/dist/utils/native-date-value.js +167 -0
  107. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  108. package/dist/utils/non-grid-row-ceiling.js +141 -0
  109. package/dist/utils/normalize-list-view.d.ts +135 -11
  110. package/dist/utils/normalize-list-view.js +243 -64
  111. package/dist/utils/number-display.d.ts +63 -14
  112. package/dist/utils/number-display.js +34 -12
  113. package/dist/utils/object-aggregate-query.d.ts +113 -0
  114. package/dist/utils/object-aggregate-query.js +50 -0
  115. package/dist/utils/parent-scope.d.ts +123 -0
  116. package/dist/utils/parent-scope.js +140 -0
  117. package/dist/utils/predicate-fields.d.ts +45 -12
  118. package/dist/utils/predicate-fields.js +141 -12
  119. package/dist/utils/record-color.d.ts +75 -0
  120. package/dist/utils/record-color.js +65 -0
  121. package/dist/utils/record-source.d.ts +298 -0
  122. package/dist/utils/record-source.js +331 -0
  123. package/dist/utils/record-title.d.ts +148 -8
  124. package/dist/utils/record-title.js +220 -26
  125. package/dist/utils/reference-keys.d.ts +21 -22
  126. package/dist/utils/reference-keys.js +532 -29
  127. package/dist/utils/sort-query.d.ts +115 -15
  128. package/dist/utils/sort-query.js +240 -13
  129. package/dist/utils/text-comparand.d.ts +92 -0
  130. package/dist/utils/text-comparand.js +99 -0
  131. package/dist/utils/without-denied-fields.d.ts +49 -0
  132. package/dist/utils/without-denied-fields.js +52 -0
  133. package/dist/validation/index.d.ts +8 -4
  134. package/dist/validation/index.js +8 -4
  135. package/dist/validation/schema-validator.d.ts +1 -1
  136. package/dist/validation/schema-validator.js +225 -83
  137. package/dist/validation/server-owned-value.d.ts +5 -1
  138. package/dist/validation/server-owned-value.js +5 -1
  139. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  140. package/dist/validation/validators/object-validation-engine.js +14 -3
  141. package/package.json +8 -9
  142. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  143. package/dist/data-scope/DataScopeManager.js +0 -229
  144. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  145. package/dist/protocols/ResponsiveProtocol.js +0 -158
  146. package/dist/validation/validation-engine.d.ts +0 -88
  147. package/dist/validation/validation-engine.js +0 -434
@@ -5,7 +5,69 @@
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
+ * Emit the spec's `dataSource` input for a registration whose renderer wraps
13
+ * `ElementDataSourceGate` (objectui#6678).
14
+ *
15
+ * ## The one place, and why it is this one
16
+ *
17
+ * The maintainer ruling of 2026-08-29 adopted option B **in the injection
18
+ * form**: the declaration is emitted mechanically at the wrapping seam so every
19
+ * gate-wrapping registration declares the key from the same place that reads it
20
+ * — one mechanism rather than nine hand-kept copies across nine packages, which
21
+ * drift and which a tenth block would simply forget. `register()` is where every
22
+ * one of those registrations passes through, so it is where the emission lands;
23
+ * the DECLARATION itself is `ELEMENT_DATA_SOURCE_INPUT`, which lives beside the
24
+ * binding's own semantics in `data-scope/element-data-source.ts`.
25
+ *
26
+ * ## What it must NOT do, which is half the ruling
27
+ *
28
+ * Widening this to every registration is option A wearing a different hat: it
29
+ * would silence `dataSource` on `flex` and `card`, which do not read it, and the
30
+ * diagnostic would lie in the other direction instead of the one it lied in
31
+ * before. So the condition is the marker and nothing else — no heuristic over
32
+ * `objectName`, no "looks object-bound".
33
+ *
34
+ * ## Idempotent, and it never overwrites
35
+ *
36
+ * A registration that declares `dataSource` ITSELF keeps its own entry: the
37
+ * emission fills a gap, it does not own the key. (Nothing declares it today —
38
+ * that is the defect — but a block whose binding needs a narrower description
39
+ * later must be able to say so without fighting this function.) Re-registering
40
+ * the same component, which the registry allows and tests do constantly, is
41
+ * likewise a no-op rather than a growing `inputs` array.
42
+ *
43
+ * ## Typed end to end — the cast is gone (`9e37d9b39`)
44
+ *
45
+ * The spliced element is an `InjectedComponentInput` (`@object-ui/types`): a
46
+ * `ComponentInput` plus the framework-set `binding` marker, so it is a plain
47
+ * subtype of the array's element type and the return value type-checks as a
48
+ * `RegistryComponentMeta` with no assertion. This used to read `as ComponentMeta`,
49
+ * because `ELEMENT_DATA_SOURCE_INPUT` carried a hand-written inline type and
50
+ * `binding` had no declared home — and a cast at the one place the key is
51
+ * written is exactly what would have hidden any later drift between the
52
+ * constant and the type. The absence is held by `eslint.config.js`, which
53
+ * scopes `@typescript-eslint/consistent-type-assertions` with
54
+ * `assertionStyle: 'never'` to THIS FILE (objectui#8316) — an AST rule,
55
+ * because `tsc --noEmit` stays at exit 0 with the cast re-added and the
56
+ * source-text pin that used to stand here was green for `<ComponentMeta>{…}`,
57
+ * the same assertion in the other spelling.
58
+ * `packages/types/src/__tests__/injected-component-input-6950.test.ts` still
59
+ * pins the positive half, that the local below is ANNOTATED. Keep it typed: if
60
+ * the shape ever stops fitting, that line is where the compiler should say so.
61
+ */
62
+ export function withElementDataSourceInput(component, meta) {
63
+ if (!isElementDataSourceBlock(component))
64
+ return meta;
65
+ const inputs = meta?.inputs ?? [];
66
+ if (inputs.some((input) => input?.name === ELEMENT_DATA_SOURCE_INPUT.name))
67
+ return meta;
68
+ const injected = { ...ELEMENT_DATA_SOURCE_INPUT };
69
+ return { ...(meta ?? {}), inputs: [...inputs, injected] };
70
+ }
9
71
  export class Registry {
10
72
  constructor() {
11
73
  Object.defineProperty(this, "components", {
@@ -65,6 +127,9 @@ export class Registry {
65
127
  */
66
128
  register(type, component, meta) {
67
129
  const fullType = meta?.namespace ? `${meta.namespace}:${type}` : type;
130
+ // The `dataSource` declaration is EMITTED here, not written by the blocks
131
+ // (objectui#6678). See `withElementDataSourceInput`.
132
+ const resolvedMeta = withElementDataSourceInput(component, meta);
68
133
  // Warn if registering without namespace (deprecated pattern)
69
134
  if (!meta?.namespace) {
70
135
  console.warn(`Registering component "${type}" without a namespace is deprecated. ` +
@@ -74,7 +139,7 @@ export class Registry {
74
139
  ` registry.register('${type}', MyComponent);\n\n` +
75
140
  ` // After:\n` +
76
141
  ` registry.register('${type}', MyComponent, { namespace: 'my-plugin' });\n\n` +
77
- ` See: https://github.com/objectstack-ai/objectui/blob/main/MIGRATION_GUIDE.md`);
142
+ ` See: https://www.objectui.org/docs/guide/plugin-development#namespaced-registration`);
78
143
  }
79
144
  if (this.components.has(fullType)) {
80
145
  // console.warn(`Component type "${fullType}" is already registered. Overwriting.`);
@@ -82,7 +147,7 @@ export class Registry {
82
147
  this.components.set(fullType, {
83
148
  type: fullType,
84
149
  component,
85
- ...meta
150
+ ...resolvedMeta
86
151
  });
87
152
  // Also register without namespace for backward compatibility
88
153
  // This allows "button" to work even when registered as "ui:button"
@@ -101,24 +166,93 @@ export class Registry {
101
166
  `If this is intentional keep going; otherwise register "${fullType}" with ` +
102
167
  `{ skipFallback: true } so it doesn't claim the bare "${type}" key.`);
103
168
  }
169
+ else if (!existing) {
170
+ // The CROSS-TABLE half of the same guard (objectui#9821), and the half
171
+ // the contest objectui#9533 measured actually went through. The check
172
+ // above reads `components` only, so a bare key held by a pending STUB
173
+ // is invisible to it: the console declared bare `dashboard` for
174
+ // `plugin-dashboard:dashboard` at boot, this door took the same bare key
175
+ // for `view:dashboard` when the chunk landed, and because `components`
176
+ // still held nothing under `dashboard` at that moment, NOTHING warned.
177
+ // Measured, not reasoned: replaying that card's three real claimants
178
+ // against a real registry emitted zero warnings in BOTH orders.
179
+ //
180
+ // ⚠️ The guard has to fire HERE and not only in `registerLazy`, because
181
+ // a contest that is only reported in one registration order is reported
182
+ // in the order this repository does not boot in. The `report-` and
183
+ // `timeline-bare-key-ownership` pins replay both orders for exactly
184
+ // that reason.
185
+ //
186
+ // ⭐ The remedy this warning names CHANGED with objectui#9839, and it
187
+ // changed because the behaviour behind it did. It used to send the
188
+ // author away from `skipFallback: true` — correctly at the time, since
189
+ // this door cleared the bare stub outside the fallback branch, so the
190
+ // opt-out left the bare key resolving to nothing at all. The delete is
191
+ // now inside the branch, so declining the bare key leaves it with its
192
+ // owner and the opt-out settles this contest exactly as it does on the
193
+ // lazy door. Both doors therefore prescribe the same thing now, which
194
+ // `each door prescribes the remedy that is true for it` pins.
195
+ const stubType = this.lazyEntries.get(type)?.fullType;
196
+ if (stubType && stubType !== fullType) {
197
+ console.warn(`Component "${type}" bare-name fallback is being overwritten by "${fullType}", ` +
198
+ `which a pending lazy stub already claims for "${stubType}". Which one an ` +
199
+ `authored "${type}" node resolves to depends on whether that chunk has loaded. ` +
200
+ `If this is intentional keep going; otherwise register "${fullType}" with ` +
201
+ `{ skipFallback: true } so it doesn't claim the bare "${type}" key — that ` +
202
+ `opt-out settles it, because the bare "${type}" key is then left with ` +
203
+ `"${stubType}".`);
204
+ }
205
+ }
104
206
  this.components.set(type, {
105
207
  type: fullType, // Keep reference to namespaced type
106
208
  component,
107
- ...meta
209
+ ...resolvedMeta
108
210
  });
211
+ // The bare key now resolves to THIS registration, so the bare stub is
212
+ // ours to clear. It is cleared here — inside the branch, under the same
213
+ // predicate that took the key — and deliberately not below (objectui#9839).
214
+ this.lazyEntries.delete(type);
109
215
  }
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.
216
+ // A real component is now available — clear the stub for the key this
217
+ // registration actually claimed, so we don't keep holding the loader
218
+ // reference, and notify subscribers.
219
+ //
220
+ // ⭐ `fullType` only. With no namespace `fullType === type`, so a bare
221
+ // registration still clears the bare stub — it claimed the bare key. What
222
+ // this line must NOT do is clear a bare stub the registration DECLINED:
223
+ // `skipFallback: true` skips the branch above, so `components` is never
224
+ // written under the bare key, and deleting the stub here took the bare key
225
+ // away from its owner and gave it to nobody — the stub was gone and this
226
+ // registration had refused to replace it (objectui#9839). A registration
227
+ // that opts out of the bare key opts out of clearing it too.
112
228
  this.lazyEntries.delete(fullType);
113
- this.lazyEntries.delete(type);
114
229
  this.notify();
115
230
  }
116
231
  /**
117
232
  * Remove a previously registered component. Mirrors {@link register} by
118
233
  * clearing both the namespaced key and the bare-name fallback (when the
119
- * fallback still points at this registration), plus any matching lazy stub.
234
+ * fallback still points at this registration), plus any matching lazy stub —
235
+ * where "matching" means the same thing on both tables: a stub that still
236
+ * belongs to THIS registration (objectui#9839).
120
237
  * Notifies subscribers only when something was actually removed.
121
238
  *
239
+ * ⭐ Why "matching" cannot mean "a stub under either key". This function
240
+ * already answers that question for the bare `components` key and answers it
241
+ * "ownership" — it will not drop bare `dashboard` when it resolves to
242
+ * `plugin-dashboard:dashboard`. Reading the next line as an unconditional
243
+ * delete makes the SAME call, on the SAME bare key, with the SAME owner,
244
+ * take the opposite decision purely because the owner happens to be sitting
245
+ * in the other table at that instant — and which table it is sitting in is
246
+ * exactly the "has the chunk loaded yet" race that objectui#9533 and
247
+ * objectui#9821 are about. A policy that flips on chunk-load timing is not a
248
+ * policy, so the stub gets the ownership test too.
249
+ *
250
+ * ⚠️ The BARE form (`unregister(type)` with no namespace) is unchanged and
251
+ * is still the unconditional one, on both tables: `fullType === type` there,
252
+ * so the deletes below address the bare key directly. That is the "force"
253
+ * form teardown sites pair with the namespaced call, and taking it away
254
+ * would break them.
255
+ *
122
256
  * Mainly used by tests that install a stub renderer and need to restore the
123
257
  * prior registry state on teardown, since the registry is a process-level
124
258
  * singleton shared across test files.
@@ -133,7 +267,13 @@ export class Registry {
133
267
  this.components.delete(type);
134
268
  }
135
269
  this.lazyEntries.delete(fullType);
136
- this.lazyEntries.delete(type);
270
+ // The same ownership test, on the other table.
271
+ if (namespace) {
272
+ const bareStub = this.lazyEntries.get(type);
273
+ if (bareStub && bareStub.fullType === fullType) {
274
+ this.lazyEntries.delete(type);
275
+ }
276
+ }
137
277
  if (removed)
138
278
  this.notify();
139
279
  return removed;
@@ -152,9 +292,45 @@ export class Registry {
152
292
  */
153
293
  registerLazy(type, loader, meta) {
154
294
  const fullType = meta?.namespace ? `${meta.namespace}:${type}` : type;
155
- const entry = { loader, meta };
295
+ const entry = { loader, meta, fullType };
156
296
  this.lazyEntries.set(fullType, entry);
157
297
  if (meta?.namespace && !meta?.skipFallback) {
298
+ // Collision guard (objectui#9821). This door took the same bare-name
299
+ // fallback branch as `register` with no check at all, so a stub could
300
+ // take the bare key off another declaration in total silence — and the
301
+ // silence is why the class stayed invisible: two of the three claimants
302
+ // in the contest objectui#9533 filed came in through here.
303
+ //
304
+ // ⭐ The predicate keys on the FULL TYPE and reads BOTH tables, and both
305
+ // halves of that were decided by the census rather than chosen:
306
+ //
307
+ // - BOTH tables, because the contention is cross-table. Copying the
308
+ // guard above verbatim would compare `lazyEntries` against itself,
309
+ // and the one contested bare key on this tree is a stub in this table
310
+ // against a loaded registration in the other one. A same-table check
311
+ // would not have caught the card that produced this one.
312
+ // - FULL TYPE and not loader identity, because the ordinary correct
313
+ // shape here is a stub re-declared with a DIFFERENT loader closure
314
+ // for the SAME full type: two console files drive the same plugin
315
+ // set, and nine of this tree's bare keys are registered twice that
316
+ // way. Keying on the loader would warn on all nine, every boot.
317
+ //
318
+ // Census taken on the declared population of this repository (the same
319
+ // one `check-registry-bare-name-collisions.mjs` reads): of 31 bare keys
320
+ // a stub claims, 30 name exactly one full type across both tables — the
321
+ // stub-then-real lifecycle this must stay silent about — and one is a
322
+ // genuine contest.
323
+ const loaded = this.components.get(type);
324
+ const priorStub = loaded ? undefined : this.lazyEntries.get(type);
325
+ const claimedBy = loaded ? loaded.type : priorStub?.fullType;
326
+ if (claimedBy && claimedBy !== fullType) {
327
+ console.warn(`Lazy component "${type}" bare-name fallback is being overwritten by "${fullType}", ` +
328
+ `which ${loaded ? 'a loaded registration' : 'another pending stub'} already claims ` +
329
+ `for "${claimedBy}". If this is intentional keep going; otherwise register ` +
330
+ `"${fullType}" with { skipFallback: true } so it doesn't claim the bare "${type}" ` +
331
+ `key — that opt-out settles it, because the stub then claims only ` +
332
+ `"${fullType}".`);
333
+ }
158
334
  this.lazyEntries.set(type, entry);
159
335
  }
160
336
  // Bump the version but do NOT notify: the set of KNOWN types grew, so
@@ -329,6 +505,47 @@ export class Registry {
329
505
  const key = namespace ? `${namespace}:${type}` : type;
330
506
  return this.components.get(key) ?? this.lazyEntries.get(key)?.meta;
331
507
  }
508
+ /**
509
+ * The deprecation `type` declares FOR `surface` — or `undefined` when it
510
+ * declares none there (objectui#6674).
511
+ *
512
+ * ## The question this makes askable
513
+ *
514
+ * "Is this type deprecated?" — which, before the {@link ComponentDeprecation}
515
+ * declaration existed, nothing could ask. The only two statements of a
516
+ * deprecation were a `console.warn` string literal in a renderer and the word
517
+ * "(Deprecated)" inside a human `label`; no gate, test or type can consult
518
+ * either. Both gates that touch component types ask whether a type RESOLVES,
519
+ * and a deprecated type resolves.
520
+ *
521
+ * ## Why the caller must name a surface
522
+ *
523
+ * Because the honest answer differs by surface, and a reader that dropped the
524
+ * distinction would let every caller re-derive it — which is how the same
525
+ * exemption ends up written N times and wrong in N-1 of them. `span` is
526
+ * deprecated for JSON-authored pages and is vocabulary of the `kind:'html'`
527
+ * tier (objectui#4000). Asking `deprecationFor('span', 'html')` therefore
528
+ * correctly answers `undefined` even when the type declares a `'json'`
529
+ * deprecation, while `deprecationFor('div', 'html')` answers the declaration,
530
+ * because `div` names both surfaces (objectui#10757). A gate over html-tier
531
+ * sources gets the right answer for either without knowing the rulings: the
532
+ * console's html compile reads exactly this to decide what it refuses.
533
+ *
534
+ * Returning the DECLARATION rather than a boolean is deliberate: the caller
535
+ * that has to report the finding also needs `replacement` to say what to
536
+ * author instead, and a boolean would send it back to the console string.
537
+ *
538
+ * Resolution is {@link getMeta}'s, so both spellings of a namespaced
539
+ * registration answer alike (`div` and `ui:div`), a `skipFallback` type
540
+ * answers only under its namespaced key, and a pending `registerLazy` stub
541
+ * answers from the meta it was registered with.
542
+ */
543
+ deprecationFor(type, surface, namespace) {
544
+ const declared = this.getMeta(type, namespace)?.deprecated;
545
+ if (!declared)
546
+ return undefined;
547
+ return declared.surfaces.includes(surface) ? declared : undefined;
548
+ }
332
549
  /**
333
550
  * Get all registered component configurations.
334
551
  *
@@ -364,9 +581,18 @@ export class Registry {
364
581
  return { ...entry.meta, type: canonical, lazy: true };
365
582
  }
366
583
  /**
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}.
584
+ * Get the CONTRACT-surface component configs (ADR-0080) — what the published
585
+ * `sdui.manifest.json` serialises. A subset of the full rendering capability
586
+ * returned by {@link getAllConfigs}, made of two tiers:
587
+ *
588
+ * - the curated PUBLIC tier — `PUBLIC_BLOCKS` plus registrations that opted
589
+ * in with `tier: 'public'` — the JSON-surface AI-authoring vocabulary;
590
+ * - the html tier's registered intrinsic elements — `HTML_TIER_INTRINSICS`,
591
+ * each entry stamped `tier: 'html'` (objectui#10735). A `kind:'html'` page
592
+ * may author these tags and the gate that reads the manifest must accept
593
+ * them; they are NOT part of the curated vocabulary, so a consumer that
594
+ * means that vocabulary alone — the `kind:'react'` JSX scope, the block
595
+ * list, a census of curated blocks — filters on the stamp.
370
596
  *
371
597
  * Includes blocks that are only lazily registered so far; those come back
372
598
  * with `lazy: true` and no `component` (see {@link PublicComponentConfig}).
@@ -398,6 +624,15 @@ export class Registry {
398
624
  if (entry.meta?.tier === 'public' && !key.includes(':'))
399
625
  add(key, this.getContractConfig(key));
400
626
  }
627
+ // … and the html tier's registered intrinsic elements, LAST and STAMPED:
628
+ // the dedupe above means a tag on both rosters comes out curated and
629
+ // unstamped, which is why the two rosters are pinned disjoint. The stamp is
630
+ // a projection, never written back onto the registration (objectui#10735).
631
+ for (const tag of HTML_TIER_INTRINSICS) {
632
+ const cfg = this.getContractConfig(tag);
633
+ if (cfg)
634
+ add(tag, { ...cfg, tier: 'html' });
635
+ }
401
636
  return out;
402
637
  }
403
638
  /**
@@ -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}. */