@object-ui/sdui-parser 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.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * ObjectUI — the retired `body` child-list dialect (objectui#6771)
3
+ *
4
+ * `body` was a declared twin of `children` on `BaseSchema`, and for a long
5
+ * while it was the ONLY child-list key a dozen registrations read. This tier
6
+ * never knew it: `BASE_PROPS` lists `children` and not `body`, so the one
7
+ * spelling that resolved drew `unknown-prop` — the same warning a typo draws,
8
+ * on the tier built to accept AI-authored pages, where the diagnostic IS the
9
+ * contract. The maintainer ruled the retirement (2026-09-01, option B): one
10
+ * concept, one spelling, and the spelling is `children`.
11
+ *
12
+ * This module is the tier's half of that ruling. It REPLACES the diagnostic the
13
+ * prop walk would otherwise emit for `body`, rather than adding to it — two
14
+ * diagnostics for one mistake is the shape `checkMemberTypes` already refuses
15
+ * (objectui#8067).
16
+ *
17
+ * WHY A REPLACEMENT AND NOT A BARE `unknown-prop`. After the retirement
18
+ * "`<badge>` has no prop body" is TRUE — but it is the answer that sent authors
19
+ * the wrong way in the first place. Every `body` node in this repository's own
20
+ * docs, examples and READMEs was migrated in the same change, and an author
21
+ * carrying the old spelling in from anywhere else needs the replacement NAMED,
22
+ * not merely the key refused. The two codes below are the ruling's own words:
23
+ * the tier teaches `children` only, and a `body` child list under a
24
+ * non-container draws the same `not-a-container` the `children` spelling draws.
25
+ *
26
+ * SCOPE — asked INSIDE the `!input` branch, because its claim is about a key
27
+ * nobody declares, so a
28
+ * component that publishes its own `body` input keeps its declared type check
29
+ * untouched. Re-derived against the registry rather than recalled: exactly one
30
+ * registration in the tree declares an input named `body`, and it is
31
+ * `record:alert` (`plugin-detail`, `register('alert', …, { namespace: 'record' })`),
32
+ * whose `body` takes an inline translation map and is not this spelling.
33
+ * Control on the same sweep: `name: 'children'` fires seven times.
34
+ */
35
+ import type { Diagnostic } from './types.js';
36
+ /** The child-list spelling objectui#6771 retired. `children` replaced it. */
37
+ export declare const RETIRED_CHILD_LIST_KEY = "body";
38
+ /**
39
+ * The diagnostic for an authored `body`, or `null` when this module has nothing
40
+ * to say about the key.
41
+ *
42
+ * @param tag the node's component type, for the message and the `tag` field
43
+ * @param key the authored prop name
44
+ * @param value its authored value
45
+ * @param acceptsChildren whether the component declares the `children` slot
46
+ * input — `acceptsChildren(comp)` in `./validate.ts`,
47
+ * the ONE containment predicate this tier has. It used
48
+ * to be `comp.isContainer`; objectui#9910 moved both
49
+ * sites together so the retired spelling and the live
50
+ * one can never disagree about who takes a list.
51
+ */
52
+ export declare function checkRetiredBodyDialect(tag: string, key: string, value: unknown, acceptsChildren: boolean): Diagnostic | null;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * ObjectUI — the retired `body` child-list dialect (objectui#6771)
3
+ *
4
+ * `body` was a declared twin of `children` on `BaseSchema`, and for a long
5
+ * while it was the ONLY child-list key a dozen registrations read. This tier
6
+ * never knew it: `BASE_PROPS` lists `children` and not `body`, so the one
7
+ * spelling that resolved drew `unknown-prop` — the same warning a typo draws,
8
+ * on the tier built to accept AI-authored pages, where the diagnostic IS the
9
+ * contract. The maintainer ruled the retirement (2026-09-01, option B): one
10
+ * concept, one spelling, and the spelling is `children`.
11
+ *
12
+ * This module is the tier's half of that ruling. It REPLACES the diagnostic the
13
+ * prop walk would otherwise emit for `body`, rather than adding to it — two
14
+ * diagnostics for one mistake is the shape `checkMemberTypes` already refuses
15
+ * (objectui#8067).
16
+ *
17
+ * WHY A REPLACEMENT AND NOT A BARE `unknown-prop`. After the retirement
18
+ * "`<badge>` has no prop body" is TRUE — but it is the answer that sent authors
19
+ * the wrong way in the first place. Every `body` node in this repository's own
20
+ * docs, examples and READMEs was migrated in the same change, and an author
21
+ * carrying the old spelling in from anywhere else needs the replacement NAMED,
22
+ * not merely the key refused. The two codes below are the ruling's own words:
23
+ * the tier teaches `children` only, and a `body` child list under a
24
+ * non-container draws the same `not-a-container` the `children` spelling draws.
25
+ *
26
+ * SCOPE — asked INSIDE the `!input` branch, because its claim is about a key
27
+ * nobody declares, so a
28
+ * component that publishes its own `body` input keeps its declared type check
29
+ * untouched. Re-derived against the registry rather than recalled: exactly one
30
+ * registration in the tree declares an input named `body`, and it is
31
+ * `record:alert` (`plugin-detail`, `register('alert', …, { namespace: 'record' })`),
32
+ * whose `body` takes an inline translation map and is not this spelling.
33
+ * Control on the same sweep: `name: 'children'` fires seven times.
34
+ */
35
+ /** The child-list spelling objectui#6771 retired. `children` replaced it. */
36
+ export const RETIRED_CHILD_LIST_KEY = 'body';
37
+ const isPlainObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
38
+ /**
39
+ * Is this value shaped like the child list the dialect used to carry?
40
+ *
41
+ * A list of nodes, or the bare single node `FlatContent` and `renderChildren`
42
+ * have always accepted. A scalar is not — `body="hi"` is a dead key and nothing
43
+ * more, so it does not get the containment sentence, which would name a shape
44
+ * its author did not write.
45
+ */
46
+ const isChildList = (value) => Array.isArray(value) || isPlainObject(value);
47
+ /**
48
+ * The diagnostic for an authored `body`, or `null` when this module has nothing
49
+ * to say about the key.
50
+ *
51
+ * @param tag the node's component type, for the message and the `tag` field
52
+ * @param key the authored prop name
53
+ * @param value its authored value
54
+ * @param acceptsChildren whether the component declares the `children` slot
55
+ * input — `acceptsChildren(comp)` in `./validate.ts`,
56
+ * the ONE containment predicate this tier has. It used
57
+ * to be `comp.isContainer`; objectui#9910 moved both
58
+ * sites together so the retired spelling and the live
59
+ * one can never disagree about who takes a list.
60
+ */
61
+ export function checkRetiredBodyDialect(tag, key, value, acceptsChildren) {
62
+ if (key !== RETIRED_CHILD_LIST_KEY)
63
+ return null;
64
+ if (isChildList(value) && !acceptsChildren) {
65
+ // The ruling, verbatim: "a `body` child list under a non-container draws
66
+ // the same `not-a-container` the `children` spelling draws". Same code, so
67
+ // a consumer keying on the code cannot tell the two spellings apart — which
68
+ // is the single-spelling rule stated in the diagnostic stream. The sentence
69
+ // carries the extra fact the `children` path has no need of.
70
+ return {
71
+ severity: 'warning',
72
+ code: 'not-a-container',
73
+ message: `<${tag}> does not accept children — and "body" is the retired spelling of ` +
74
+ `"children" (objectui#6771), so this child list has no door here at all`,
75
+ tag,
76
+ };
77
+ }
78
+ return {
79
+ severity: 'warning',
80
+ code: 'unknown-prop',
81
+ message: `<${tag}> has no prop "body" — the child-list key is "children" ` +
82
+ `("body" was retired by objectui#6771)`,
83
+ tag,
84
+ };
85
+ }
package/dist/codegen.d.ts CHANGED
@@ -19,5 +19,10 @@ export declare function propsName(type: string): string;
19
19
  /**
20
20
  * Generate the human-facing PUBLIC block list (the curated "清单") from a
21
21
  * manifest — a Markdown table. Derived, never hand-maintained (ADR-0046).
22
+ *
23
+ * The html tier's intrinsic elements (`tier: 'html'`, objectui#10735) are
24
+ * listed in a section of their own, under their own count: they are in the
25
+ * manifest because a `kind:'html'` page may author them, not because they are
26
+ * curated blocks, and a single table would read them as the latter.
22
27
  */
23
28
  export declare function generateBlockList(manifest: Manifest): string;
package/dist/codegen.js CHANGED
@@ -8,6 +8,23 @@
8
8
  * fulfills it via the registry at render time.
9
9
  */
10
10
  import { inputTypeArms } from './input-type.js';
11
+ import { SDUI_BASE_PROPS } from './validate.js';
12
+ /**
13
+ * `SduiBaseProps`, emitted from {@link SDUI_BASE_PROPS} — the list
14
+ * `validateTree` reads — so the generated types and the validator cannot
15
+ * disagree about a base prop (objectui#11044). ⛔ Never a second hand-kept
16
+ * copy of the list here: that is how this interface came to refuse `bind` and
17
+ * `hidden` after the validator accepted them.
18
+ */
19
+ const emitBaseProps = () => `export interface SduiBaseProps {\n${SDUI_BASE_PROPS.filter((prop) => prop.tsType !== null)
20
+ .map((prop) => ` ${prop.name}?: ${prop.tsType};`)
21
+ .join('\n')}\n}`;
22
+ /**
23
+ * The attributes `SduiBaseProps` carries — every entry of the list but `type`,
24
+ * whatever its scope. A declared input of the same name outranks each of them
25
+ * in the generated types (objectui#11044, objectui#11075).
26
+ */
27
+ const BASE_ATTRIBUTES = new Set(SDUI_BASE_PROPS.filter((prop) => prop.tsType !== null).map((prop) => prop.name));
11
28
  export function generateDts(manifest, options = {}) {
12
29
  const { standaloneJsx = true } = options;
13
30
  const comps = Object.values(manifest.components).sort((a, b) => a.type.localeCompare(b.type));
@@ -27,16 +44,7 @@ export function generateDts(manifest, options = {}) {
27
44
  // Source of truth: ComponentRegistry inputs (ADR-0080 §3). Regenerate via codegen.
28
45
  /* eslint-disable */
29
46
 
30
- export interface SduiBaseProps {
31
- id?: string;
32
- className?: string;
33
- style?: Record<string, unknown>;
34
- visible?: boolean;
35
- visibleOn?: string;
36
- disabled?: boolean;
37
- disabledOn?: string;
38
- children?: unknown;
39
- }
47
+ ${emitBaseProps()}
40
48
 
41
49
  ${interfaces}
42
50
 
@@ -58,13 +66,31 @@ export {};
58
66
  * those arms only (objectui#3832): the old test was `i.type !== 'slot'`, which a
59
67
  * union like `['slot', 'string']` would have passed while `tsType` fell through
60
68
  * to the default and typed it `string` anyway.
69
+ *
70
+ * A base attribute the component declares is `Omit`ted from the base it
71
+ * extends, whatever its scope: the declared input wins with its type. So a
72
+ * declared type the base one does not admit is no TS2430 conflict — neither
73
+ * `label?: string | Record<string, unknown>` on a `'where-undeclared'` member
74
+ * (objectui#11044) nor `record:alert`'s
75
+ * `visible?: boolean | string | Record<string, unknown>` on an `'every-node'`
76
+ * one (objectui#11075) — and one the base admits (`className?: string`) types
77
+ * the same either way.
78
+ *
79
+ * The scope is the VALIDATOR's distinction — `validateTree` skips an
80
+ * `'every-node'` key before the declared-input lookup — and does not reach this
81
+ * surface. ⛔ Every declared base attribute is `Omit`ted, not only the ones
82
+ * whose types conflict: assignability is TypeScript's judgement, and a
83
+ * conflicts-only rule here would re-derive it from type strings.
84
+ *
85
+ * A slot-only input emits no attribute, so a declared `children: 'slot'` is
86
+ * not `Omit`ted and the base `children` stays.
61
87
  */
62
88
  function emitInterface(comp) {
63
- const lines = comp.inputs
64
- .filter((i) => valueArms(i).length > 0)
65
- .map((i) => ` ${propLine(i)}`)
66
- .join('\n');
67
- return `export interface ${propsName(comp.type)} extends SduiBaseProps {\n${lines}\n}`;
89
+ const emitted = comp.inputs.filter((i) => valueArms(i).length > 0);
90
+ const lines = emitted.map((i) => ` ${propLine(i)}`).join('\n');
91
+ const yielded = emitted.filter((i) => BASE_ATTRIBUTES.has(i.name)).map((i) => JSON.stringify(i.name));
92
+ const base = yielded.length > 0 ? `Omit<SduiBaseProps, ${yielded.join(' | ')}>` : 'SduiBaseProps';
93
+ return `export interface ${propsName(comp.type)} extends ${base} {\n${lines}\n}`;
68
94
  }
69
95
  /** The arms that describe a VALUE (every arm except `'slot'`). */
70
96
  const valueArms = (input) => inputTypeArms(input.type).filter((arm) => arm !== 'slot');
@@ -85,9 +111,9 @@ function armTsType(arm, input) {
85
111
  case 'boolean':
86
112
  return 'boolean';
87
113
  case 'array':
88
- return 'unknown[]';
114
+ return `${memberTsType(input)}[]`;
89
115
  case 'object':
90
- return 'Record<string, unknown>';
116
+ return `Record<string, ${memberTsType(input)}>`;
91
117
  case 'enum': {
92
118
  const vals = (input.enum ?? []).map((e) => (typeof e === 'object' ? e.value : e));
93
119
  return vals.length ? vals.map((v) => JSON.stringify(v)).join(' | ') : 'string';
@@ -96,6 +122,29 @@ function armTsType(arm, input) {
96
122
  return 'string';
97
123
  }
98
124
  }
125
+ /**
126
+ * The TypeScript type of a container's MEMBERS, from `ManifestInput.of`
127
+ * (objectui#8067).
128
+ *
129
+ * `unknown` when nothing is declared, which is what every input published
130
+ * before `of` existed says — so an undeclared array still emits `unknown[]` and
131
+ * an undeclared object still emits `Record<string, unknown>`, byte for byte.
132
+ * A declared member kind narrows it: `of: 'string'` on `page:header.actions`
133
+ * turns `unknown[]` into `string[]`, and the JSX surface finally types the
134
+ * action IDs the contract has always required.
135
+ *
136
+ * `'slot'` is dropped for the same reason {@link valueArms} drops it one level
137
+ * up: it names a child position, not a value, so it contributes no member type.
138
+ * If that leaves nothing, the result is `unknown` — a declaration that types no
139
+ * member must not silently type them all as strings.
140
+ */
141
+ function memberTsType(input) {
142
+ const arms = inputTypeArms(input.of).filter((arm) => arm !== 'slot');
143
+ if (arms.length === 0)
144
+ return 'unknown';
145
+ const emitted = [...new Set(arms.map((arm) => armTsType(arm, { ...input, of: undefined })))];
146
+ return emitted.length === 1 ? emitted[0] : `(${emitted.join(' | ')})`;
147
+ }
99
148
  /**
100
149
  * A union declaration emits a TypeScript union, so the `.d.ts` an author
101
150
  * type-checks their page against accepts exactly the arms the manifest gate
@@ -123,23 +172,40 @@ export function propsName(type) {
123
172
  /**
124
173
  * Generate the human-facing PUBLIC block list (the curated "清单") from a
125
174
  * manifest — a Markdown table. Derived, never hand-maintained (ADR-0046).
175
+ *
176
+ * The html tier's intrinsic elements (`tier: 'html'`, objectui#10735) are
177
+ * listed in a section of their own, under their own count: they are in the
178
+ * manifest because a `kind:'html'` page may author them, not because they are
179
+ * curated blocks, and a single table would read them as the latter.
126
180
  */
127
181
  export function generateBlockList(manifest) {
128
- const rows = Object.values(manifest.components)
129
- .sort((a, b) => a.type.localeCompare(b.type))
130
- .map((c) => {
182
+ const sorted = Object.values(manifest.components).sort((a, b) => a.type.localeCompare(b.type));
183
+ const curated = sorted.filter((c) => c.tier !== 'html');
184
+ const htmlTier = sorted.filter((c) => c.tier === 'html');
185
+ const row = (c) => {
131
186
  const req = c.inputs.filter((i) => i.required).map((i) => i.name);
132
187
  const binds = c.inputs.filter((i) => i.binding).map((i) => `${i.name}:${i.binding}`);
133
188
  return `| \`${c.type}\` | ${c.namespace ?? '—'} | ${c.isContainer ? '✓' : ''} | ${req.join(', ') || '—'} | ${binds.join(', ') || '—'} |`;
134
- });
189
+ };
190
+ const header = ['| block | plugin | container | required props | bindings |', '|---|---|---|---|---|'];
135
191
  return [
136
- `# SDUI public blocks (${Object.keys(manifest.components).length})`,
192
+ `# SDUI public blocks (${curated.length})`,
137
193
  '',
138
194
  '> Auto-generated from the registry `tier:\'public\'` set (ADR-0080). Do not edit by hand.',
139
195
  '',
140
- '| block | plugin | container | required props | bindings |',
141
- '|---|---|---|---|---|',
142
- ...rows,
196
+ ...header,
197
+ ...curated.map(row),
198
+ ...(htmlTier.length
199
+ ? [
200
+ '',
201
+ `## html tier intrinsic elements (${htmlTier.length})`,
202
+ '',
203
+ "> The intrinsic HTML tags a `kind:'html'` page may author (ADR-0081 §2), declared by the registry's `HTML_TIER_INTRINSICS` roster and marked `tier: 'html'` in the manifest. Not part of the curated vocabulary above.",
204
+ '',
205
+ ...header,
206
+ ...htmlTier.map(row),
207
+ ]
208
+ : []),
143
209
  '',
144
210
  ].join('\n');
145
211
  }
@@ -1,19 +1,21 @@
1
1
  /**
2
- * ObjectUI — unconsumed dashboard-widget `options` keys (objectui#5709)
2
+ * ObjectUI — unconsumed dashboard-widget `options` keys (`8d58f46b4`)
3
3
  *
4
4
  * `@objectstack/spec`'s `DashboardWidgetOptionsSchema` ends in `.passthrough()`
5
5
  * ("declared query keys + open renderer extras"), so ANY key parses, validates
6
6
  * and lints cleanly — including one no renderer reads. That is how a showcase
7
7
  * dashboard shipped `options: { invert: true }` on a gauge with a comment
8
8
  * saying what it was believed to do, and rendered the un-inverted measure with
9
- * no diagnostic anywhere (objectui#5709). The 2026-08-23 maintainer ruling:
9
+ * no diagnostic anywhere (`8d58f46b4`). The 2026-08-23 maintainer ruling:
10
10
  * open extras stay open — they just stop being SILENT. A key that reaches no
11
11
  * renderer draws a WARNING naming the consumed set. Not an error: no gate
12
12
  * weakening and no new red gates were ruled.
13
13
  *
14
14
  * ## The census (measured on origin/main @ 8689166f6, spec 17.1.0)
15
15
  *
16
- * The spec REQUIRES `dataset` on every widget (`DashboardWidgetSchema`), and
16
+ * The spec REQUIRES `dataset` on every widget (`DashboardWidgetSchema` —
17
+ * re-read for objectui#7293 against the PUBLISHED `@objectstack/spec@17.4.0`,
18
+ * whose required keys are exactly `id` / `dataset` / `values`), and
17
19
  * both dashboard surfaces route a dataset-bound widget to `DatasetWidget`
18
20
  * (`DashboardRenderer.tsx` renders `<DatasetWidget>` when `widget.dataset` is
19
21
  * set; `DashboardGridLayout.tsx` mirrors the fork, objectui#4614). On that —
@@ -24,22 +26,48 @@
24
26
  * dateGranularity, sortBy, sortOrder, limit (query-affecting, framework#3588)
25
27
  * stageOrder (funnel/pyramid stage order)
26
28
  *
27
- * plus ONE undeclared key with a real read site:
29
+ * and nothing else. ⛔ Not `description`: it was the metric-card sub-caption
30
+ * (objectui#4032 item 4, objectui#7293), read by `DatasetWidget.tsx`'s metric
31
+ * branch and a resolver both dashboard surfaces called, and fed by the
32
+ * server's `translateDashboard` overlay of the `widgets.{id}.subCaption`
33
+ * translation. The spec never declared it. objectui#11389 (ruling C, which
34
+ * reverses objectstack#5428 item 4) retires it at both ends: objectstack
35
+ * first, in `@objectstack/spec` 17.7.0 (the overlay is gone and the
36
+ * translation key is a tombstone), then the readers here. So an authored
37
+ * `options.description` reaches no renderer and draws this warning like any
38
+ * other unconsumed key. A widget keeps one authored description,
39
+ * `widget.description`, the card-header subtitle, which is not an `options`
40
+ * key and is not judged here.
28
41
  *
29
- * description — the metric-card sub-caption channel. Read at
30
- * `DashboardRenderer.tsx` (`(widget.options as …)?.description`,
31
- * objectui#4032 item 4); the server's `translateDashboard` OVERLAYS the
32
- * `widgets.{id}.subCaption` translation onto this key (objectstack#8056,
33
- * objectstack#5428 item-4: 「两个作者字段两个 key」). Warning on a key the
34
- * platform's own translation pipeline writes would be a false positive on
35
- * legal metadata, so it is in the accepted set even though the dataset-bound
36
- * render path does not currently display it.
42
+ * Notably NOT consumed on the path a widget really renders through:
43
+ * `thresholds` and `format`. Both were widely believed to work; both draw this
44
+ * warning, which is the point. They are NOT the same kind of claim, and since
45
+ * the 2026-08-25 maintainer ruling (batch adjudication close-out, decision B1 —
46
+ * a closure claim must be BOUNDED or DERIVABLE, and a claim written in two
47
+ * places is single-sourced) they are written separately (objectui#6186):
37
48
  *
38
- * Notably NOT consumed anywhere in this repository: `thresholds` (zero read
39
- * sites repo-wide) and `format` — the dataset-bound value is formatted with
40
- * the MEASURE's own metadata (`measureField(...).format`, from the dataset
41
- * definition), not with `options.format`. Both were widely believed to work;
42
- * both draw this warning, which is the point.
49
+ * `thresholds` — zero read sites repo-wide. This paragraph is the CANONICAL
50
+ * statement of that closure claim. `content/docs/plugins/plugin-dashboard.mdx`
51
+ * used to assert it a second time in its own words; two copies of one
52
+ * closure claim drift apart independently and neither knows when the other
53
+ * stopped being true, so the page now POINTS here rather than restating it.
54
+ * Single-sourcing is what makes this copy load-bearing, so it is DERIVED
55
+ * rather than trusted:
56
+ * `scripts/__tests__/unconsumed-widget-option-claim-6186.test.ts` re-scans
57
+ * every JS/TS file git tracks for an access to a key of that name and fails
58
+ * if one appears. Land a renderer that reads it and this module goes red in
59
+ * the same run.
60
+ * `format` — not read ON THE DATASET-BOUND PATH; the value is formatted with
61
+ * the MEASURE's own metadata (`measureField(...).format`, from the dataset
62
+ * definition), not from the widget's bag. Deliberately a BOUNDED claim and
63
+ * not a repo-wide one: `format` is a live key in other vocabularies, so a
64
+ * repo-wide scan would red on a TRUE claim — and an assertion that reds on
65
+ * legitimate code gets deleted by the next person who hits it, which puts
66
+ * the claim back where it started. Leg 2 of
67
+ * `__tests__/dashboard-widget-options-census.test.ts` derives exactly this
68
+ * bound: the DatasetWidget read set is the declared set, and neither
69
+ * `format` nor `thresholds` is in it. That absence is asserted in its OWN
70
+ * right rather than riding on an equality whose right-hand side can grow.
43
71
  *
44
72
  * ## Scope — where the warning deliberately does NOT fire
45
73
  *
@@ -65,7 +93,8 @@
65
93
  * `@objectstack/spec`, re-extracts the `options.<key>` reads from
66
94
  * `DatasetWidget.tsx` source text (and fails loudly if that file gains a
67
95
  * consumption shape the extractor cannot see — a spread, a destructuring, a
68
- * computed access), re-checks the sub-caption read site, and trips on any NEW
96
+ * computed access), re-checks that the retired sub-caption key has no read
97
+ * site left in `plugin-dashboard`, and trips on any NEW
69
98
  * file in `packages/*\/src` or `apps/*\/src` that starts reading
70
99
  * `widget.options`. A renderer change that adds or removes a consumed key
71
100
  * fails that test until this list is updated — the cost of keeping this
@@ -82,8 +111,9 @@ export declare const UNCONSUMED_WIDGET_OPTION = "unconsumed-widget-option";
82
111
  export declare const DASHBOARD_WIDGET_HOST_TYPES: ReadonlySet<string>;
83
112
  /**
84
113
  * The accepted set: every `options` key with a renderer read site on the
85
- * dataset-bound path, plus the sub-caption convention key. Alphabetical; the
86
- * warning message prints it verbatim. Derivation and evidence: file header.
114
+ * dataset-bound path, which is exactly the set the spec declares.
115
+ * Alphabetical; the warning message prints it verbatim. Derivation and
116
+ * evidence: file header.
87
117
  */
88
118
  export declare const CONSUMED_WIDGET_OPTION_KEYS: readonly string[];
89
119
  /**
@@ -11,12 +11,12 @@ export const DASHBOARD_WIDGET_HOST_TYPES = new Set([
11
11
  ]);
12
12
  /**
13
13
  * The accepted set: every `options` key with a renderer read site on the
14
- * dataset-bound path, plus the sub-caption convention key. Alphabetical; the
15
- * warning message prints it verbatim. Derivation and evidence: file header.
14
+ * dataset-bound path, which is exactly the set the spec declares.
15
+ * Alphabetical; the warning message prints it verbatim. Derivation and
16
+ * evidence: file header.
16
17
  */
17
18
  export const CONSUMED_WIDGET_OPTION_KEYS = [
18
19
  'dateGranularity',
19
- 'description',
20
20
  'limit',
21
21
  'sortBy',
22
22
  'sortOrder',
package/dist/index.d.ts CHANGED
@@ -8,11 +8,13 @@
8
8
  export * from './types.js';
9
9
  export { parseJsx, interpretBrace } from './parse.js';
10
10
  export { HTML_TIER_NODE, isHtmlTierNode, markHtmlTierNode } from './provenance.js';
11
- export { validateTree } from './validate.js';
11
+ export { validateTree, acceptsChildren, CHILD_LIST_KEY, SDUI_BASE_PROPS } from './validate.js';
12
+ export type { SduiBaseProp, SduiBasePropScope } from './validate.js';
12
13
  export { checkDashboardWidgetOptions, CONSUMED_WIDGET_OPTION_KEYS, DASHBOARD_WIDGET_HOST_TYPES, UNCONSUMED_WIDGET_OPTION, } from './dashboard-widget-options.js';
13
14
  export { generateDts, propsName, generateBlockList } from './codegen.js';
14
15
  export type { CodegenOptions } from './codegen.js';
15
16
  export { inputTypeArms, canonicalizeInputType, MANIFEST_INPUT_TYPES } from './input-type.js';
17
+ export { checkRetiredBodyDialect, RETIRED_CHILD_LIST_KEY } from './body-dialect.js';
16
18
  import type { Diagnostic, Manifest, SchemaElement, ManifestValidationResult } from './types.js';
17
19
  export interface CompileResult {
18
20
  tree: SchemaElement | null;
@@ -27,12 +29,34 @@ export interface CompileResult {
27
29
  * → validate against the manifest → derive `requires` + binding sites.
28
30
  */
29
31
  export declare function compile(source: string, manifest: Manifest): CompileResult;
32
+ /**
33
+ * The shape of one `@object-ui/types` `NodeSlotDeclaration`, restated
34
+ * structurally because this package imports nothing at runtime: a position in
35
+ * that package's key-path grammar, and whether its spelling is retired.
36
+ */
37
+ export interface NodeSlotDeclarationLike {
38
+ readonly path: string;
39
+ readonly retired?: boolean;
40
+ }
30
41
  export interface RegistryConfigLike {
31
42
  type: string;
32
43
  namespace?: string;
44
+ /**
45
+ * LAYOUT containment — the registration's `ComponentMeta.isContainer`,
46
+ * carried through to the manifest for the consumers that read it there.
47
+ * ⛔ It does not mean "accepts children" and `validateTree` does not read
48
+ * it: whether a component takes an authored child list is declared as an
49
+ * input named `children` (objectui#9910; `acceptsChildren` in `validate.ts`).
50
+ */
33
51
  isContainer?: boolean;
34
- /** ADR-0080 contract tier — only 'public' configs form the AI/contract surface. */
35
- tier?: 'public' | 'internal';
52
+ /**
53
+ * ADR-0080 contract tier — only 'public' configs form the AI/contract
54
+ * surface under `publicOnly`. `'html'` is the projection-only stamp
55
+ * `ComponentRegistry.getPublicConfigs()` puts on the html tier's intrinsic
56
+ * elements (objectui#10735); it is the one value {@link manifestFromConfigs}
57
+ * carries into `ManifestComponent.tier`.
58
+ */
59
+ tier?: 'public' | 'internal' | 'html';
36
60
  label?: string;
37
61
  category?: string;
38
62
  inputs?: Array<{
@@ -45,12 +69,44 @@ export interface RegistryConfigLike {
45
69
  * `canonicalizeInputType` on the way in.
46
70
  */
47
71
  type: string | string[];
72
+ /**
73
+ * The declared member kind(s) — array elements, or an object map's values
74
+ * (objectui#8067). Typed as loosely as `type` above and for the same
75
+ * reason: this interface is the STRUCTURAL boundary that keeps the package
76
+ * free of a registry dependency, so an off-vocabulary value has to be
77
+ * representable here and is normalized on the way in.
78
+ */
79
+ of?: string | string[];
48
80
  required?: boolean;
49
81
  enum?: Array<string | {
50
82
  value: unknown;
51
83
  label?: string;
52
84
  }>;
53
- binding?: 'object' | 'field';
85
+ /**
86
+ * The binding marker — exactly `'object'` (`9e37d9b39`).
87
+ *
88
+ * `'field'` was declared beside it from the first draft of ADR-0080 §6.3
89
+ * and never written: every `binding:` literal in `packages/`, `apps/`
90
+ * and `examples/` is `'object'` (7 of 7, measured 2026-09-06 and
91
+ * re-measured at the retiring PR's merge-base), and nothing on either
92
+ * side of the manifest resolved a field binding. Retired under ADR-0049
93
+ * enforce-or-remove by the maintainer ruling of 2026-09-07 (decision
94
+ * batch #69): a config that spells the arm is a `tsc` error here rather
95
+ * than an entry the server would never resolve.
96
+ *
97
+ * The retirement then reached the two faces the ruling did not name
98
+ * (objectui#8315): `ManifestInput.binding` and
99
+ * `ManifestValidationResult.bindings[].kind` in `types.ts` read `'object'`
100
+ * too, so the published package no longer narrows on one face and stays
101
+ * wide on the other. The argument for keeping the manifest face wide —
102
+ * producer → reader is a subset relation — is answered at
103
+ * `ManifestInput.binding`, which is where a later reader lands.
104
+ *
105
+ * Only the framework writes the key: `InjectedComponentInput` in
106
+ * `@object-ui/types`, spliced by `Registry.register`. `ComponentInput`
107
+ * itself has no `binding` member, deliberately.
108
+ */
109
+ binding?: 'object';
54
110
  description?: string;
55
111
  }>;
56
112
  /**
@@ -92,8 +148,21 @@ export declare function assertFullyLoaded(configs: RegistryConfigLike[]): void;
92
148
  * no renderer read and sailed through it green (objectstack#4413; corrected in
93
149
  * objectstack#4472). Evidence about the render path has to come from the render
94
150
  * path — see `apps/console/src/__tests__/public-block-binding-reach.test.tsx`.
151
+ *
152
+ * `opts.slotsFor` is the one exception to "copied from the registration", and
153
+ * it is a DECLARATION too (objectui#11170): hand it `nodeSlotsFor` from
154
+ * `@object-ui/types` and every entry carries `slots`, the node-slot positions
155
+ * that type's renderer reads besides `children`, so `validateTree` judges the
156
+ * nodes under them. This package has no runtime dependency and so cannot
157
+ * import the declaration itself; the producers that build a live manifest
158
+ * (`getJsxManifest` in `@object-ui/components`' page renderer, the console's
159
+ * manifest generator) pass it. A RETIRED position is left out here: this is
160
+ * the authoring tier, and the spec's own authoring walks skip the tombstoned
161
+ * spellings for the same reason. Without the option no `slots` key is written,
162
+ * so every manifest built before the option existed serialises byte-identically.
95
163
  */
96
164
  export declare function manifestFromConfigs(configs: RegistryConfigLike[], opts?: {
97
165
  only?: Set<string>;
98
166
  publicOnly?: boolean;
167
+ slotsFor?: (type: string) => readonly NodeSlotDeclarationLike[];
99
168
  }): Manifest;
package/dist/index.js CHANGED
@@ -8,10 +8,11 @@
8
8
  export * from './types.js';
9
9
  export { parseJsx, interpretBrace } from './parse.js';
10
10
  export { HTML_TIER_NODE, isHtmlTierNode, markHtmlTierNode } from './provenance.js';
11
- export { validateTree } from './validate.js';
11
+ export { validateTree, acceptsChildren, CHILD_LIST_KEY, SDUI_BASE_PROPS } from './validate.js';
12
12
  export { checkDashboardWidgetOptions, CONSUMED_WIDGET_OPTION_KEYS, DASHBOARD_WIDGET_HOST_TYPES, UNCONSUMED_WIDGET_OPTION, } from './dashboard-widget-options.js';
13
13
  export { generateDts, propsName, generateBlockList } from './codegen.js';
14
14
  export { inputTypeArms, canonicalizeInputType, MANIFEST_INPUT_TYPES } from './input-type.js';
15
+ export { checkRetiredBodyDialect, RETIRED_CHILD_LIST_KEY } from './body-dialect.js';
15
16
  import { parseJsx } from './parse.js';
16
17
  import { validateTree } from './validate.js';
17
18
  import { canonicalizeInputType } from './input-type.js';
@@ -76,6 +77,18 @@ export function assertFullyLoaded(configs) {
76
77
  * no renderer read and sailed through it green (objectstack#4413; corrected in
77
78
  * objectstack#4472). Evidence about the render path has to come from the render
78
79
  * path — see `apps/console/src/__tests__/public-block-binding-reach.test.tsx`.
80
+ *
81
+ * `opts.slotsFor` is the one exception to "copied from the registration", and
82
+ * it is a DECLARATION too (objectui#11170): hand it `nodeSlotsFor` from
83
+ * `@object-ui/types` and every entry carries `slots`, the node-slot positions
84
+ * that type's renderer reads besides `children`, so `validateTree` judges the
85
+ * nodes under them. This package has no runtime dependency and so cannot
86
+ * import the declaration itself; the producers that build a live manifest
87
+ * (`getJsxManifest` in `@object-ui/components`' page renderer, the console's
88
+ * manifest generator) pass it. A RETIRED position is left out here: this is
89
+ * the authoring tier, and the spec's own authoring walks skip the tombstoned
90
+ * spellings for the same reason. Without the option no `slots` key is written,
91
+ * so every manifest built before the option existed serialises byte-identically.
79
92
  */
80
93
  export function manifestFromConfigs(configs, opts = {}) {
81
94
  const components = {};
@@ -84,13 +97,33 @@ export function manifestFromConfigs(configs, opts = {}) {
84
97
  continue;
85
98
  if (opts.publicOnly && c.tier !== 'public')
86
99
  continue;
100
+ const slots = opts.slotsFor
101
+ ? opts.slotsFor(c.type).filter((s) => !s.retired).map((s) => s.path)
102
+ : undefined;
87
103
  components[c.type] = {
88
104
  type: c.type,
89
105
  namespace: c.namespace,
90
106
  isContainer: c.isContainer,
107
+ // `undefined` is dropped by `JSON.stringify`, so a manifest built with
108
+ // no resolver — or an entry with no slots — serialises exactly as before
109
+ // this key existed.
110
+ slots: slots && slots.length > 0 ? slots : undefined,
111
+ // The html tier's stamp, and ONLY that stamp (objectui#10735): a
112
+ // registration's `'public'` / `'internal'` is registry mechanics the
113
+ // manifest never carried, and `undefined` is dropped by `JSON.stringify`,
114
+ // so every curated entry serialises exactly as before this key existed.
115
+ tier: c.tier === 'html' ? 'html' : undefined,
91
116
  inputs: (c.inputs ?? []).map((i) => ({
92
117
  name: i.name,
93
118
  type: canonicalizeInputType(i.type),
119
+ // Undefined stays undefined rather than going through
120
+ // `canonicalizeInputType`, whose no-arms fallback is `'string'`: an
121
+ // input that declares no member kind must publish NO `of`, or every
122
+ // array in every manifest would suddenly claim string members it was
123
+ // never told it had. `JSON.stringify` drops the undefined key, so the
124
+ // published artifact is byte-identical for every input that does not
125
+ // declare one (objectui#8067).
126
+ of: i.of === undefined ? undefined : canonicalizeInputType(i.of),
94
127
  required: i.required,
95
128
  enum: i.enum,
96
129
  binding: i.binding,