@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/parse.js CHANGED
@@ -17,6 +17,47 @@ import { markHtmlTierNode } from './provenance.js';
17
17
  /** Event handlers and raw-HTML injection are never allowed (parse ≠ execute). */
18
18
  const EVENT_ATTR = /^on[A-Z]/;
19
19
  const FORBIDDEN_ATTRS = new Set(['dangerouslySetInnerHTML', 'ref', 'key']);
20
+ /**
21
+ * The envelope's own discriminator, which on THIS tier the tag name sets.
22
+ *
23
+ * An authored `type=` attribute is a NAME COLLISION with it, and the parser
24
+ * refuses it at parse time (maintainer ruling 2026-09-01, recorded as an
25
+ * append-only amendment on ADR-0080 — 「响亮拒绝」, quoted verbatim there;
26
+ * objectui#7235 ports it to this copy, objectstack#13957 landed the other).
27
+ * One diagnostic naming BOTH the tag and the attribute replaces two bad
28
+ * outcomes:
29
+ *
30
+ * - the value named another REGISTERED type (`<flex type="grid">`) — the tree
31
+ * carried `type:'grid'`, `validateTree` found `grid` in the manifest, every
32
+ * check passed, and the page rendered a grid where the author wrote a flex.
33
+ * ZERO diagnostics. On the one tier whose whole premise is that unreviewed
34
+ * and AI-authored source is safe to accept.
35
+ * - the value named NOTHING registered (`<object-chart type="bar">`, the shape
36
+ * a react-tier author carries across) — loud, but `unknown-component`
37
+ * naming `"bar"` reads as a missing plugin, never as a bad prop.
38
+ *
39
+ * ⛔ NOT rescued as `specType` the way the react tier rescues it (objectui#2880):
40
+ * that is consumer-side tolerance, and it would spread an alias concept to a
41
+ * second tier. ⛔ NOT a warning grace period either — the same ruling declined
42
+ * a staged rollout. ⛔ And NOT fixable at the warning layer: `type` is in
43
+ * `validate.ts`'s `BASE_PROPS` deliberately (it is correct for every other
44
+ * member), so removing it there would make every legitimate node warn. The
45
+ * refusal belongs here, at parse.
46
+ *
47
+ * ⚠️ The code is the EXISTING `forbidden-attr`, not a new one, and that is
48
+ * load-bearing rather than lazy: objectstack's `scripts/check-sdui-lockstep.mjs`
49
+ * holds ITS copy's diagnostic-code set equal to THIS one's at the pinned
50
+ * revision, so a code minted on one side only IS the dialect split that gate
51
+ * exists to catch (objectstack#12719). `forbidden-attr` already carries this
52
+ * shape — an attribute this tier refuses, named beside its element — and both
53
+ * copies stamp it.
54
+ *
55
+ * ⚠️ This copy is the RENDERER's (and the console's live edit preview); the
56
+ * save gate runs objectstack's. Until this landed the two differed in accepted
57
+ * grammar: a page objectstack refused still compiled silently while its author
58
+ * was typing.
59
+ */
60
+ const DISCRIMINATOR_ATTR = 'type';
20
61
  export function parseJsx(source, options = {}) {
21
62
  return new Parser(source, options).parseDocument();
22
63
  }
@@ -85,7 +126,16 @@ class Parser {
85
126
  const attr = this.parseAttr(start, tag);
86
127
  if (!attr)
87
128
  break;
88
- props[attr.name] = attr.value;
129
+ // `drop` is set only for the refused discriminator attribute, and only so
130
+ // that ONE diagnostic is what the author gets. The `__forbidden_<name>`
131
+ // sentinel the other refusals park in `props` reaches `validateTree`,
132
+ // which knows no such prop and adds `unknown-prop` naming a key nobody
133
+ // wrote — loud, and pointing at the wrong thing, which is the species of
134
+ // diagnostic this whole change exists to remove. The existing sentinel
135
+ // behaviour is left exactly as it was for the attributes that already had
136
+ // it (`ref`, `key`, `dangerouslySetInnerHTML`, `on*`).
137
+ if (!attr.drop)
138
+ props[attr.name] = attr.value;
89
139
  }
90
140
  this.skipWs();
91
141
  let children;
@@ -107,7 +157,22 @@ class Parser {
107
157
  // JSON, to the DOM, and to anything an authored document could forge — see
108
158
  // provenance.ts. Values reached through a braced attribute are NOT marked:
109
159
  // that JSON was written by hand, and the JSON surface's advice does apply.
110
- const node = markHtmlTierNode({ type: tag, ...props });
160
+ // DEFENSE IN DEPTH (ruled together with the refusal above). `props` used to
161
+ // be spread AFTER `type: tag`, so an authored `type` attribute overwrote the
162
+ // discriminator the tag established and nothing downstream restored it —
163
+ // `compile()` returns this tree as-is and `validateTree` then looks up
164
+ // `manifest.components[node.type]`, i.e. the value the author wrote, not the
165
+ // tag they wrote. The refusal makes that overwrite unreachable; the order
166
+ // here makes it impossible. ⚠️ Reversing the order ALONE would have been a
167
+ // regression of its own — the authored value would then be dropped in
168
+ // silence, trading one silence for another. It is correct only BECAUSE the
169
+ // attribute is refused loudly one function up.
170
+ //
171
+ // ⚠️ The `markHtmlTierNode` wrapper is objectui-only and survives the
172
+ // reversal: it stamps the object the spread produced, so the marker is
173
+ // applied after every key is in place regardless of their order. Pinned in
174
+ // provenance.test.ts, because this is the one edit that could drop it.
175
+ const node = markHtmlTierNode({ ...props, type: tag });
111
176
  if (children && children.length)
112
177
  node.children = children;
113
178
  return node;
@@ -126,6 +191,14 @@ class Parser {
126
191
  this.skipWs();
127
192
  value = this.parseAttrValue(tag);
128
193
  }
194
+ if (name === DISCRIMINATOR_ATTR) {
195
+ // ONE diagnostic naming both the tag and the attribute — see
196
+ // DISCRIMINATOR_ATTR above for why it replaces both prior outcomes.
197
+ this.error('forbidden-attr', `Attribute "${DISCRIMINATOR_ATTR}" is not allowed on <${tag}> — on this tier the tag name IS the `
198
+ + `component, so <${tag}> already means type "${tag}". Delete the attribute, or write the tag of the `
199
+ + 'component you meant.', elStart, tag);
200
+ return { name, value: undefined, drop: true };
201
+ }
129
202
  if (EVENT_ATTR.test(name) || FORBIDDEN_ATTRS.has(name)) {
130
203
  this.error('forbidden-attr', `Attribute "${name}" is not allowed on <${tag}>`, elStart, tag);
131
204
  return { name: `__forbidden_${name}`, value: undefined };
@@ -301,9 +374,18 @@ class Parser {
301
374
  }
302
375
  /**
303
376
  * Interpret a braced attribute value `{...}`.
304
- * JSON-literal values (numbers, booleans, null, strings, arrays, objects with
305
- * quoted keys) are materialized. Anything else is kept as a deferred expression
306
- * marker `{ $expr }` — typed and validated later, NEVER evaluated here.
377
+ *
378
+ * Strict-JSON values are materialized by `JSON.parse`, exactly as they always
379
+ * were. Beyond that, the JS **literal subset** below is materialized too
380
+ * (objectui#6614 Q1-A, maintainer ruling 2026-08-28). Anything left over — a
381
+ * genuine expression — is kept as the deferred marker `{ $expr }`: typed and
382
+ * validated later, drawing `inert-expression`, and NEVER evaluated here.
383
+ *
384
+ * ORDER IS LOAD-BEARING. `JSON.parse` runs FIRST and is untouched, so every
385
+ * input JSON accepts takes byte-identically the path it took before the literal
386
+ * subset existed. The reader below only ever sees strings `JSON.parse` has
387
+ * already thrown on, which makes strict-JSON invariance a property of the
388
+ * structure rather than of a test.
307
389
  */
308
390
  export function interpretBrace(raw) {
309
391
  const trimmed = raw.trim();
@@ -311,6 +393,242 @@ export function interpretBrace(raw) {
311
393
  return JSON.parse(trimmed);
312
394
  }
313
395
  catch {
314
- return { $expr: trimmed };
396
+ const literal = readLiteral(trimmed);
397
+ return literal === NOT_LITERAL ? { $expr: trimmed } : literal;
398
+ }
399
+ }
400
+ /* ---------------------- the JS literal subset (#6614) ---------------------- */
401
+ /**
402
+ * EXACTLY TWO widenings over JSON, and nothing else:
403
+ *
404
+ * 1. **single-quoted strings** — `{'name'}`, `{['name','amount']}`, and in
405
+ * key position `{{'pageSize': 25}}`;
406
+ * 2. **unquoted identifier object keys** — `{{pageSize: 25}}`.
407
+ *
408
+ * Everything else JSON refuses is still refused and still becomes `{ $expr }`:
409
+ * trailing commas, comments, array holes, spreads, `undefined` / `NaN` /
410
+ * `Infinity`, `+1` / `.5` / `1.` / `0x1f`, template literals, and every genuine
411
+ * expression — identifiers, member access, calls, operators, ternaries.
412
+ *
413
+ * That list is deliberately short. This is a VALUE grammar, not an evaluator:
414
+ * it contains no identifier lookup and no operator, so there is nothing here to
415
+ * execute (ADR-0080 — this tier parses, never executes). The widening moves the
416
+ * spellings an author writes by habit onto the materialized side; it does not
417
+ * move the boundary between data and code.
418
+ */
419
+ const NOT_LITERAL = Symbol('not-a-literal');
420
+ /** JSON's whitespace set, not JS's — narrower, and one less thing to diverge. */
421
+ const LITERAL_WS = /[ \t\n\r]/;
422
+ const IDENT_START = /[A-Za-z_$]/;
423
+ const IDENT_CHAR = /[A-Za-z0-9_$]/;
424
+ /** JSON's number grammar verbatim: no leading `+`, no `.5`, no `1.`, no hex. */
425
+ const NUMBER = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/;
426
+ /** JSON's escape set. `\'` is added for single-quoted strings only. */
427
+ const SIMPLE_ESCAPE = {
428
+ '"': '"',
429
+ '\\': '\\',
430
+ '/': '/',
431
+ b: '\b',
432
+ f: '\f',
433
+ n: '\n',
434
+ r: '\r',
435
+ t: '\t',
436
+ };
437
+ function readLiteral(src) {
438
+ const reader = new LiteralReader(src);
439
+ const value = reader.value();
440
+ if (value === NOT_LITERAL)
441
+ return NOT_LITERAL;
442
+ reader.ws();
443
+ // Trailing anything means the input was an expression that merely STARTS with
444
+ // a literal (`['a'] + x`, `1 + 2`). Refuse the whole input.
445
+ return reader.done() ? value : NOT_LITERAL;
446
+ }
447
+ class LiteralReader {
448
+ constructor(src) {
449
+ Object.defineProperty(this, "src", {
450
+ enumerable: true,
451
+ configurable: true,
452
+ writable: true,
453
+ value: src
454
+ });
455
+ Object.defineProperty(this, "pos", {
456
+ enumerable: true,
457
+ configurable: true,
458
+ writable: true,
459
+ value: 0
460
+ });
461
+ }
462
+ done() {
463
+ return this.pos >= this.src.length;
464
+ }
465
+ ws() {
466
+ while (this.pos < this.src.length && LITERAL_WS.test(this.src[this.pos]))
467
+ this.pos++;
468
+ }
469
+ value() {
470
+ this.ws();
471
+ const c = this.src[this.pos];
472
+ if (c === undefined)
473
+ return NOT_LITERAL;
474
+ if (c === '"' || c === "'")
475
+ return this.string(c);
476
+ if (c === '[')
477
+ return this.array();
478
+ if (c === '{')
479
+ return this.object();
480
+ if (this.keyword('true'))
481
+ return true;
482
+ if (this.keyword('false'))
483
+ return false;
484
+ if (this.keyword('null'))
485
+ return null;
486
+ return this.number();
487
+ }
488
+ /** A keyword only when it is not the prefix of a longer identifier. */
489
+ keyword(word) {
490
+ if (!this.src.startsWith(word, this.pos))
491
+ return false;
492
+ const after = this.src[this.pos + word.length];
493
+ if (after !== undefined && IDENT_CHAR.test(after))
494
+ return false;
495
+ this.pos += word.length;
496
+ return true;
497
+ }
498
+ number() {
499
+ const m = NUMBER.exec(this.src.slice(this.pos));
500
+ if (!m)
501
+ return NOT_LITERAL;
502
+ this.pos += m[0].length;
503
+ return Number(m[0]);
504
+ }
505
+ string(quote) {
506
+ this.pos++; // opening quote
507
+ let out = '';
508
+ for (;;) {
509
+ const c = this.src[this.pos];
510
+ if (c === undefined)
511
+ return NOT_LITERAL; // unterminated
512
+ if (c === quote) {
513
+ this.pos++;
514
+ return out;
515
+ }
516
+ if (c === '\\') {
517
+ const esc = this.src[this.pos + 1];
518
+ if (esc === undefined)
519
+ return NOT_LITERAL;
520
+ if (esc === 'u') {
521
+ const hex = this.src.slice(this.pos + 2, this.pos + 6);
522
+ if (!/^[0-9a-fA-F]{4}$/.test(hex))
523
+ return NOT_LITERAL;
524
+ out += String.fromCharCode(parseInt(hex, 16));
525
+ this.pos += 6;
526
+ continue;
527
+ }
528
+ // `\'` is legal inside a single-quoted string only — JSON's set otherwise.
529
+ if (esc === "'" && quote === "'") {
530
+ out += "'";
531
+ this.pos += 2;
532
+ continue;
533
+ }
534
+ const simple = SIMPLE_ESCAPE[esc];
535
+ if (simple === undefined)
536
+ return NOT_LITERAL; // `\x41`, `\0`, line continuation
537
+ out += simple;
538
+ this.pos += 2;
539
+ continue;
540
+ }
541
+ // JSON forbids raw control characters inside a string; so does this.
542
+ if (c < ' ')
543
+ return NOT_LITERAL;
544
+ out += c;
545
+ this.pos++;
546
+ }
547
+ }
548
+ array() {
549
+ this.pos++; // '['
550
+ const out = [];
551
+ this.ws();
552
+ if (this.src[this.pos] === ']') {
553
+ this.pos++;
554
+ return out;
555
+ }
556
+ for (;;) {
557
+ const item = this.value();
558
+ if (item === NOT_LITERAL)
559
+ return NOT_LITERAL;
560
+ out.push(item);
561
+ this.ws();
562
+ const c = this.src[this.pos];
563
+ // A trailing comma leaves `value()` facing `]`, which it refuses — so
564
+ // `['a',]` is NOT in the subset. Only two widenings were ruled.
565
+ if (c === ',') {
566
+ this.pos++;
567
+ continue;
568
+ }
569
+ if (c === ']') {
570
+ this.pos++;
571
+ return out;
572
+ }
573
+ return NOT_LITERAL;
574
+ }
575
+ }
576
+ object() {
577
+ this.pos++; // '{'
578
+ const out = {};
579
+ this.ws();
580
+ if (this.src[this.pos] === '}') {
581
+ this.pos++;
582
+ return out;
583
+ }
584
+ for (;;) {
585
+ this.ws();
586
+ const key = this.key();
587
+ if (key === NOT_LITERAL)
588
+ return NOT_LITERAL;
589
+ this.ws();
590
+ if (this.src[this.pos] !== ':')
591
+ return NOT_LITERAL;
592
+ this.pos++;
593
+ const item = this.value();
594
+ if (item === NOT_LITERAL)
595
+ return NOT_LITERAL;
596
+ // ⚠️ Plain `out[key] = item` would hand an authored `__proto__` key the
597
+ // prototype SETTER. `JSON.parse` creates an ordinary own data property,
598
+ // and this path must too: the whole point of this tier is that untrusted
599
+ // source is safe to parse, so a widening must not open a
600
+ // prototype-pollution lever the strict-JSON path never had.
601
+ Object.defineProperty(out, key, {
602
+ value: item,
603
+ writable: true,
604
+ enumerable: true,
605
+ configurable: true,
606
+ });
607
+ this.ws();
608
+ const c = this.src[this.pos];
609
+ if (c === ',') {
610
+ this.pos++;
611
+ continue;
612
+ }
613
+ if (c === '}') {
614
+ this.pos++;
615
+ return out;
616
+ }
617
+ return NOT_LITERAL;
618
+ }
619
+ }
620
+ /** A quoted string, or a bare identifier — the second ruled widening. */
621
+ key() {
622
+ const c = this.src[this.pos];
623
+ if (c === '"' || c === "'")
624
+ return this.string(c);
625
+ if (c !== undefined && IDENT_START.test(c)) {
626
+ const start = this.pos;
627
+ this.pos++;
628
+ while (this.pos < this.src.length && IDENT_CHAR.test(this.src[this.pos]))
629
+ this.pos++;
630
+ return this.src.slice(start, this.pos);
631
+ }
632
+ return NOT_LITERAL;
315
633
  }
316
634
  }
@@ -6,10 +6,13 @@
6
6
  * source, compiled tag-name-straight-through) or AUTHORED AS JSON metadata.
7
7
  *
8
8
  * Why a renderer needs it: the two surfaces have different vocabularies. A type
9
- * the JSON surface is steering authors away from can still be a first-class,
10
- * permanently-supported tag in the html tier — the plain box element is exactly
11
- * that. Advice aimed at the JSON surface, delivered to an html-tier author, is
12
- * advice they cannot act on: in that tier there is no other spelling. The
9
+ * the JSON surface is steering authors away from can still be a supported tag
10
+ * in the html tier — the plain inline element (`span`) is exactly that. (The
11
+ * plain box element was too, until objectui#10757 deprecated `div` on the html
12
+ * surface as well; the console's html compile now refuses it outright, so no
13
+ * notice is involved there.) Advice aimed at the JSON surface, delivered to an
14
+ * html-tier author, is advice they cannot act on: in that tier there is no
15
+ * other spelling. The
13
16
  * deprecation notice fired at them anyway (objectui#4000), which is also why the
14
17
  * type could never be retired — the engine's own compiler keeps emitting it.
15
18
  *
@@ -6,10 +6,13 @@
6
6
  * source, compiled tag-name-straight-through) or AUTHORED AS JSON metadata.
7
7
  *
8
8
  * Why a renderer needs it: the two surfaces have different vocabularies. A type
9
- * the JSON surface is steering authors away from can still be a first-class,
10
- * permanently-supported tag in the html tier — the plain box element is exactly
11
- * that. Advice aimed at the JSON surface, delivered to an html-tier author, is
12
- * advice they cannot act on: in that tier there is no other spelling. The
9
+ * the JSON surface is steering authors away from can still be a supported tag
10
+ * in the html tier — the plain inline element (`span`) is exactly that. (The
11
+ * plain box element was too, until objectui#10757 deprecated `div` on the html
12
+ * surface as well; the console's html compile now refuses it outright, so no
13
+ * notice is involved there.) Advice aimed at the JSON surface, delivered to an
14
+ * html-tier author, is advice they cannot act on: in that tier there is no
15
+ * other spelling. The
13
16
  * deprecation notice fired at them anyway (objectui#4000), which is also why the
14
17
  * type could never be retired — the engine's own compiler keeps emitting it.
15
18
  *
package/dist/types.d.ts CHANGED
@@ -53,22 +53,109 @@ export interface ManifestInput {
53
53
  * declared and every already-published entry serializes byte-identically.
54
54
  */
55
55
  type: ManifestInputType | ManifestInputType[];
56
+ /**
57
+ * The coarse kind of the input's MEMBERS — array elements, or the values of
58
+ * an object used as a map — as ONE kind or an ARRAY of kinds for a member
59
+ * contract that is a union (objectui#8067).
60
+ *
61
+ * Absent means "not declared", which is what every input published before
62
+ * this key existed says: {@link validateTree} checks no member and the
63
+ * codegen emits the unnarrowed element type, exactly as before. So a
64
+ * manifest gains this key only where a member kind was really declared, and
65
+ * every already-published entry serializes byte-identically.
66
+ *
67
+ * Read the arms through `inputTypeArms(input.of)` — the same accessor
68
+ * `type`'s arms go through, since the two fields carry the same shape and a
69
+ * reader that forgets the array form is silently inert on it.
70
+ */
71
+ of?: ManifestInputType | ManifestInputType[];
56
72
  required?: boolean;
57
73
  /** allowed values for `enum` inputs */
58
74
  enum?: Array<string | {
59
75
  value: unknown;
60
76
  label?: string;
61
77
  }>;
62
- /** marks a data-binding input the server must resolve (ADR-0080 §6.3) */
63
- binding?: 'object' | 'field';
78
+ /**
79
+ * Marks a data-binding input the server must resolve (ADR-0080 §6.3):
80
+ * `binding: 'object'` says the input NAMES an object, so the server-side
81
+ * binding check knows what to resolve it against. Unrelated to
82
+ * `type: 'object'`, the coarse control kind — a record-shaped value and an
83
+ * object-naming input are two different facts.
84
+ *
85
+ * ## The vocabulary is exactly `'object'` — `'field'` is retired on this face too
86
+ *
87
+ * `'field'` stood beside it from the first draft of ADR-0080 and was never
88
+ * written. It was retired on the serializer's INPUT boundary
89
+ * (`RegistryConfigLike` in `index.ts`) under ADR-0049 enforce-or-remove by
90
+ * the maintainer ruling of 2026-09-07 (decision batch #69), and on THIS
91
+ * face by objectui#8315 — the residue that ruling did not name, which left
92
+ * one published package narrow on one face and wide on the other.
93
+ *
94
+ * ⚠️ The argument for leaving this face wide was ANSWERED, not overlooked.
95
+ * It runs: producer → reader is a subset relation, so a reader accepting a
96
+ * value no producer emits is permissive rather than wrong. Three measured
97
+ * facts decided against it (objectui#8315):
98
+ *
99
+ * 1. **This is not a pure reader face.** `manifestFromConfigs` RETURNS a
100
+ * `Manifest`, so `ManifestInput` is also this package's OUTPUT type,
101
+ * and this key is fed straight from the already-narrowed boundary. A
102
+ * union wider than the producer's is imprecision on the way out, not
103
+ * permissiveness on the way in.
104
+ * 2. **Its sibling is a pure producer face.**
105
+ * `ManifestValidationResult.bindings[].kind` is written by
106
+ * `validateTree` by copying this key, so the subset relation runs the
107
+ * other way there — see that declaration. The two are COUPLED by that
108
+ * assignment: narrowing one alone needs a cast at the only conversion
109
+ * site, which is the lenient fallback AGENTS.md #0.1 bans. So "both
110
+ * narrow" and "both wide" were the only self-consistent states, and
111
+ * only one of them can be justified on the producer face.
112
+ * 3. **The permissiveness protected nothing.** `binding: 'field'` has
113
+ * zero writers here and zero in the objectstack copy of this package
114
+ * (measured 2026-09-09 on both heads, each with a firing
115
+ * `binding: 'object'` control), and the only manifest producer in
116
+ * either tree is `manifestFromConfigs`, whose input face is narrow.
117
+ *
118
+ * The reopen route is the ruling's own: a MEASURED need for field bindings
119
+ * is filed as a widening with the vocabulary decided then — not
120
+ * pre-declared here for a producer that does not exist.
121
+ */
122
+ binding?: 'object';
64
123
  description?: string;
65
124
  }
66
125
  export interface ManifestComponent {
67
126
  type: string;
68
127
  /** plugin namespace — provenance that drives `requires` */
69
128
  namespace?: string;
129
+ /**
130
+ * The declared authoring surface. An input named `children` (type `slot`)
131
+ * is ALSO the containment declaration: `validateTree` draws
132
+ * `not-a-container` on a child list under any component whose `inputs`
133
+ * carry no such entry (objectui#9910; `acceptsChildren` in `validate.ts`).
134
+ */
70
135
  inputs: ManifestInput[];
136
+ /**
137
+ * LAYOUT containment (objectui#6804, objectui#9910 Q2-A): the flag the
138
+ * react-page JSX scope builder skips and the public layout ledger lists.
139
+ * ⛔ Not "accepts children" — this tier's containment check does not read it.
140
+ */
71
141
  isContainer?: boolean;
142
+ /**
143
+ * The authoring tier this entry belongs to (objectui#10735).
144
+ *
145
+ * Absent = the curated PUBLIC tier (ADR-0080 §4), the JSON-surface
146
+ * AI-authoring vocabulary. `'html'` = one of the intrinsic HTML elements a
147
+ * `kind:'html'` page may author (ADR-0081 §2), declared by the registry's
148
+ * `HTML_TIER_INTRINSICS` roster and carried here so the ONE flat `components`
149
+ * map can serve both of its readers: a whitelist reader (`compile`, the
150
+ * objectstack gate) takes every key and needs no change; a reader that means
151
+ * the curated vocabulary — the `kind:'react'` scope, the block list, a census
152
+ * of curated blocks — filters this key out.
153
+ *
154
+ * ⛔ Not the registration's `tier` copied through. `manifestFromConfigs`
155
+ * writes exactly `'html'` or nothing, so every entry published before this
156
+ * key existed serialises byte-identically.
157
+ */
158
+ tier?: 'html';
72
159
  }
73
160
  export interface Manifest {
74
161
  /** keyed by component `type` */
@@ -94,11 +181,22 @@ export interface ManifestValidationResult {
94
181
  diagnostics: Diagnostic[];
95
182
  /** unique plugin namespaces referenced — the page's `requires` */
96
183
  requires: string[];
97
- /** binding sites (object/field) the server must resolve against object schema */
184
+ /**
185
+ * Binding sites the server must resolve against object schema.
186
+ *
187
+ * `kind` is a PRODUCER face, not a reader face: `validateTree` writes it,
188
+ * copying {@link ManifestInput.binding} at the one site that builds this
189
+ * array. So the subset relation that licenses a permissive READER runs the
190
+ * other way here — a wider union accepts nothing extra, it obliges every
191
+ * consumer to handle an arm this package cannot emit. That is why the
192
+ * retired `'field'` arm (`9e37d9b39` on the input boundary, objectui#8315
193
+ * here) is gone from this end as well; the measurements are on
194
+ * {@link ManifestInput.binding}.
195
+ */
98
196
  bindings: Array<{
99
197
  tag: string;
100
198
  input: string;
101
- kind: 'object' | 'field';
199
+ kind: 'object';
102
200
  value: unknown;
103
201
  }>;
104
202
  }
@@ -2,9 +2,116 @@
2
2
  * ObjectUI — SDUI tree validation against the registry manifest (ADR-0080 §3/§6)
3
3
  *
4
4
  * Shallow, author-time validation: unknown component, unknown/missing prop,
5
- * wrong coarse type, illegal enum value. Collects `requires` (plugin provenance)
6
- * and binding sites the SERVER must resolve against object schema (we cannot
7
- * resolve objects/fields here — that check is framework-side by design).
5
+ * wrong coarse type, illegal enum value, and containment — a child list under
6
+ * a component whose registration declares no `children` input draws
7
+ * `not-a-container` (objectui#9910; see `acceptsChildren`). Collects
8
+ * `requires` (plugin provenance) and binding sites the SERVER must resolve
9
+ * against object schema (we cannot resolve objects/fields here — that check is
10
+ * framework-side by design).
8
11
  */
9
- import type { Manifest, SchemaElement, ManifestValidationResult } from './types.js';
12
+ import type { Manifest, ManifestComponent, SchemaElement, ManifestValidationResult } from './types.js';
13
+ /**
14
+ * The protocol's ONE child-list key — `BaseSchema.children` — and therefore the
15
+ * name of the `inputs` entry a registration declares when its renderer puts
16
+ * that list on the page: `{ name: 'children', type: 'slot' }` (objectui#9910).
17
+ */
18
+ export declare const CHILD_LIST_KEY = "children";
19
+ /**
20
+ * Does this component ACCEPT an authored child list? (objectui#9910)
21
+ *
22
+ * Read from exactly ONE declaration: an input named {@link CHILD_LIST_KEY} in
23
+ * the component's `inputs`. That is the shape ten registrations already
24
+ * carried when the maintainer ruled it the containment contract
25
+ * (2026-09-24, objectui#9910 Q1-A "declare-and-pin"), and it is held in both
26
+ * directions by the runtime census in
27
+ * `packages/components/src/renderers/__tests__/container-declaration-ratchet.test.tsx`:
28
+ * a renderer that puts `schema.children` on the page without declaring the
29
+ * input is red, and a declaration whose renderer never renders the list is
30
+ * red too.
31
+ *
32
+ * ⛔ `isContainer` is NOT consulted and is NOT a fallback. It used to decide
33
+ * this branch and it lied both ways: a hand-kept flag drifted from the code
34
+ * (objectui#3900 / `7c9b044f4` / #6764 / #6779 found the same drift four times),
35
+ * and after objectui#6771 converged a dozen registrations onto `children`
36
+ * the flag put a FALSE `not-a-container` on the one key they read
37
+ * (objectui#9910). The flag now means LAYOUT containment only — the
38
+ * react-page JSX scope and the public layout ledger read it; this tier does
39
+ * not (objectui#6804, objectui#9910 Q2-A).
40
+ */
41
+ export declare function acceptsChildren(comp: Pick<ManifestComponent, 'inputs'>): boolean;
42
+ /**
43
+ * Where a base prop is legal without a declaration (objectui#11044).
44
+ *
45
+ * - `'every-node'` — on every node, and a registration's own input of the
46
+ * same name is not consulted: {@link validateTree} skips the key before the
47
+ * declared-input lookup.
48
+ * - `'where-undeclared'` — on a type whose registration declares NO input of
49
+ * that name. Where one does, the declared input wins, its `type-mismatch`
50
+ * check included, and the generated JSX types take the declared type too.
51
+ */
52
+ export type SduiBasePropScope = 'every-node' | 'where-undeclared';
53
+ /** One entry of {@link SDUI_BASE_PROPS}. */
54
+ export interface SduiBaseProp {
55
+ /** The `BaseSchema` member (`@object-ui/types`). */
56
+ readonly name: string;
57
+ readonly scope: SduiBasePropScope;
58
+ /**
59
+ * The attribute's type in the generated JSX surface (`SduiBaseProps` in
60
+ * `sdui-intrinsics.d.ts`), or `null` for the one key that is no attribute:
61
+ * `type`, which the tag name carries (`parse.ts` refuses a `type` attribute).
62
+ */
63
+ readonly tsType: string | null;
64
+ }
65
+ /**
66
+ * The base props: the `BaseSchema` members this tier accepts on a node without
67
+ * a registration declaring them. ONE list, read by BOTH of its consumers —
68
+ * {@link validateTree}'s `unknown-prop` branch and `generateDts`'s
69
+ * `SduiBaseProps` (objectui#11044). Before this list the two were separate
70
+ * hand-kept copies and had drifted: the validator accepted `bind` and `hidden`
71
+ * (objectui#11008) while the generated types still refused both.
72
+ *
73
+ * `bind` and `hidden` are `'every-node'` (objectui#11008). `BaseSchema` declares
74
+ * both for every node and no registration declares either as an input, so
75
+ * before they joined, the undeclared-key branch below answered every authored
76
+ * one with `unknown-prop` — "has no prop" about a key the protocol declares,
77
+ * on the nodes that honour it: `hidden` is read for every node by
78
+ * `SchemaRenderer`'s hide chain, and `bind` by every renderer that calls
79
+ * `useDataScope` (`list`, `tree-view`, the `object-*` widgets). The
80
+ * declaration outranks the implementation, so the parser's view is the
81
+ * declared type's, not a per-registration subset. The cost is accepted and
82
+ * named: a `bind` on a node that does not read it — `data-table`
83
+ * (objectui#6575) — draws nothing here either, and its render-time console
84
+ * warning is the one signal left.
85
+ *
86
+ * `visibleWhen`, `hiddenOn` and `testId` are `'every-node'` for the same reason
87
+ * (objectui#11044): no registration declares any of them, `SchemaRenderer`'s
88
+ * hide chain reads the first two for every node, and it strips `testId` and
89
+ * re-emits it as `data-testid`. `visibleWhen` is the canonical ADR-0089
90
+ * predicate; before it joined, the deprecated `visibleOn` was silent while it
91
+ * drew `unknown-prop`.
92
+ *
93
+ * The `'where-undeclared'` members (objectui#11044, triage ruling) are the
94
+ * `BaseSchema` members some registrations DECLARE as typed inputs — the input
95
+ * family's `placeholder`, `label`, `name`, … . Skipping them the way the
96
+ * `'every-node'` members are skipped would silence those registrations'
97
+ * `type-mismatch`, so they are base props only where the type declares no
98
+ * input of that name. ⛔ Never move one to `'every-node'` to accept a key: that
99
+ * silences a declared type check.
100
+ *
101
+ * Held over the live registry by `base-props-one-list-11044.test.tsx` in
102
+ * `@object-ui/components` — every member a `BaseSchema` member, and `body` the
103
+ * one member left out.
104
+ *
105
+ * ⛔ `body` is NOT here and must not be added. It was `BaseSchema`'s second
106
+ * child-list spelling until objectui#6771 retired it; teaching this list the
107
+ * key was the option that ruling refused, because it would have blessed a
108
+ * second permanent spelling of one concept. `./body-dialect.ts` answers it by
109
+ * name instead.
110
+ *
111
+ * `children` IS here: the key is legal on every node, so it never draws
112
+ * `unknown-prop` and its declared `slot` input is never type-checked. Whether
113
+ * a given component RENDERS it is the containment question below, answered by
114
+ * {@link acceptsChildren} from the declared input.
115
+ */
116
+ export declare const SDUI_BASE_PROPS: readonly SduiBaseProp[];
10
117
  export declare function validateTree(tree: SchemaElement | null, manifest: Manifest): ManifestValidationResult;