@object-ui/sdui-parser 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.
@@ -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 —
@@ -27,19 +29,51 @@
27
29
  * plus ONE undeclared key with a real read site:
28
30
  *
29
31
  * 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.
32
+ * `widgetSubCaption.ts` (`(widget.options as …)?.description`, objectui#4032
33
+ * item 4 — that read sat inline in `DashboardRenderer.tsx` until
34
+ * objectui#8889 moved it, verbatim, into the hook BOTH dashboard surfaces
35
+ * now call, so the authored and bundle channels compose at one decision
36
+ * point) and, since objectui#7293, at `DatasetWidget.tsx`'s
37
+ * metric branch, which renders it in the caption row; the server's
38
+ * `translateDashboard` OVERLAYS the `widgets.{id}.subCaption` translation
39
+ * onto this key (objectstack#8056, objectstack#5428 item-4: 「两个作者字段两个
40
+ * key」). It entered the accepted set on the translation-pipeline evidence
41
+ * alone — warning on a key the platform's own pipeline writes would be a
42
+ * false positive on legal metadata — while the dataset-bound render path
43
+ * displayed it nowhere. #7293 closed that gap, so `description` is now
44
+ * accepted for the same reason as the declared five and the accepted set no
45
+ * longer outruns the measured read set.
37
46
  *
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.
47
+ * Notably NOT consumed on the path a widget really renders through:
48
+ * `thresholds` and `format`. Both were widely believed to work; both draw this
49
+ * warning, which is the point. They are NOT the same kind of claim, and since
50
+ * the 2026-08-25 maintainer ruling (batch adjudication close-out, decision B1 —
51
+ * a closure claim must be BOUNDED or DERIVABLE, and a claim written in two
52
+ * places is single-sourced) they are written separately (objectui#6186):
53
+ *
54
+ * `thresholds` — zero read sites repo-wide. This paragraph is the CANONICAL
55
+ * statement of that closure claim. `content/docs/plugins/plugin-dashboard.mdx`
56
+ * used to assert it a second time in its own words; two copies of one
57
+ * closure claim drift apart independently and neither knows when the other
58
+ * stopped being true, so the page now POINTS here rather than restating it.
59
+ * Single-sourcing is what makes this copy load-bearing, so it is DERIVED
60
+ * rather than trusted:
61
+ * `scripts/__tests__/unconsumed-widget-option-claim-6186.test.ts` re-scans
62
+ * every JS/TS file git tracks for an access to a key of that name and fails
63
+ * if one appears. Land a renderer that reads it and this module goes red in
64
+ * the same run.
65
+ * `format` — not read ON THE DATASET-BOUND PATH; the value is formatted with
66
+ * the MEASURE's own metadata (`measureField(...).format`, from the dataset
67
+ * definition), not from the widget's bag. Deliberately a BOUNDED claim and
68
+ * not a repo-wide one: `format` is a live key in other vocabularies, so a
69
+ * repo-wide scan would red on a TRUE claim — and an assertion that reds on
70
+ * legitimate code gets deleted by the next person who hits it, which puts
71
+ * the claim back where it started. Leg 2 of
72
+ * `__tests__/dashboard-widget-options-census.test.ts` derives exactly this
73
+ * bound: the DatasetWidget read set is the declared set plus the
74
+ * sub-caption key, and neither `format` nor `thresholds` is in it. Since
75
+ * objectui#7293 that absence is asserted in its OWN right rather than
76
+ * riding on an equality whose right-hand side can grow.
43
77
  *
44
78
  * ## Scope — where the warning deliberately does NOT fire
45
79
  *
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;
@@ -30,9 +32,22 @@ export declare function compile(source: string, manifest: Manifest): CompileResu
30
32
  export interface RegistryConfigLike {
31
33
  type: string;
32
34
  namespace?: string;
35
+ /**
36
+ * LAYOUT containment — the registration's `ComponentMeta.isContainer`,
37
+ * carried through to the manifest for the consumers that read it there.
38
+ * ⛔ It does not mean "accepts children" and `validateTree` does not read
39
+ * it: whether a component takes an authored child list is declared as an
40
+ * input named `children` (objectui#9910; `acceptsChildren` in `validate.ts`).
41
+ */
33
42
  isContainer?: boolean;
34
- /** ADR-0080 contract tier — only 'public' configs form the AI/contract surface. */
35
- tier?: 'public' | 'internal';
43
+ /**
44
+ * ADR-0080 contract tier — only 'public' configs form the AI/contract
45
+ * surface under `publicOnly`. `'html'` is the projection-only stamp
46
+ * `ComponentRegistry.getPublicConfigs()` puts on the html tier's intrinsic
47
+ * elements (objectui#10735); it is the one value {@link manifestFromConfigs}
48
+ * carries into `ManifestComponent.tier`.
49
+ */
50
+ tier?: 'public' | 'internal' | 'html';
36
51
  label?: string;
37
52
  category?: string;
38
53
  inputs?: Array<{
@@ -45,12 +60,44 @@ export interface RegistryConfigLike {
45
60
  * `canonicalizeInputType` on the way in.
46
61
  */
47
62
  type: string | string[];
63
+ /**
64
+ * The declared member kind(s) — array elements, or an object map's values
65
+ * (objectui#8067). Typed as loosely as `type` above and for the same
66
+ * reason: this interface is the STRUCTURAL boundary that keeps the package
67
+ * free of a registry dependency, so an off-vocabulary value has to be
68
+ * representable here and is normalized on the way in.
69
+ */
70
+ of?: string | string[];
48
71
  required?: boolean;
49
72
  enum?: Array<string | {
50
73
  value: unknown;
51
74
  label?: string;
52
75
  }>;
53
- binding?: 'object' | 'field';
76
+ /**
77
+ * The binding marker — exactly `'object'` (`9e37d9b39`).
78
+ *
79
+ * `'field'` was declared beside it from the first draft of ADR-0080 §6.3
80
+ * and never written: every `binding:` literal in `packages/`, `apps/`
81
+ * and `examples/` is `'object'` (7 of 7, measured 2026-09-06 and
82
+ * re-measured at the retiring PR's merge-base), and nothing on either
83
+ * side of the manifest resolved a field binding. Retired under ADR-0049
84
+ * enforce-or-remove by the maintainer ruling of 2026-09-07 (decision
85
+ * batch #69): a config that spells the arm is a `tsc` error here rather
86
+ * than an entry the server would never resolve.
87
+ *
88
+ * The retirement then reached the two faces the ruling did not name
89
+ * (objectui#8315): `ManifestInput.binding` and
90
+ * `ManifestValidationResult.bindings[].kind` in `types.ts` read `'object'`
91
+ * too, so the published package no longer narrows on one face and stays
92
+ * wide on the other. The argument for keeping the manifest face wide —
93
+ * producer → reader is a subset relation — is answered at
94
+ * `ManifestInput.binding`, which is where a later reader lands.
95
+ *
96
+ * Only the framework writes the key: `InjectedComponentInput` in
97
+ * `@object-ui/types`, spliced by `Registry.register`. `ComponentInput`
98
+ * itself has no `binding` member, deliberately.
99
+ */
100
+ binding?: 'object';
54
101
  description?: string;
55
102
  }>;
56
103
  /**
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';
@@ -88,9 +89,22 @@ export function manifestFromConfigs(configs, opts = {}) {
88
89
  type: c.type,
89
90
  namespace: c.namespace,
90
91
  isContainer: c.isContainer,
92
+ // The html tier's stamp, and ONLY that stamp (objectui#10735): a
93
+ // registration's `'public'` / `'internal'` is registry mechanics the
94
+ // manifest never carried, and `undefined` is dropped by `JSON.stringify`,
95
+ // so every curated entry serialises exactly as before this key existed.
96
+ tier: c.tier === 'html' ? 'html' : undefined,
91
97
  inputs: (c.inputs ?? []).map((i) => ({
92
98
  name: i.name,
93
99
  type: canonicalizeInputType(i.type),
100
+ // Undefined stays undefined rather than going through
101
+ // `canonicalizeInputType`, whose no-arms fallback is `'string'`: an
102
+ // input that declares no member kind must publish NO `of`, or every
103
+ // array in every manifest would suddenly claim string members it was
104
+ // never told it had. `JSON.stringify` drops the undefined key, so the
105
+ // published artifact is byte-identical for every input that does not
106
+ // declare one (objectui#8067).
107
+ of: i.of === undefined ? undefined : canonicalizeInputType(i.of),
94
108
  required: i.required,
95
109
  enum: i.enum,
96
110
  binding: i.binding,
package/dist/parse.d.ts CHANGED
@@ -17,8 +17,17 @@ import type { ParseOptions, ParseResult } from './types.js';
17
17
  export declare function parseJsx(source: string, options?: ParseOptions): ParseResult;
18
18
  /**
19
19
  * Interpret a braced attribute value `{...}`.
20
- * JSON-literal values (numbers, booleans, null, strings, arrays, objects with
21
- * quoted keys) are materialized. Anything else is kept as a deferred expression
22
- * marker `{ $expr }` — typed and validated later, NEVER evaluated here.
20
+ *
21
+ * Strict-JSON values are materialized by `JSON.parse`, exactly as they always
22
+ * were. Beyond that, the JS **literal subset** below is materialized too
23
+ * (objectui#6614 Q1-A, maintainer ruling 2026-08-28). Anything left over — a
24
+ * genuine expression — is kept as the deferred marker `{ $expr }`: typed and
25
+ * validated later, drawing `inert-expression`, and NEVER evaluated here.
26
+ *
27
+ * ORDER IS LOAD-BEARING. `JSON.parse` runs FIRST and is untouched, so every
28
+ * input JSON accepts takes byte-identically the path it took before the literal
29
+ * subset existed. The reader below only ever sees strings `JSON.parse` has
30
+ * already thrown on, which makes strict-JSON invariance a property of the
31
+ * structure rather than of a test.
23
32
  */
24
33
  export declare function interpretBrace(raw: string): unknown;