Skip to content

Commit a093ce3

Browse files
objectstack-fleet[bot]hotlongclaude
authored
feat(sdui-parser): the manifest marks the html tier's intrinsic tags tier: 'html', ported from objectui's lockstep copy (#20582)
Fixes #20112 Clause-②: yes (widening) Executes the one item left on this card after PR #20436: port objectui's `tier` plumbing into this repository's copy of `packages/sdui-parser`, regenerate `sdui.manifest.json` so the html tier's intrinsic tags carry `tier: 'html'`, and re-record. This follows ruling A (`5852014527`) as triage re-derived it at pin `dd3f7e1be356` (`5882165416`). Base `origin/main` `f11b5f20a2`, head `ffbb0c4802`. ## What changed - **`packages/sdui-parser/src/{index,types,codegen}.ts`**: the three hunks of objectui `baac95a261` (objectui#10735), copied byte-for-byte from objectui at the pin `dd3f7e1be356`: - `RegistryConfigLike.tier` admits `'html'`; - `ManifestComponent.tier?: 'html'` is new; - `manifestFromConfigs` writes exactly `'html'` or omits the key; - `generateBlockList` titles the curated count and lists the html tier in its own section. - **`sdui.manifest.json` + `scripts/sdui-manifest.record.json`**: regenerated with `scripts/gen-sdui-manifest-node.mjs` over objectui's built tree at the pin. `"tier": "html"` lands on 48 entries and nothing else moves. The record's sha256 and date are re-recorded. - **`packages/sdui-parser/src/__tests__/html-tier-manifest.test.ts`**: objectui's own ten cases for the mechanism (`html-tier-manifest-10735.test.ts` at the pin), with this repository's header. - **`.changeset/20112-sdui-parser-html-tier-stamp.md`**: `minor` for `@objectstack/sdui-parser` (a new accepted value and a new type member) and for `@objectstack/console` (its `dist` ships the regenerated manifest). **Byte-faithfulness, measured.** For each of the three files, the `+`/`-` lines of this diff equal the `+`/`-` lines of objectui `baac95a261` for the same file (index 15 lines, types 17, codegen 39; `cmp` identical). Every added line is present verbatim in the file at the pin. `generateBlockList`, `ManifestComponent` and `manifestFromConfigs` extracted whole are `cmp`-identical to the pin. The one later objectui commit to these files in the window (`fec3b1a8b`) rewrites two citation comments unrelated to `tier`. ## The PM's mechanism hypotheses, measured - **H0: holds.** At the pin, objectui declares `tier?: 'public' | 'internal' | 'html'` (`index.ts:87`) and `ManifestComponent.tier` (`types.ts:165`, member at `:180`). This copy declared `'public' | 'internal'` (`index.ts:72`). The tracked manifest had 107 components and **0** carrying any `tier` key. - **H1: holds, with a different count: 48, not 47.** After the port and regeneration, the manifest keeps the same 107 keys in the same order. 48 entries gain `"tier": "html"`, no other tier value appears, and 0 entries change any other field (each entry deep-compared with `tier` removed). `git diff --stat`: 48 insertions, 0 deletions. The 48th tag is `code`. objectui#10756 (PR objectui#10776, `29b45f6a0`) added it to `HTML_TIER_INTRINSICS` after the triage count was taken, and the pin contains it. `div` and `kbd` stay absent. - **Control leg for the build tree:** the UNPORTED serializer over the same built tree reproduced the tracked artefact byte-for-byte (sha256 `4073897dcdc1…`, unchanged file). - After the port: sha256 `d0666ac585db…`, 103874 bytes. `node scripts/check-sdui-manifest.mjs`: `✓ … intact (sha256 d0666ac585db…, 107 components) and recorded at the live objectui pin dd3f7e1be356…`. - **H2: holds.** `check:sdui-lockstep` is green both before and after the port. With the three source files restored to `f11b5f20a2` (the drift state: 0 hits for the three-arm union, 0 for `ManifestComponent.tier`), it prints `OK — this copy is byte-identical to objectui@dd3f7e1be356 …`. It compares the grammar region, the diagnostic-code set and the `not-a-container` predicate, never a type union. So it cannot see this drift. The gate is not widened (see Acceptance notes). - **Re-record:** `gen:sdui-lockstep` rewrote `packages/sdui-parser/objectui-lockstep.json` byte-identically (0 diff lines), so the lockstep record was already current at the pin. The re-recorded artefact is the manifest's record. - **H3: holds.** `compile()` over the three shipped `kind:'html'` pages (`capability-map`, `command-center-jsx`, `start-here`), against the old manifest and against the new one: - all three are `ok=true` with 0 errors and 0 warnings on both sides; - the full result objects, tree included, are deep-equal (0 of 3 moved). - Lit control: the same harness with `box` deleted from the manifest turns `start-here` to `ok=false`, 54 errors. - `@objectstack/lint`'s production witness reads the regenerated file from disk and passes (`wired run over the three shipped html pages is clean`). ## Verification All runs are at head `ffbb0c4802` unless noted. - `pnpm --filter @objectstack/sdui-parser build`, then `exec vitest run --maxWorkers=2`, then `typecheck`: 12 files, 199 tests passed; `tsc --noEmit` clean. `--listFiles` shows the typecheck program includes the new test file. - **Ablation, from the committed fix at `9a77e00415`.** The three source files were restored to `f11b5f20a2`, with a restore trap: - landing proven by `grep -c` (three-arm union 1→0, `tier?: 'html'` 0, html section 0); - the new test file goes 2 failed / 8 passed: the stamp case and the block-list section case. The other eight pin the readers the stamp must not change, and stay green by design; - `tsc --noEmit` exits 2 with six errors on the test (TS2322 ×4, TS2339, TS2367). - Restored with `git checkout HEAD -- …`. Each blob `hash-object` equals `HEAD`, and `git diff HEAD` is empty. - **Consumer**, the only importer: `pnpm --filter '@objectstack/lint^...' build`, then `@objectstack/lint` `vitest run --maxWorkers=2` (115 files, 5358 passed, 5 skipped) and `typecheck` (`tsc --noEmit` plus `check:test-typecheck` OK). - `pnpm lint` (eslint over the whole repository, `--no-inline-config`): exit 0 at `ffbb0c4802`. This was a full run, not a narrowed one. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`: 71 derived commands, all run. Reconciled with `--ran`: `71 derived famil(ies) accounted for — 68 run, 3 NOT-MEASURED`. - NOT MEASURED (exit 3, PREREQUISITE NOT MET, each needs every package's `dist`): `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:type-check-debt`. Narrowed stand-in for the first: `require()` of `dist/index.js` and `import()` of `dist/index.mjs` both load `manifestFromConfigs` / `compile`. - One red, host-bound: `check:bash32-floor`'s `--self-test` fails 7 of 179 cases on this macOS host (bash 3.2). Control: the same command in a detached worktree at `f11b5f20a2` fails the same 7 of 179. This diff touches no shell and not that script. - Artifact-roster rows that could apply, all exit 0: `check-changeset-fixed`, `check-published-list-mirrors`, `check:authz-resolver`, `check:console-injection`, `check:error-code-casing`, `check:filter-alias-parity`, `check:i18n-stale-fill`, `check:lockstep-package-count`, `check:widget-option-census`, `spec check:react-blocks`, `check:cli-examples-parity`, `check:scaffold-emission-policy`. `check:published-readme-exports` is NOT MEASURED (exit 3). `@objectstack/sdui-parser` ships no README. - `node scripts/check-issue-citations.mjs --base origin/main`: exit 0 (4 cross-repo citations). `origin/main` is still `f11b5f20a2`, an ancestor of the head, so nothing needed merging. - `check-changeset-no-major`, `check-empty-changeset` and `check-adr-0087-registration` (all `--base origin/main`): exit 0. ## Acceptance notes - **`check:sdui-lockstep` cannot see a type-union drift between the two copies**, as H2 measures. This port closes the one such drift the card named. The gate is deliberately not widened. - **The two copies still differ outside the lockstep's three comparisons.** objectui's `RegistryConfigLike` also carries `lazy?: boolean` and a longer `isContainer` docblock. objectui has `body-dialect.ts` and `provenance.ts`, which this copy lacks. `kanban-quick-add.ts`, `dashboard-widget-options.ts`, `parse.ts` and `validate.ts` differ outside the recorded region. None of that is this card's. Carrier: none. - **`scripts/gen-sdui-manifest-node.mjs` fails on macOS with the default `TMPDIR`.** Its resolve hook compares `parentURL` with a runner URL built from `os.tmpdir()` (`/var/folders/…`), but Node reports the realpath (`/private/var/folders/…`). So no bare specifier is re-anchored, and the run exits 1 with 16 `Cannot find package '@object-ui/…'` failures. With `TMPDIR` set to a realpath directory it succeeds. That is how this PR's regeneration ran. Reported to the seat, not fixed here. - **The generator's header says `check:sdui-lockstep` "holds the two copies byte-equal".** It holds the grammar region, the code set and the containment predicate only. The header is stale prose. Carrier: none. - **The pending `.changeset/console-dd3f7e1be356.md` states that the published manifest declares the html tier's elements "marked `tier: 'html'`".** For the manifest `@objectstack/console` ships, that was untrue until this PR. It is true once this lands in the same release. - **The regeneration read an existing built objectui tree at the pin** (another worktree's `.cache/objectui-dd3f7e1be356`, symlinked read-only into this worktree's `.cache/` and removed afterwards), rather than rebuilding objectui on a shared host. The control leg under H1 proves that tree reproduces the tracked artefact byte-for-byte. - **`Clause-②: no` is the claim's declaration, copied verbatim.** The changeset grades both packages `minor` (a new accepted value and a new type member; a new key on the shipped manifest), so the level axis passes under either reading of the line. **Declared narrowing — verification ran UNLOCKED.** `scripts/pm/os-verify-lock.sh` could not take the shared verify lock on this host: no usable `flock`. The shared verify lock is declared Linux-only (`flock` is util-linux, and a stock macOS does not ship it), so the command below was run directly, without the lock — a declared narrowing, not a silent one. No serialization guarantee held for this run, nor for any sibling agent in this container while it ran. pnpm --filter @objectstack/sdui-parser build pnpm --filter @objectstack/sdui-parser build && pnpm --filter @objectstack/sdui-parser exec vitest run --maxWorkers=2 && pnpm --filter @objectstack/sdui-parser typecheck pnpm lint pnpm --workspace-concurrency=2 --filter '@objectstack/lint^...' build pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 --reporter=verbose src/validate-jsx-pages.production-witness.test.ts src/validate-jsx-pages.test.ts pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 && pnpm --filter @objectstack/lint typecheck --- _Generated by [Claude Code](https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289)_ --------- Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6e3aa75 commit a093ce3

7 files changed

Lines changed: 254 additions & 15 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/sdui-parser': minor
3+
'@objectstack/console': minor
4+
---
5+
6+
The SDUI manifest now marks the html tier's intrinsic tags `tier: 'html'`, and this copy of the parser carries that marker the way the renderer's copy does.
7+
8+
objectui's copy of `packages/sdui-parser` gained the marker when its registry started declaring the intrinsic HTML tags a `kind:'html'` page may author (`h1`–`h6`, `p`, `a`, `code`, `span`, `table`, the sectioning tags and the rest of the roster; never `div`). This copy still declared only `'public' | 'internal'` and dropped the key. The repo-root `sdui.manifest.json` is serialised through this copy, and `@objectstack/console` ships it in its `dist`, so the published manifest listed those tags with no marker. A reader could not tell them from curated blocks. The port is byte-faithful to objectui at the console pin `dd3f7e1be356`:
9+
10+
- `RegistryConfigLike.tier` accepts `'html'`, the stamp objectui's `getPublicConfigs()` puts on the roster in its projection.
11+
- `ManifestComponent.tier?: 'html'` is new. `manifestFromConfigs` writes exactly `'html'` or omits the key, so every other entry serialises byte-identically to before.
12+
- `generateBlockList` counts only curated blocks in its title and lists the html tier in a section of its own.
13+
14+
**What moved in the published manifest:** `"tier": "html"` on 48 entries, and nothing else. It still has the same 107 components in the same order, and no other field on any entry changed.
15+
16+
**What did not move:** `compile()` and `validateTree()` read the manifest as a whitelist of keys and never read `tier`. So no page's verdict changes. Measured on the three shipped `kind:'html'` pages of `examples/app-showcase`: the full `compile()` result is identical against the old and the new manifest.
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/**
2+
* The manifest carries the html tier's intrinsic elements marked
3+
* `tier: 'html'` — the objectui#10735 mechanism, ported into this copy in
4+
* lockstep (objectstack#20112).
5+
*
6+
* objectui's `getPublicConfigs()` stamps the html tier's registered intrinsic
7+
* set `tier: 'html'` in its projection, and the tracked repo-root
8+
* `sdui.manifest.json` is serialised through THIS copy's `manifestFromConfigs`
9+
* (`scripts/gen-sdui-manifest-node.mjs`). Before the port this copy's
10+
* `RegistryConfigLike.tier` admitted only `'public' | 'internal'` and the
11+
* serializer dropped the key, so the manifest declared the html tier's tags
12+
* with no marker at all. `check:sdui-lockstep` compares diagnostic codes, the grammar
13+
* region and the containment predicate — never a type union — so nothing
14+
* surfaced the drift.
15+
*
16+
* The cases below are the ones objectui pins in its own
17+
* `html-tier-manifest-10735.test.ts`, one per reader of the ONE flat
18+
* `components` map:
19+
*
20+
* - `manifestFromConfigs` copies exactly `'html'` and nothing else, so every
21+
* curated entry serialises byte-identically to before the key existed;
22+
* - `compile` whitelists the html tags by key — the gate that reads the
23+
* manifest needs no change — and still refuses `<div>` because the roster
24+
* leaves it out;
25+
* - `generateBlockList` sections the html tier under its own count instead
26+
* of reading it as curated blocks;
27+
* - `generateDts` emits the tags into `JSX.IntrinsicElements`, which is the
28+
* authoring surface a `kind:'html'` page type-checks against.
29+
*/
30+
31+
import { describe, expect, it } from 'vitest';
32+
import { compile, generateBlockList, generateDts, manifestFromConfigs, type RegistryConfigLike } from '../index.js';
33+
34+
const configs: RegistryConfigLike[] = [
35+
{ type: 'flex', namespace: 'ui', isContainer: true, inputs: [{ name: 'gap', type: 'number' }, { name: 'children', type: 'slot' }] },
36+
{ type: 'my-widget', namespace: 'x', tier: 'public', inputs: [{ name: 'label', type: 'string' }] },
37+
{ type: 'studio-admin', namespace: 'app-shell', tier: 'internal', inputs: [] },
38+
{ type: 'p', namespace: 'ui', tier: 'html', inputs: [{ name: 'className', type: 'string' }, { name: 'children', type: 'slot' }] },
39+
{ type: 'a', namespace: 'ui', tier: 'html', inputs: [{ name: 'className', type: 'string' }, { name: 'href', type: 'string' }, { name: 'children', type: 'slot' }] },
40+
{ type: 'img', namespace: 'ui', tier: 'html', inputs: [{ name: 'src', type: 'string' }, { name: 'alt', type: 'string' }] },
41+
{ type: 'main', namespace: 'ui', tier: 'html', isContainer: true, inputs: [{ name: 'className', type: 'string' }, { name: 'children', type: 'slot' }] },
42+
];
43+
44+
const manifest = manifestFromConfigs(configs);
45+
/** What a consumer reads back off disk — `JSON.stringify` drops every `undefined`. */
46+
const published = JSON.parse(JSON.stringify(manifest)) as typeof manifest;
47+
48+
describe('manifestFromConfigs carries the html-tier stamp, and only that stamp', () => {
49+
it('writes `tier: html` on a stamped config', () => {
50+
for (const tag of ['p', 'a', 'img', 'main']) {
51+
expect(published.components[tag].tier, `${tag} lost its stamp`).toBe('html');
52+
}
53+
});
54+
55+
it('publishes NO `tier` key for a curated, an opted-in or an internal config — byte-identical to before', () => {
56+
for (const tag of ['flex', 'my-widget', 'studio-admin']) {
57+
expect(Object.prototype.hasOwnProperty.call(published.components[tag], 'tier'), `${tag} grew a tier key`).toBe(false);
58+
}
59+
});
60+
61+
it('keeps every other field exactly as the registration declared it', () => {
62+
expect(published.components.a.inputs.map((i) => i.name)).toEqual(['className', 'href', 'children']);
63+
expect(published.components.main.isContainer).toBe(true);
64+
expect(published.components.img.inputs.map((i) => i.name)).toEqual(['src', 'alt']);
65+
});
66+
67+
it('`publicOnly` keeps the curated tier alone — the html tier is not `public`', () => {
68+
expect(Object.keys(manifestFromConfigs(configs, { publicOnly: true }).components)).toEqual(['my-widget']);
69+
});
70+
});
71+
72+
describe('compile reads the manifest as a whitelist — the stamp changes nothing for that reader', () => {
73+
it('accepts an html-tier tag with its declared inputs and child slot', () => {
74+
const r = compile('<main className="x"><p className="lead">Hello <a href="/docs">docs</a></p><img src="/x.png" alt="x" /></main>', published);
75+
expect(r.diagnostics.filter((d) => d.severity === 'error')).toEqual([]);
76+
expect(r.ok).toBe(true);
77+
expect(r.tree?.type).toBe('main');
78+
});
79+
80+
it('refuses `<div>` — the roster leaves it out, so it is `forbidden-tag` like any unknown tag', () => {
81+
const r = compile('<main><div>hi</div></main>', published);
82+
expect(r.ok).toBe(false);
83+
expect(r.diagnostics).toEqual(
84+
expect.arrayContaining([expect.objectContaining({ severity: 'error', code: 'forbidden-tag', tag: 'div' })]),
85+
);
86+
// The lit control: the same source with `p` in place of `div` is clean.
87+
expect(compile('<main><p>hi</p></main>', published).ok).toBe(true);
88+
});
89+
90+
it('warns `not-a-container` on a void tag authored with children — the slot is declared per tag, honestly', () => {
91+
const r = compile('<main><img src="/x.png" alt="x">stray</img></main>', published);
92+
expect(r.diagnostics).toEqual(
93+
expect.arrayContaining([expect.objectContaining({ severity: 'warning', code: 'not-a-container', tag: 'img' })]),
94+
);
95+
});
96+
});
97+
98+
describe('generateBlockList sections the html tier under its own count', () => {
99+
it('counts curated blocks in the title and lists the html tier separately', () => {
100+
const list = generateBlockList(published);
101+
// Curated title counts `flex`, `my-widget`, `studio-admin` — not the four html-tier rows.
102+
expect(list).toContain('# SDUI public blocks (3)');
103+
expect(list).toContain('## html tier intrinsic elements (4)');
104+
// Rows land in the right section: the html rows come AFTER the section heading.
105+
const heading = list.indexOf('## html tier intrinsic elements');
106+
for (const tag of ['p', 'a', 'img', 'main']) {
107+
expect(list.indexOf(`| \`${tag}\` |`), `${tag} row is missing or above the html section`).toBeGreaterThan(heading);
108+
}
109+
for (const tag of ['flex', 'my-widget']) {
110+
expect(list.indexOf(`| \`${tag}\` |`), `${tag} row landed in the html section`).toBeLessThan(heading);
111+
}
112+
});
113+
114+
it('emits no html section for a manifest without html-tier entries — the curated list is unchanged', () => {
115+
const curatedOnly = manifestFromConfigs(configs.filter((c) => c.tier !== 'html'));
116+
const list = generateBlockList(curatedOnly);
117+
expect(list).toContain('# SDUI public blocks (3)');
118+
expect(list).not.toContain('html tier intrinsic elements');
119+
});
120+
});
121+
122+
describe('generateDts publishes the html tier into JSX.IntrinsicElements', () => {
123+
it('emits an interface and an intrinsic entry per html-tier tag, beside the curated ones', () => {
124+
const dts = generateDts(published);
125+
expect(dts).toContain('"p": PProps;');
126+
expect(dts).toContain('"a": AProps;');
127+
expect(dts).toContain('export interface AProps extends SduiBaseProps {\n className?: string;\n href?: string;\n}');
128+
expect(dts).toContain('"flex": FlexProps;');
129+
});
130+
});

‎packages/sdui-parser/src/codegen.ts‎

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -165,23 +165,40 @@ export function propsName(type: string): string {
165165
/**
166166
* Generate the human-facing PUBLIC block list (the curated "清单") from a
167167
* manifest — a Markdown table. Derived, never hand-maintained (ADR-0046).
168+
*
169+
* The html tier's intrinsic elements (`tier: 'html'`, objectui#10735) are
170+
* listed in a section of their own, under their own count: they are in the
171+
* manifest because a `kind:'html'` page may author them, not because they are
172+
* curated blocks, and a single table would read them as the latter.
168173
*/
169174
export function generateBlockList(manifest: Manifest): string {
170-
const rows = Object.values(manifest.components)
171-
.sort((a, b) => a.type.localeCompare(b.type))
172-
.map((c) => {
173-
const req = c.inputs.filter((i) => i.required).map((i) => i.name);
174-
const binds = c.inputs.filter((i) => i.binding).map((i) => `${i.name}:${i.binding}`);
175-
return `| \`${c.type}\` | ${c.namespace ?? '—'} | ${c.isContainer ? '✓' : ''} | ${req.join(', ') || '—'} | ${binds.join(', ') || '—'} |`;
176-
});
175+
const sorted = Object.values(manifest.components).sort((a, b) => a.type.localeCompare(b.type));
176+
const curated = sorted.filter((c) => c.tier !== 'html');
177+
const htmlTier = sorted.filter((c) => c.tier === 'html');
178+
const row = (c: ManifestComponent): string => {
179+
const req = c.inputs.filter((i) => i.required).map((i) => i.name);
180+
const binds = c.inputs.filter((i) => i.binding).map((i) => `${i.name}:${i.binding}`);
181+
return `| \`${c.type}\` | ${c.namespace ?? '—'} | ${c.isContainer ? '✓' : ''} | ${req.join(', ') || '—'} | ${binds.join(', ') || '—'} |`;
182+
};
183+
const header = ['| block | plugin | container | required props | bindings |', '|---|---|---|---|---|'];
177184
return [
178-
`# SDUI public blocks (${Object.keys(manifest.components).length})`,
185+
`# SDUI public blocks (${curated.length})`,
179186
'',
180187
'> Auto-generated from the registry `tier:\'public\'` set (ADR-0080). Do not edit by hand.',
181188
'',
182-
'| block | plugin | container | required props | bindings |',
183-
'|---|---|---|---|---|',
184-
...rows,
189+
...header,
190+
...curated.map(row),
191+
...(htmlTier.length
192+
? [
193+
'',
194+
`## html tier intrinsic elements (${htmlTier.length})`,
195+
'',
196+
"> 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.",
197+
'',
198+
...header,
199+
...htmlTier.map(row),
200+
]
201+
: []),
185202
'',
186203
].join('\n');
187204
}

‎packages/sdui-parser/src/index.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,14 @@ export interface RegistryConfigLike {
6868
type: string;
6969
namespace?: string;
7070
isContainer?: boolean;
71-
/** ADR-0080 contract tier — only 'public' configs form the AI/contract surface. */
72-
tier?: 'public' | 'internal';
71+
/**
72+
* ADR-0080 contract tier — only 'public' configs form the AI/contract
73+
* surface under `publicOnly`. `'html'` is the projection-only stamp
74+
* `ComponentRegistry.getPublicConfigs()` puts on the html tier's intrinsic
75+
* elements (objectui#10735); it is the one value {@link manifestFromConfigs}
76+
* carries into `ManifestComponent.tier`.
77+
*/
78+
tier?: 'public' | 'internal' | 'html';
7379
label?: string;
7480
category?: string;
7581
inputs?: Array<{
@@ -131,6 +137,11 @@ export function manifestFromConfigs(
131137
type: c.type,
132138
namespace: c.namespace,
133139
isContainer: c.isContainer,
140+
// The html tier's stamp, and ONLY that stamp (objectui#10735): a
141+
// registration's `'public'` / `'internal'` is registry mechanics the
142+
// manifest never carried, and `undefined` is dropped by `JSON.stringify`,
143+
// so every curated entry serialises exactly as before this key existed.
144+
tier: c.tier === 'html' ? 'html' : undefined,
134145
inputs: (c.inputs ?? []).map((i) => ({
135146
name: i.name,
136147
type: canonicalizeInputType(i.type),

‎packages/sdui-parser/src/types.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,23 @@ export interface ManifestComponent {
172172
* ⛔ Not "accepts children" — this tier's containment check does not read it.
173173
*/
174174
isContainer?: boolean;
175+
/**
176+
* The authoring tier this entry belongs to (objectui#10735).
177+
*
178+
* Absent = the curated PUBLIC tier (ADR-0080 §4), the JSON-surface
179+
* AI-authoring vocabulary. `'html'` = one of the intrinsic HTML elements a
180+
* `kind:'html'` page may author (ADR-0081 §2), declared by the registry's
181+
* `HTML_TIER_INTRINSICS` roster and carried here so the ONE flat `components`
182+
* map can serve both of its readers: a whitelist reader (`compile`, the
183+
* objectstack gate) takes every key and needs no change; a reader that means
184+
* the curated vocabulary — the `kind:'react'` scope, the block list, a census
185+
* of curated blocks — filters this key out.
186+
*
187+
* ⛔ Not the registration's `tier` copied through. `manifestFromConfigs`
188+
* writes exactly `'html'` or nothing, so every entry published before this
189+
* key existed serialises byte-identically.
190+
*/
191+
tier?: 'html';
175192
}
176193

177194
export interface Manifest {

‎scripts/sdui-manifest.record.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"modulesRoot": ".cache/objectui-dd3f7e1be356/apps/console",
1717
"objectuiWorkspaceVersion": "17.6.0",
1818
"generator": "scripts/gen-sdui-manifest-node.mjs",
19-
"generatedAt": "2026-09-28",
20-
"sha256": "4073897dcdc1cd68966f12f1868d37c2c25a750ea044760ce58e181fce6f21bc",
19+
"generatedAt": "2026-09-29",
20+
"sha256": "d0666ac585db8fa974471d9ebf33b906be776d77a170e7e5ecac774095d7b26a",
2121
"components": 107
2222
}

0 commit comments

Comments
 (0)