@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.
- package/dist/body-dialect.d.ts +52 -0
- package/dist/body-dialect.js +85 -0
- package/dist/codegen.d.ts +5 -0
- package/dist/codegen.js +91 -25
- package/dist/dashboard-widget-options.d.ts +49 -15
- package/dist/index.d.ts +51 -4
- package/dist/index.js +15 -1
- package/dist/parse.d.ts +12 -3
- package/dist/parse.js +324 -6
- package/dist/provenance.d.ts +7 -4
- package/dist/provenance.js +7 -4
- package/dist/types.d.ts +102 -4
- package/dist/validate.d.ts +111 -4
- package/dist/validate.js +246 -20
- package/package.json +5 -5
|
@@ -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
|
-
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return `export interface ${propsName(comp.type)} extends
|
|
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
|
|
114
|
+
return `${memberTsType(input)}[]`;
|
|
89
115
|
case 'object':
|
|
90
|
-
return
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
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 (${
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
...
|
|
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 (
|
|
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 (
|
|
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`
|
|
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
|
-
* `
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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
|
-
/**
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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;
|