From 9a77e0041503124b65cc2c5b072ca5e0b43283f7 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:46:57 +0800 Subject: [PATCH 1/3] feat(sdui-parser): port the html-tier `tier` stamp from objectui's lockstep copy objectui's sdui-parser, at the console pin dd3f7e1be356, admits `tier: 'html'` on RegistryConfigLike, declares ManifestComponent.tier, has manifestFromConfigs write exactly `'html'` or omit the key, and has generateBlockList section the html tier under its own count. This copy still declared `'public' | 'internal'` and dropped the key, so the tracked manifest serialised through it carried the html tier's intrinsic tags with no marker. The three hunks are copied byte-for-byte from the pin; the tests are objectui's own cases for the same mechanism. Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude --- .../src/__tests__/html-tier-manifest.test.ts | 130 ++++++++++++++++++ packages/sdui-parser/src/codegen.ts | 39 ++++-- packages/sdui-parser/src/index.ts | 15 +- packages/sdui-parser/src/types.ts | 17 +++ 4 files changed, 188 insertions(+), 13 deletions(-) create mode 100644 packages/sdui-parser/src/__tests__/html-tier-manifest.test.ts diff --git a/packages/sdui-parser/src/__tests__/html-tier-manifest.test.ts b/packages/sdui-parser/src/__tests__/html-tier-manifest.test.ts new file mode 100644 index 00000000000..79648f7574b --- /dev/null +++ b/packages/sdui-parser/src/__tests__/html-tier-manifest.test.ts @@ -0,0 +1,130 @@ +/** + * The manifest carries the html tier's intrinsic elements marked + * `tier: 'html'` — the objectui#10735 mechanism, ported into this copy in + * lockstep (objectstack#20112). + * + * objectui's `getPublicConfigs()` stamps the html tier's registered intrinsic + * set `tier: 'html'` in its projection, and the tracked repo-root + * `sdui.manifest.json` is serialised through THIS copy's `manifestFromConfigs` + * (`scripts/gen-sdui-manifest-node.mjs`). Before the port this copy's + * `RegistryConfigLike.tier` admitted only `'public' | 'internal'` and the + * serializer dropped the key, so the manifest declared the 47 tags with no + * marker at all. `check:sdui-lockstep` compares diagnostic codes, the grammar + * region and the containment predicate — never a type union — so nothing + * surfaced the drift. + * + * The cases below are the ones objectui pins in its own + * `html-tier-manifest-10735.test.ts`, one per reader of the ONE flat + * `components` map: + * + * - `manifestFromConfigs` copies exactly `'html'` and nothing else, so every + * curated entry serialises byte-identically to before the key existed; + * - `compile` whitelists the html tags by key — the gate that reads the + * manifest needs no change — and still refuses `
` because the roster + * leaves it out; + * - `generateBlockList` sections the html tier under its own count instead + * of reading it as curated blocks; + * - `generateDts` emits the tags into `JSX.IntrinsicElements`, which is the + * authoring surface a `kind:'html'` page type-checks against. + */ + +import { describe, expect, it } from 'vitest'; +import { compile, generateBlockList, generateDts, manifestFromConfigs, type RegistryConfigLike } from '../index.js'; + +const configs: RegistryConfigLike[] = [ + { type: 'flex', namespace: 'ui', isContainer: true, inputs: [{ name: 'gap', type: 'number' }, { name: 'children', type: 'slot' }] }, + { type: 'my-widget', namespace: 'x', tier: 'public', inputs: [{ name: 'label', type: 'string' }] }, + { type: 'studio-admin', namespace: 'app-shell', tier: 'internal', inputs: [] }, + { type: 'p', namespace: 'ui', tier: 'html', inputs: [{ name: 'className', type: 'string' }, { name: 'children', type: 'slot' }] }, + { type: 'a', namespace: 'ui', tier: 'html', inputs: [{ name: 'className', type: 'string' }, { name: 'href', type: 'string' }, { name: 'children', type: 'slot' }] }, + { type: 'img', namespace: 'ui', tier: 'html', inputs: [{ name: 'src', type: 'string' }, { name: 'alt', type: 'string' }] }, + { type: 'main', namespace: 'ui', tier: 'html', isContainer: true, inputs: [{ name: 'className', type: 'string' }, { name: 'children', type: 'slot' }] }, +]; + +const manifest = manifestFromConfigs(configs); +/** What a consumer reads back off disk — `JSON.stringify` drops every `undefined`. */ +const published = JSON.parse(JSON.stringify(manifest)) as typeof manifest; + +describe('manifestFromConfigs carries the html-tier stamp, and only that stamp', () => { + it('writes `tier: html` on a stamped config', () => { + for (const tag of ['p', 'a', 'img', 'main']) { + expect(published.components[tag].tier, `${tag} lost its stamp`).toBe('html'); + } + }); + + it('publishes NO `tier` key for a curated, an opted-in or an internal config — byte-identical to before', () => { + for (const tag of ['flex', 'my-widget', 'studio-admin']) { + expect(Object.prototype.hasOwnProperty.call(published.components[tag], 'tier'), `${tag} grew a tier key`).toBe(false); + } + }); + + it('keeps every other field exactly as the registration declared it', () => { + expect(published.components.a.inputs.map((i) => i.name)).toEqual(['className', 'href', 'children']); + expect(published.components.main.isContainer).toBe(true); + expect(published.components.img.inputs.map((i) => i.name)).toEqual(['src', 'alt']); + }); + + it('`publicOnly` keeps the curated tier alone — the html tier is not `public`', () => { + expect(Object.keys(manifestFromConfigs(configs, { publicOnly: true }).components)).toEqual(['my-widget']); + }); +}); + +describe('compile reads the manifest as a whitelist — the stamp changes nothing for that reader', () => { + it('accepts an html-tier tag with its declared inputs and child slot', () => { + const r = compile('

Hello docs

x
', published); + expect(r.diagnostics.filter((d) => d.severity === 'error')).toEqual([]); + expect(r.ok).toBe(true); + expect(r.tree?.type).toBe('main'); + }); + + it('refuses `
` — the roster leaves it out, so it is `forbidden-tag` like any unknown tag', () => { + const r = compile('
hi
', published); + expect(r.ok).toBe(false); + expect(r.diagnostics).toEqual( + expect.arrayContaining([expect.objectContaining({ severity: 'error', code: 'forbidden-tag', tag: 'div' })]), + ); + // The lit control: the same source with `p` in place of `div` is clean. + expect(compile('

hi

', published).ok).toBe(true); + }); + + it('warns `not-a-container` on a void tag authored with children — the slot is declared per tag, honestly', () => { + const r = compile('
xstray
', published); + expect(r.diagnostics).toEqual( + expect.arrayContaining([expect.objectContaining({ severity: 'warning', code: 'not-a-container', tag: 'img' })]), + ); + }); +}); + +describe('generateBlockList sections the html tier under its own count', () => { + it('counts curated blocks in the title and lists the html tier separately', () => { + const list = generateBlockList(published); + // Curated title counts `flex`, `my-widget`, `studio-admin` — not the four html-tier rows. + expect(list).toContain('# SDUI public blocks (3)'); + expect(list).toContain('## html tier intrinsic elements (4)'); + // Rows land in the right section: the html rows come AFTER the section heading. + const heading = list.indexOf('## html tier intrinsic elements'); + for (const tag of ['p', 'a', 'img', 'main']) { + expect(list.indexOf(`| \`${tag}\` |`), `${tag} row is missing or above the html section`).toBeGreaterThan(heading); + } + for (const tag of ['flex', 'my-widget']) { + expect(list.indexOf(`| \`${tag}\` |`), `${tag} row landed in the html section`).toBeLessThan(heading); + } + }); + + it('emits no html section for a manifest without html-tier entries — the curated list is unchanged', () => { + const curatedOnly = manifestFromConfigs(configs.filter((c) => c.tier !== 'html')); + const list = generateBlockList(curatedOnly); + expect(list).toContain('# SDUI public blocks (3)'); + expect(list).not.toContain('html tier intrinsic elements'); + }); +}); + +describe('generateDts publishes the html tier into JSX.IntrinsicElements', () => { + it('emits an interface and an intrinsic entry per html-tier tag, beside the curated ones', () => { + const dts = generateDts(published); + expect(dts).toContain('"p": PProps;'); + expect(dts).toContain('"a": AProps;'); + expect(dts).toContain('export interface AProps extends SduiBaseProps {\n className?: string;\n href?: string;\n}'); + expect(dts).toContain('"flex": FlexProps;'); + }); +}); diff --git a/packages/sdui-parser/src/codegen.ts b/packages/sdui-parser/src/codegen.ts index 76a7d654f1f..895d403f5a8 100644 --- a/packages/sdui-parser/src/codegen.ts +++ b/packages/sdui-parser/src/codegen.ts @@ -165,23 +165,40 @@ export function propsName(type: string): string { /** * Generate the human-facing PUBLIC block list (the curated "清单") from a * manifest — a Markdown table. Derived, never hand-maintained (ADR-0046). + * + * The html tier's intrinsic elements (`tier: 'html'`, objectui#10735) are + * listed in a section of their own, under their own count: they are in the + * manifest because a `kind:'html'` page may author them, not because they are + * curated blocks, and a single table would read them as the latter. */ export function generateBlockList(manifest: Manifest): string { - const rows = Object.values(manifest.components) - .sort((a, b) => a.type.localeCompare(b.type)) - .map((c) => { - const req = c.inputs.filter((i) => i.required).map((i) => i.name); - const binds = c.inputs.filter((i) => i.binding).map((i) => `${i.name}:${i.binding}`); - return `| \`${c.type}\` | ${c.namespace ?? '—'} | ${c.isContainer ? '✓' : ''} | ${req.join(', ') || '—'} | ${binds.join(', ') || '—'} |`; - }); + const sorted = Object.values(manifest.components).sort((a, b) => a.type.localeCompare(b.type)); + const curated = sorted.filter((c) => c.tier !== 'html'); + const htmlTier = sorted.filter((c) => c.tier === 'html'); + const row = (c: ManifestComponent): string => { + const req = c.inputs.filter((i) => i.required).map((i) => i.name); + const binds = c.inputs.filter((i) => i.binding).map((i) => `${i.name}:${i.binding}`); + return `| \`${c.type}\` | ${c.namespace ?? '—'} | ${c.isContainer ? '✓' : ''} | ${req.join(', ') || '—'} | ${binds.join(', ') || '—'} |`; + }; + const header = ['| block | plugin | container | required props | bindings |', '|---|---|---|---|---|']; return [ - `# SDUI public blocks (${Object.keys(manifest.components).length})`, + `# SDUI public blocks (${curated.length})`, '', '> Auto-generated from the registry `tier:\'public\'` set (ADR-0080). Do not edit by hand.', '', - '| block | plugin | container | required props | bindings |', - '|---|---|---|---|---|', - ...rows, + ...header, + ...curated.map(row), + ...(htmlTier.length + ? [ + '', + `## html tier intrinsic elements (${htmlTier.length})`, + '', + "> 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.", + '', + ...header, + ...htmlTier.map(row), + ] + : []), '', ].join('\n'); } diff --git a/packages/sdui-parser/src/index.ts b/packages/sdui-parser/src/index.ts index 2ea35795b9c..78bc31e5edd 100644 --- a/packages/sdui-parser/src/index.ts +++ b/packages/sdui-parser/src/index.ts @@ -68,8 +68,14 @@ export interface RegistryConfigLike { type: string; namespace?: string; isContainer?: boolean; - /** ADR-0080 contract tier — only 'public' configs form the AI/contract surface. */ - tier?: 'public' | 'internal'; + /** + * ADR-0080 contract tier — only 'public' configs form the AI/contract + * surface under `publicOnly`. `'html'` is the projection-only stamp + * `ComponentRegistry.getPublicConfigs()` puts on the html tier's intrinsic + * elements (objectui#10735); it is the one value {@link manifestFromConfigs} + * carries into `ManifestComponent.tier`. + */ + tier?: 'public' | 'internal' | 'html'; label?: string; category?: string; inputs?: Array<{ @@ -131,6 +137,11 @@ export function manifestFromConfigs( type: c.type, namespace: c.namespace, isContainer: c.isContainer, + // The html tier's stamp, and ONLY that stamp (objectui#10735): a + // registration's `'public'` / `'internal'` is registry mechanics the + // manifest never carried, and `undefined` is dropped by `JSON.stringify`, + // so every curated entry serialises exactly as before this key existed. + tier: c.tier === 'html' ? 'html' : undefined, inputs: (c.inputs ?? []).map((i) => ({ name: i.name, type: canonicalizeInputType(i.type), diff --git a/packages/sdui-parser/src/types.ts b/packages/sdui-parser/src/types.ts index 97739b9dd7a..cd37a78c654 100644 --- a/packages/sdui-parser/src/types.ts +++ b/packages/sdui-parser/src/types.ts @@ -172,6 +172,23 @@ export interface ManifestComponent { * ⛔ Not "accepts children" — this tier's containment check does not read it. */ isContainer?: boolean; + /** + * The authoring tier this entry belongs to (objectui#10735). + * + * Absent = the curated PUBLIC tier (ADR-0080 §4), the JSON-surface + * AI-authoring vocabulary. `'html'` = one of the intrinsic HTML elements a + * `kind:'html'` page may author (ADR-0081 §2), declared by the registry's + * `HTML_TIER_INTRINSICS` roster and carried here so the ONE flat `components` + * map can serve both of its readers: a whitelist reader (`compile`, the + * objectstack gate) takes every key and needs no change; a reader that means + * the curated vocabulary — the `kind:'react'` scope, the block list, a census + * of curated blocks — filters this key out. + * + * ⛔ Not the registration's `tier` copied through. `manifestFromConfigs` + * writes exactly `'html'` or nothing, so every entry published before this + * key existed serialises byte-identically. + */ + tier?: 'html'; } export interface Manifest { From 69836cebbcf0b351fe7084a830caffe2e73bcf24 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:51:41 +0800 Subject: [PATCH 2/3] chore(sdui-manifest): regenerate at the pin so the html tier's 48 intrinsic tags carry `tier: 'html'` Regenerated with scripts/gen-sdui-manifest-node.mjs over objectui's built tree at dd3f7e1be356, serialised through the ported manifestFromConfigs. Same 107 components in the same order; the only change is `"tier": "html"` on the 48 entries objectui's HTML_TIER_INTRINSICS roster stamps. The record is re-recorded (sha256, date). The unported serializer over the same tree reproduced the old artefact byte-for-byte (sha256 4073897dcdc1), so the tree is the one the tracked file was cut from. Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude --- .../src/__tests__/html-tier-manifest.test.ts | 4 +- scripts/sdui-manifest.record.json | 4 +- sdui.manifest.json | 48 +++++++++++++++++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/packages/sdui-parser/src/__tests__/html-tier-manifest.test.ts b/packages/sdui-parser/src/__tests__/html-tier-manifest.test.ts index 79648f7574b..5992f4bc336 100644 --- a/packages/sdui-parser/src/__tests__/html-tier-manifest.test.ts +++ b/packages/sdui-parser/src/__tests__/html-tier-manifest.test.ts @@ -8,8 +8,8 @@ * `sdui.manifest.json` is serialised through THIS copy's `manifestFromConfigs` * (`scripts/gen-sdui-manifest-node.mjs`). Before the port this copy's * `RegistryConfigLike.tier` admitted only `'public' | 'internal'` and the - * serializer dropped the key, so the manifest declared the 47 tags with no - * marker at all. `check:sdui-lockstep` compares diagnostic codes, the grammar + * serializer dropped the key, so the manifest declared the html tier's tags + * with no marker at all. `check:sdui-lockstep` compares diagnostic codes, the grammar * region and the containment predicate — never a type union — so nothing * surfaced the drift. * diff --git a/scripts/sdui-manifest.record.json b/scripts/sdui-manifest.record.json index e66fbb6331f..61a72c008df 100644 --- a/scripts/sdui-manifest.record.json +++ b/scripts/sdui-manifest.record.json @@ -16,7 +16,7 @@ "modulesRoot": ".cache/objectui-dd3f7e1be356/apps/console", "objectuiWorkspaceVersion": "17.6.0", "generator": "scripts/gen-sdui-manifest-node.mjs", - "generatedAt": "2026-09-28", - "sha256": "4073897dcdc1cd68966f12f1868d37c2c25a750ea044760ce58e181fce6f21bc", + "generatedAt": "2026-09-29", + "sha256": "d0666ac585db8fa974471d9ebf33b906be776d77a170e7e5ecac774095d7b26a", "components": 107 } diff --git a/sdui.manifest.json b/sdui.manifest.json index 4365e978a0c..05053a86d42 100644 --- a/sdui.manifest.json +++ b/sdui.manifest.json @@ -2667,6 +2667,7 @@ "h1": { "type": "h1", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2681,6 +2682,7 @@ "h2": { "type": "h2", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2695,6 +2697,7 @@ "h3": { "type": "h3", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2709,6 +2712,7 @@ "h4": { "type": "h4", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2723,6 +2727,7 @@ "h5": { "type": "h5", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2737,6 +2742,7 @@ "h6": { "type": "h6", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2751,6 +2757,7 @@ "p": { "type": "p", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2765,6 +2772,7 @@ "a": { "type": "a", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2795,6 +2803,7 @@ "blockquote": { "type": "blockquote", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2813,6 +2822,7 @@ "pre": { "type": "pre", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2827,6 +2837,7 @@ "code": { "type": "code", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2841,6 +2852,7 @@ "strong": { "type": "strong", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2855,6 +2867,7 @@ "em": { "type": "em", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2869,6 +2882,7 @@ "b": { "type": "b", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2883,6 +2897,7 @@ "i": { "type": "i", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2897,6 +2912,7 @@ "u": { "type": "u", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2911,6 +2927,7 @@ "small": { "type": "small", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2925,6 +2942,7 @@ "mark": { "type": "mark", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2939,6 +2957,7 @@ "sub": { "type": "sub", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2953,6 +2972,7 @@ "sup": { "type": "sup", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2967,6 +2987,7 @@ "del": { "type": "del", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2981,6 +3002,7 @@ "ins": { "type": "ins", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -2995,6 +3017,7 @@ "abbr": { "type": "abbr", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -3013,6 +3036,7 @@ "ul": { "type": "ul", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -3027,6 +3051,7 @@ "ol": { "type": "ol", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -3041,6 +3066,7 @@ "li": { "type": "li", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -3055,6 +3081,7 @@ "dl": { "type": "dl", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -3069,6 +3096,7 @@ "dt": { "type": "dt", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -3083,6 +3111,7 @@ "dd": { "type": "dd", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -3097,6 +3126,7 @@ "figure": { "type": "figure", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -3111,6 +3141,7 @@ "figcaption": { "type": "figcaption", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -3125,6 +3156,7 @@ "img": { "type": "img", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -3155,6 +3187,7 @@ "hr": { "type": "hr", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -3165,6 +3198,7 @@ "br": { "type": "br", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -3175,6 +3209,7 @@ "time": { "type": "time", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -3193,6 +3228,7 @@ "address": { "type": "address", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -3207,6 +3243,7 @@ "cite": { "type": "cite", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -3221,6 +3258,7 @@ "q": { "type": "q", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -3239,6 +3277,7 @@ "span": { "type": "span", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "className", @@ -3254,6 +3293,7 @@ "table": { "type": "table", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "caption", @@ -3282,6 +3322,7 @@ "label": { "type": "label", "namespace": "ui", + "tier": "html", "inputs": [ { "name": "text", @@ -3298,6 +3339,7 @@ "type": "aside", "namespace": "ui", "isContainer": true, + "tier": "html", "inputs": [ { "name": "className", @@ -3313,6 +3355,7 @@ "type": "main", "namespace": "ui", "isContainer": true, + "tier": "html", "inputs": [ { "name": "className", @@ -3328,6 +3371,7 @@ "type": "header", "namespace": "ui", "isContainer": true, + "tier": "html", "inputs": [ { "name": "className", @@ -3343,6 +3387,7 @@ "type": "nav", "namespace": "ui", "isContainer": true, + "tier": "html", "inputs": [ { "name": "className", @@ -3358,6 +3403,7 @@ "type": "footer", "namespace": "ui", "isContainer": true, + "tier": "html", "inputs": [ { "name": "className", @@ -3373,6 +3419,7 @@ "type": "section", "namespace": "ui", "isContainer": true, + "tier": "html", "inputs": [ { "name": "className", @@ -3388,6 +3435,7 @@ "type": "article", "namespace": "ui", "isContainer": true, + "tier": "html", "inputs": [ { "name": "className", From ffbb0c48023cb993634c6b4a0b75cf4c0c4bfdaf Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:52:26 +0800 Subject: [PATCH 3/3] chore(changeset): sdui-parser and console minor for the html-tier stamp The port widens RegistryConfigLike.tier and adds ManifestComponent.tier, and the console's shipped manifest gains the key on 48 entries: an additive widening of two published surfaces. Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude --- .changeset/20112-sdui-parser-html-tier-stamp.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .changeset/20112-sdui-parser-html-tier-stamp.md diff --git a/.changeset/20112-sdui-parser-html-tier-stamp.md b/.changeset/20112-sdui-parser-html-tier-stamp.md new file mode 100644 index 00000000000..10b4a113947 --- /dev/null +++ b/.changeset/20112-sdui-parser-html-tier-stamp.md @@ -0,0 +1,16 @@ +--- +'@objectstack/sdui-parser': minor +'@objectstack/console': minor +--- + +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. + +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`: + +- `RegistryConfigLike.tier` accepts `'html'`, the stamp objectui's `getPublicConfigs()` puts on the roster in its projection. +- `ManifestComponent.tier?: 'html'` is new. `manifestFromConfigs` writes exactly `'html'` or omits the key, so every other entry serialises byte-identically to before. +- `generateBlockList` counts only curated blocks in its title and lists the html tier in a section of its own. + +**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. + +**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.