@objectstack/sdui-parser 17.2.0 → 17.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,198 @@
1
1
  # @objectstack/sdui-parser
2
2
 
3
+ ## 17.4.0
4
+
5
+ ## 17.3.0
6
+
7
+ ### Minor Changes
8
+
9
+ - 2182bd1: sdui-parser: `interpretBrace` materializes the JS literal subset, in lockstep with objectui
10
+
11
+ The html tier's braced attribute values accepted strict JSON only, so the spelling every
12
+ JSX author and every AI author writes — `columns={['name','amount']}` — compiled to the
13
+ deferred `{ $expr }` marker that nothing downstream evaluates, and the author's data
14
+ binding vanished at render. Under the maintainer's ruling on objectui#6614 (Q1-A,
15
+ 2026-08-28) `interpretBrace` now materializes the JS **literal subset**: exactly two
16
+ widenings over JSON — single-quoted strings (value position and key position) and unquoted
17
+ identifier object keys.
18
+
19
+ Everything else JSON refuses is still refused and still becomes `{ $expr }`: trailing
20
+ commas, comments, array holes, spreads, `undefined` / `NaN` / `Infinity`, `+1` / `.5` /
21
+ `1.` / `0x1f`, template literals, and every genuine expression. `JSON.parse` still runs
22
+ first and untouched, so strict-JSON behaviour is invariant by construction, and the subset
23
+ contains no identifier lookup and no operator — the widening moves habitual spellings onto
24
+ the materialized side, it does not move the data/code boundary (ADR-0080: this tier parses,
25
+ never executes).
26
+
27
+ An authored `__proto__` key is written as an own data property, the way `JSON.parse` gives
28
+ it, never through the prototype setter — a plain assignment in the unquoted-key path would
29
+ hand untrusted page source a prototype-pollution lever the strict-JSON path never had.
30
+
31
+ The `inert-expression` diagnostic message is reworded to match: the old text advised
32
+ writing the value as JSON with double-quoted strings and keys, which now names a legal
33
+ spelling as the wrong one. Diagnostic **codes** are unchanged.
34
+ - 2a5c1cd: html tier: a braced attribute value that is not strict JSON now draws an `inert-expression` warning instead of vanishing silently
35
+
36
+ `interpretBrace` materializes strict-JSON values only; anything else — the
37
+ single-quoted array every JSX author writes (`columns={['name','amount']}`),
38
+ unquoted object keys, any JS expression — compiles to the deferred `{ $expr }`
39
+ marker, and nothing downstream evaluates that marker: this tier parses, never
40
+ executes (ADR-0080), and no renderer consumes `$expr`. The value reached the
41
+ renderer as an opaque object, defensive non-array/non-object reads degraded it
42
+ to "not declared", and the author's binding vanished with zero diagnostics
43
+ anywhere — a production page's `list-view` rendered its row count and toolbar
44
+ with no data columns, through eight `columns` spellings (objectui#6598). That
45
+ is ADR-0078's prohibited parsed-but-silently-inert state.
46
+
47
+ `validateTree` now emits a warning-severity `inert-expression` diagnostic when a
48
+ declared input's value is the `$expr` marker, with the fix in the message: write
49
+ the value as JSON (double-quoted strings and keys).
50
+
51
+ This is the lockstep port of objectui PR #6613 into this repo's hoisted copy of
52
+ the parser. There are two copies, and the invariant is that both agree on the
53
+ accepted grammar **and** on diagnostic codes — if they drift, the save gate and
54
+ the renderer speak different dialects, and a page can save clean and render
55
+ inert. The emitted diagnostic is byte-equal to objectui's.
56
+
57
+ Warning, not error, per the objectui#5709 posture for inert authored keys: this
58
+ reports an **already**-inert state, so the accept/reject set does not move.
59
+ Pages that compiled before still compile, and a warning is non-gating on every
60
+ consuming surface in this repo (`runtime-gate` files warnings as advisories, not
61
+ as write refusals; `os lint` exits non-zero on error-severity findings only).
62
+ The silence is what changed. Escalating the severity, widening the accepted
63
+ literal grammar (single-quoted strings, unquoted keys), and wiring the registry
64
+ manifest into `validate-jsx-pages` — without which this warning is recorded in
65
+ compile output but displayed by no production surface — are separate decisions
66
+ tracked on objectui#6614 and its follow-ups.
67
+ - 0e68ed2: html tier: an authored `type=` attribute is now refused at parse time instead of overwriting the component discriminator
68
+
69
+ On a `kind:'html'` page the tag name **is** the node's `type`, so a `type` attribute is a
70
+ name collision with the envelope's own discriminator. The parser now refuses it with one
71
+ `forbidden-attr` error naming **both** the tag and the attribute — *Attribute "type" is
72
+ not allowed on `<flex>` — on this tier the tag name IS the component…* — replacing two
73
+ outcomes, neither good:
74
+
75
+ - the value named another **registered** type (`<flex type="grid">`): the tree carried the
76
+ author's value as its discriminator, `validateTree` resolved `grid` in the manifest,
77
+ every check passed, and the page rendered a grid where the author wrote a flex — **zero
78
+ diagnostics**, on the one tier whose premise is that unreviewed, AI-authored source is
79
+ safe to accept;
80
+ - the value named **nothing** registered (`<object-chart type="bar">`, the shape a
81
+ react-tier author carries across): `unknown-component` naming `"bar"`, which reads as a
82
+ missing plugin rather than as an attribute that should not be there.
83
+
84
+ Alongside the refusal, `parseElement` builds the node as `{ ...props, type: tag }` rather
85
+ than `{ type: tag, ...props }` — defense in depth, and correct only *because* the
86
+ attribute is refused loudly: reversing the spread alone would trade a silent overwrite for
87
+ a silent discard.
88
+
89
+ The react tier is unaffected: its `specType` rescue (objectui#2880) stays where it lives
90
+ and is deliberately **not** carried over — the two tiers are two source formats, and a
91
+ consumer-side alias on a second tier is the tolerance ADR-0080's amendment declined.
92
+ `validate.ts`'s `BASE_PROPS` is unchanged (`type` is correct there for every other
93
+ member), and no warning grace period is introduced.
94
+
95
+ **This narrows what the html tier accepts**: a page that compiles today with a `type=`
96
+ attribute will be refused. The in-repo migration surface was measured before the change
97
+ and is **zero** — no html-tier page source under `content/docs/**` or the example apps
98
+ carries one. Maintainer ruling 2026-09-01, recorded as an amendment on ADR-0080.
99
+ - 8beb3de: html tier: a dashboard widget `options` key that reaches no renderer now draws an `unconsumed-widget-option` warning naming the consumed set
100
+
101
+ `@objectstack/spec`'s `DashboardWidgetOptionsSchema` ends in `.passthrough()`
102
+ ("declared query keys + open renderer extras"), so ANY key parses, validates
103
+ and lints cleanly — including one no renderer reads. That is how a dashboard
104
+ shipped `options: { invert: true }` on a gauge with a comment saying what it
105
+ was believed to do and rendered the un-inverted measure with no diagnostic
106
+ anywhere (objectui#5709). The 2026-08-23 maintainer ruling on that card: open
107
+ extras stay open — they just stop being **silent**. A key that reaches no
108
+ renderer draws a **warning** naming the consumed set.
109
+
110
+ objectui's copy of this parser has emitted that warning since the ruling
111
+ landed; this repo's hoisted copy emitted nothing, so the same authored page
112
+ produced a diagnostic on one surface and silence on the other — the dialect
113
+ split the two copies' invariant forbids (objectstack#12719 — both copies agree
114
+ on the accepted grammar **and** on diagnostic codes). `validateTree` now ends
115
+ its known-component branch with `checkDashboardWidgetOptions(node)`, and the
116
+ new module is a byte-equal port of objectui's save for one token (the emitted
117
+ `code` is spelled as an inline literal rather than through the exported
118
+ constant, so this repo's ADR-0112 vocabulary gate can classify it — called out
119
+ at the site, and pinned equal to the constant by test), so the emitted `code`,
120
+ `severity`, `message` and census scope are identical.
121
+
122
+ The warning is scoped to the only spec-legal render path: a `dashboard` /
123
+ `dashboard-grid` host, a widget with a `dataset`, not in the legacy
124
+ `component` format, and not carrying the spec's own
125
+ `suppressWarnings: ['unconsumed-widget-option']` escape hatch. The consumed set
126
+ is the five keys `DashboardWidgetOptionsSchema` declares (`dateGranularity`,
127
+ `sortBy`, `sortOrder`, `limit`, `stageOrder`) plus `description`, the metric
128
+ sub-caption channel `translateDashboard` writes into `options`.
129
+
130
+ New exports for third-party manifest consumers: `checkDashboardWidgetOptions`,
131
+ `CONSUMED_WIDGET_OPTION_KEYS`, `DASHBOARD_WIDGET_HOST_TYPES` and
132
+ `UNCONSUMED_WIDGET_OPTION` (the diagnostic code, which is also the id
133
+ `suppressWarnings` suppresses).
134
+
135
+ Unlike the union-arm port that preceded it, this change is **additive**: it
136
+ reports an already-inert state and emits `warning` only, so what this copy
137
+ accepts and rejects is exactly where it stood — pinned by a dedicated test.
138
+ Today it is latent in the production gate anyway: this repo resolves no
139
+ `sdui.manifest.json`, so `validateJsxPages` runs parse-only and `validateTree`
140
+ is not reached from it. Wiring that manifest (the second gap recorded on
141
+ objectstack#12719, still unowned) is what makes this author-visible, and this
142
+ port lands ahead of that wiring deliberately.
143
+ - 4a9f461: html tier: a union-typed manifest input is now coarse-type-checked over every declared arm instead of drawing no diagnostic at all
144
+
145
+ `ManifestInput.type` now carries ONE coarse kind, or an ARRAY of kinds when the
146
+ key's contract is a union (objectui#3832). Before this change, this copy's
147
+ `checkType` was the older single-arm `switch (input.type)`: a manifest input
148
+ declaring a union fell through `default: return null` and drew **no diagnostic
149
+ at all** — silence indistinguishable from a value that validated cleanly —
150
+ while objectui's copy checked every arm. The same authored page produced
151
+ diagnostics on one surface and none on the other: the dialect split the two
152
+ parser copies' invariant forbids (objectstack#12719 — both copies agree on the
153
+ accepted grammar **and** on diagnostic codes).
154
+
155
+ `validateTree`'s coarse check now clears a prop when **any** declared arm
156
+ accepts the value, and when **no** arm accepts it emits **one** `type-mismatch`
157
+ diagnostic naming every arm — at `error` severity when an `enum` arm is
158
+ present (an enum's closed list is the one fact this layer can be certain
159
+ about), `warning` otherwise. A single-arm input produces the byte-identical
160
+ diagnostic it always did, `invalid-enum` included. `generateDts` emits a
161
+ TypeScript union for a union declaration, and `manifestFromConfigs`
162
+ canonicalizes union declarations through the new `input-type.ts` module
163
+ (`inputTypeArms`, `canonicalizeInputType`, `MANIFEST_INPUT_TYPES` — all
164
+ exported, so third-party manifest consumers read arms through the same
165
+ accessor the gate does).
166
+
167
+ This is the lockstep port of the objectui#3832 ruling into this repo's hoisted
168
+ copy of the parser — the ported check is byte-equal to objectui's. It changes
169
+ what the save gate accepts and rejects for union-typed inputs: a value fitting
170
+ no arm of an enum-carrying union now draws an `error` where it previously drew
171
+ nothing. Today that change is latent in the production gate — this repo
172
+ resolves no `sdui.manifest.json`, so `validateJsxPages` runs parse-only; wiring
173
+ the manifest (the second gap recorded on objectstack#12719) is what makes it
174
+ author-visible, and this port lands ahead of that wiring deliberately.
175
+
176
+ ### Patch Changes
177
+
178
+ - 34f60b7: The JSX-source parser no longer deletes the space that separates a text run
179
+ from an adjacent sibling element. `parseChildren` collapsed each text run's
180
+ whitespace to a single space (correct — that is HTML's own whitespace model)
181
+ and then `.trim()`ed it (not correct — HTML collapses a whitespace run to one
182
+ space, it does not delete it), so `A <strong>x</strong> page` compiled to
183
+ `['A', {strong}, 'page']` and the words ran together wherever that tree is
184
+ rendered.
185
+
186
+ The rule now applied: collapse the run, then keep one leading space when a
187
+ sibling precedes it and one trailing space when a sibling element follows it;
188
+ at the parent's own start/end the edge space is still dropped, so
189
+ `<p> hi </p>` still compiles to `['hi']`. It is deliberately mechanical — it
190
+ invents no block/inline taxonomy for a schema tree that has none. Its one
191
+ bounded cost is that a whitespace-only run between two siblings survives as a
192
+ single space, so a pretty-printed `<ul>` gains one `' '` child per inter-item
193
+ gap; the tests pin that bound. This matches the rule the downstream copy of
194
+ this parser already applies, so the two agree on the tree they produce.
195
+
3
196
  ## 17.2.0
4
197
 
5
198
  ## 17.1.0
package/dist/index.d.mts CHANGED
@@ -39,7 +39,20 @@ interface ParseResult {
39
39
  type ManifestInputType = 'string' | 'number' | 'boolean' | 'enum' | 'array' | 'object' | 'color' | 'date' | 'code' | 'file' | 'slot';
40
40
  interface ManifestInput {
41
41
  name: string;
42
- type: ManifestInputType;
42
+ /**
43
+ * The input's coarse type: ONE kind, or an ARRAY of kinds when the key's
44
+ * contract is a union (objectui#3832).
45
+ *
46
+ * A value passes {@link validateTree}'s coarse check when ANY arm accepts it,
47
+ * and is reported when none does — the array widens what is legal, it does
48
+ * not switch the check off.
49
+ *
50
+ * The single-kind form is unchanged and stays the canonical spelling for a
51
+ * one-arm key: `manifestFromConfigs` collapses a one-element array back to
52
+ * the bare string, so a manifest gains arrays only where a union was really
53
+ * declared and every already-published entry serializes byte-identically.
54
+ */
55
+ type: ManifestInputType | ManifestInputType[];
43
56
  required?: boolean;
44
57
  /** allowed values for `enum` inputs */
45
58
  enum?: Array<string | {
@@ -94,9 +107,25 @@ interface ValidationResult {
94
107
  declare function parseJsx(source: string, options?: ParseOptions): ParseResult;
95
108
  /**
96
109
  * Interpret a braced attribute value `{...}`.
97
- * JSON-literal values (numbers, booleans, null, strings, arrays, objects with
98
- * quoted keys) are materialized. Anything else is kept as a deferred expression
99
- * marker `{ $expr }` typed and validated later, NEVER evaluated here.
110
+ *
111
+ * Strict-JSON values are materialized by `JSON.parse`, exactly as they always
112
+ * were. Beyond that, the JS **literal subset** below is materialized too
113
+ * (objectui#6614 Q1-A, maintainer ruling 2026-08-28). Anything left over — a
114
+ * genuine expression — is kept as the deferred marker `{ $expr }`: typed and
115
+ * validated later, drawing `inert-expression`, and NEVER evaluated here.
116
+ *
117
+ * ORDER IS LOAD-BEARING. `JSON.parse` runs FIRST and is untouched, so every
118
+ * input JSON accepts takes byte-identically the path it took before the literal
119
+ * subset existed. The reader below only ever sees strings `JSON.parse` has
120
+ * already thrown on, which makes strict-JSON invariance a property of the
121
+ * structure rather than of a test.
122
+ *
123
+ * LOCKSTEP: this grammar is the port of objectui's `packages/sdui-parser` copy
124
+ * (objectui#6614). The two copies must agree on the accepted grammar AND on
125
+ * diagnostic codes — if they drift, the save gate and the renderer speak
126
+ * different dialects and a page can save clean and render inert
127
+ * (objectstack#12719 states the invariant; #12977 carries this half of it).
128
+ * Change this block only together with the objectui copy.
100
129
  */
101
130
  declare function interpretBrace(raw: string): unknown;
102
131
 
@@ -111,6 +140,128 @@ declare function interpretBrace(raw: string): unknown;
111
140
 
112
141
  declare function validateTree(tree: SchemaElement | null, manifest: Manifest): ValidationResult;
113
142
 
143
+ /**
144
+ * Unconsumed dashboard-widget `options` keys (objectui#5709), ported into this
145
+ * copy in lockstep (objectstack#12810).
146
+ *
147
+ * `@objectstack/spec`'s `DashboardWidgetOptionsSchema` ends in `.passthrough()`
148
+ * ("declared query keys + open renderer extras"), so ANY key parses, validates
149
+ * and lints cleanly — including one no renderer reads. That is how a showcase
150
+ * dashboard shipped `options: { invert: true }` on a gauge with a comment
151
+ * saying what it was believed to do, and rendered the un-inverted measure with
152
+ * no diagnostic anywhere (objectui#5709). The 2026-08-23 maintainer ruling:
153
+ * open extras stay open — they just stop being SILENT. A key that reaches no
154
+ * renderer draws a WARNING naming the consumed set. Not an error: no gate
155
+ * weakening and no new red gates were ruled.
156
+ *
157
+ * ## LOCKSTEP — what is byte-equal here and what deliberately is not
158
+ *
159
+ * Two copies of this parser exist: objectui's `packages/sdui-parser` and this
160
+ * hoisted `@objectstack/sdui-parser`. The invariant they owe each other is that
161
+ * both agree on the accepted grammar AND on diagnostic codes — if they drift,
162
+ * the save gate and the renderer speak different dialects and a page can save
163
+ * clean and render inert, or the reverse (objectstack#12719, objectstack#12810).
164
+ *
165
+ * Everything from the `import` line below to end of file is a byte-equal port of
166
+ * objectui's `src/dashboard-widget-options.ts` SAVE FOR ONE TOKEN, called out
167
+ * at the site itself: the emitted `code` is spelled as an inline literal here
168
+ * and as the constant there, because this repo runs a vocabulary gate objectui
169
+ * does not. The emitted `code`, `severity`, `message` and the whole census
170
+ * scope are identical, and `__tests__/dashboard-widget-options.test.ts`
171
+ * re-derives that rather than trusting it — including an explicit pin that the
172
+ * literal equals `UNCONSUMED_WIDGET_OPTION`. Change these functions only
173
+ * together with the objectui copy.
174
+ *
175
+ * THIS HEADER is the one deliberate divergence, and it has to be: objectui's
176
+ * header cites the maintenance machinery that derives the census — its
177
+ * `DatasetWidget.tsx` / `DashboardRenderer.tsx` read sites, its
178
+ * `plugin-dashboard.mdx` claim and its two census tests. NONE of those files
179
+ * exists in this repo (measured: no dashboard renderer package here at all), so
180
+ * copying those sentences would ship claims this checkout cannot support and
181
+ * nothing here would ever notice them going false. What follows instead states
182
+ * where each half of the census is derivable, and from what.
183
+ *
184
+ * ## The accepted set, and where each half is authoritative
185
+ *
186
+ * The spec REQUIRES `dataset` on every widget (`DashboardWidgetSchema`, this
187
+ * repo: `packages/spec/src/ui/dashboard.zod.ts`), and both of objectui's
188
+ * dashboard surfaces route a dataset-bound widget to `DatasetWidget`. On that —
189
+ * the only spec-legal — path the renderer-consumed `options` keys are exactly
190
+ * the five the spec DECLARES:
191
+ *
192
+ * dateGranularity, sortBy, sortOrder, limit (query-affecting, framework#3588)
193
+ * stageOrder (funnel/pyramid stage order)
194
+ *
195
+ * plus ONE undeclared key with a real read site:
196
+ *
197
+ * description — the metric-card sub-caption channel. `translateDashboard`
198
+ * OVERLAYS the `widgets.{id}.subCaption` translation onto this key, and that
199
+ * pipeline lives IN THIS REPO: `packages/spec/src/system/i18n-resolver.ts`
200
+ * documents `WidgetLike.options` as "the renderer-extras bag …
201
+ * `translateDashboard` writes exactly one key into it — `description`"
202
+ * (objectstack#5428 item 4, objectstack#7862). Warning on a key the
203
+ * platform's own translation pipeline writes would be a false positive on
204
+ * legal metadata, so it is in the accepted set even though the dataset-bound
205
+ * render path does not currently display it.
206
+ *
207
+ * Notably NOT consumed on the path a widget really renders through:
208
+ * `thresholds` and `format`. Both were widely believed to work; both draw this
209
+ * warning, which is the point. Their closure claims are objectui's to derive —
210
+ * `thresholds` by a repo-wide read-site scan there, `format` by the bounded
211
+ * claim that the dataset-bound path formats from the MEASURE's own metadata —
212
+ * and they are NOT restated here as claims about this repo, which has no
213
+ * renderer to make them about.
214
+ *
215
+ * ## The drift risk that lives on THIS side
216
+ *
217
+ * The spec whose `.passthrough()` this reasons about ships from this repo. So
218
+ * the one way this list can go stale HERE is a new DECLARED key landing in
219
+ * `DashboardWidgetOptionsSchema` without landing in the array below: the key
220
+ * would be spec-legal, renderer-consumed on the objectui side, and warned about
221
+ * here — a false positive on legal metadata. `@objectstack/sdui-parser` takes
222
+ * no dependency on `@objectstack/spec` (it is dependency-free and hoistable by
223
+ * design), so that cross-check is not mechanized in this copy; the census test
224
+ * next door pins the array and names the spec file to re-read when it moves.
225
+ *
226
+ * ## Scope — where the warning deliberately does NOT fire
227
+ *
228
+ * - Widgets WITHOUT `dataset`: the legacy inline forms (`options.data`
229
+ * arrays, `provider: 'object'` bags) consume a much larger, spread-shaped
230
+ * key set, whose true reach is each child component's prop surface. That
231
+ * form is spec-illegal today (`dataset` is required) and its census would be
232
+ * the unmaintainable one; skipping it keeps every warning this module emits
233
+ * a statement about the path the widget actually renders through.
234
+ * - Widgets in the legacy COMPONENT format (`widget.component`): `options`
235
+ * is not part of that contract.
236
+ * - Widgets carrying the spec's own escape hatch
237
+ * `suppressWarnings: ['unconsumed-widget-option']` — the spec models
238
+ * per-widget diagnostic suppression (`DashboardWidgetSchema.suppressWarnings`,
239
+ * "Build diagnostic rule ids suppressed on this widget"), so an author with
240
+ * a genuine out-of-band consumer can say so in metadata.
241
+ */
242
+
243
+ /** The diagnostic `code` — also the id `suppressWarnings` suppresses. */
244
+ declare const UNCONSUMED_WIDGET_OPTION = "unconsumed-widget-option";
245
+ /**
246
+ * Component types that host a dashboard `widgets` array. Both resolve to the
247
+ * surfaces measured by the census above (`DashboardRenderer`,
248
+ * `DashboardGridLayout`), which share one dispatch (`widgetDispatch.ts`).
249
+ */
250
+ declare const DASHBOARD_WIDGET_HOST_TYPES: ReadonlySet<string>;
251
+ /**
252
+ * The accepted set: every `options` key with a renderer read site on the
253
+ * dataset-bound path, plus the sub-caption convention key. Alphabetical; the
254
+ * warning message prints it verbatim. Derivation and evidence: file header.
255
+ */
256
+ declare const CONSUMED_WIDGET_OPTION_KEYS: readonly string[];
257
+ /**
258
+ * Diagnostics for `options` keys no renderer consumes, over one dashboard-host
259
+ * node's `widgets` array. Pure and shallow by design: it never descends into
260
+ * `children` (the caller's walk owns that) and answers `[]` for every shape
261
+ * outside its census — see the scope notes in the file header.
262
+ */
263
+ declare function checkDashboardWidgetOptions(node: SchemaElement): Diagnostic[];
264
+
114
265
  /**
115
266
  * ObjectUI — codegen the JSX type surface from the registry manifest (ADR-0080 §3)
116
267
  *
@@ -135,6 +286,53 @@ declare function propsName(type: string): string;
135
286
  */
136
287
  declare function generateBlockList(manifest: Manifest): string;
137
288
 
289
+ /**
290
+ * ObjectUI — the arms of a manifest input's coarse type (objectui#3832)
291
+ *
292
+ * `ManifestInput.type` carries ONE coarse kind, or an ARRAY of kinds when the
293
+ * key's contract is a union. Every reader of that field needs the same two
294
+ * decisions made the same way — how to see the arms, and which single form to
295
+ * publish — so they live here once instead of at each call site. A reader that
296
+ * forgets is not loud: `switch (input.type)` handed an array falls through to
297
+ * the default branch and reports NOTHING, which looks exactly like a value that
298
+ * validated cleanly.
299
+ */
300
+
301
+ /** The eleven coarse kinds, as a runtime set for guarding untyped input. */
302
+ declare const MANIFEST_INPUT_TYPES: ReadonlySet<string>;
303
+ /**
304
+ * The declared arms of an input's coarse type, always as an array.
305
+ *
306
+ * Exported (not merely internal) so a third-party manifest consumer — a
307
+ * designer panel, a codegen, a validator of its own — reads the arms through
308
+ * the same accessor this package's own gate does, rather than re-deriving the
309
+ * `Array.isArray` branch and getting it subtly wrong on the union form.
310
+ */
311
+ declare function inputTypeArms(type: ManifestInput['type'] | undefined): ManifestInputType[];
312
+ /**
313
+ * Project a DECLARED type (which may come from an untyped registry config) into
314
+ * the canonical published form: a bare string for one arm, an array for a real
315
+ * union.
316
+ *
317
+ * Two deliberate asymmetries between the single and array forms:
318
+ *
319
+ * - A single unrecognized kind still becomes `'string'`. That coercion predates
320
+ * the union work and is kept exactly: with one arm there is no other
321
+ * information to fall back on, and a manifest whose `type` is off-vocabulary
322
+ * would make every consumer's switch silently inert.
323
+ * - An unrecognized arm INSIDE an array is DROPPED, not coerced. Promoting it
324
+ * to `'string'` would publish an arm the author never declared — a widening
325
+ * invented by the serializer — and the surviving arms already carry the
326
+ * declaration. If dropping empties the array, the single-arm fallback applies
327
+ * so the output is always a valid manifest.
328
+ *
329
+ * Collapsing one arm to the bare string is what keeps this a backward-compatible
330
+ * extension of `sdui.manifest.json`: every input declared today serializes to
331
+ * the byte-identical entry it does now, and arrays appear only where a union was
332
+ * really declared.
333
+ */
334
+ declare function canonicalizeInputType(type: unknown): ManifestInputType | ManifestInputType[];
335
+
138
336
  /**
139
337
  * @objectstack/sdui-parser — constrained JSX-source → SDUI SchemaNode tree (ADR-0080)
140
338
  *
@@ -166,7 +364,14 @@ interface RegistryConfigLike {
166
364
  category?: string;
167
365
  inputs?: Array<{
168
366
  name: string;
169
- type: string;
367
+ /**
368
+ * One coarse kind, or the arms of a union (objectui#3832). Typed loosely
369
+ * (`string`) on purpose — this interface is the STRUCTURAL boundary that
370
+ * keeps this package free of a dependency on the registry, so an
371
+ * off-vocabulary value has to be representable here and is normalized by
372
+ * `canonicalizeInputType` on the way in.
373
+ */
374
+ type: string | string[];
170
375
  required?: boolean;
171
376
  enum?: Array<string | {
172
377
  value: unknown;
@@ -181,4 +386,4 @@ declare function manifestFromConfigs(configs: RegistryConfigLike[], opts?: {
181
386
  publicOnly?: boolean;
182
387
  }): Manifest;
183
388
 
184
- export { type CodegenOptions, type CompileResult, type Diagnostic, type Manifest, type ManifestComponent, type ManifestInput, type ManifestInputType, type ParseOptions, type ParseResult, type RegistryConfigLike, type SchemaElement, type SchemaNode, type Severity, type ValidationResult, compile, generateBlockList, generateDts, interpretBrace, manifestFromConfigs, parseJsx, propsName, validateTree };
389
+ export { CONSUMED_WIDGET_OPTION_KEYS, type CodegenOptions, type CompileResult, DASHBOARD_WIDGET_HOST_TYPES, type Diagnostic, MANIFEST_INPUT_TYPES, type Manifest, type ManifestComponent, type ManifestInput, type ManifestInputType, type ParseOptions, type ParseResult, type RegistryConfigLike, type SchemaElement, type SchemaNode, type Severity, UNCONSUMED_WIDGET_OPTION, type ValidationResult, canonicalizeInputType, checkDashboardWidgetOptions, compile, generateBlockList, generateDts, inputTypeArms, interpretBrace, manifestFromConfigs, parseJsx, propsName, validateTree };