Repository navigation
feat(components,types)!: retire the raw-HTML html node type (objectui#13449) - #13475
Conversation
…ui#13449) The `html` renderer wrote `schema.html` into the page through dangerouslySetInnerHTML with no sanitiser, and @objectstack/spec declares no html component. Remove the registration, `HtmlSchema` and its zod arm (kept as a by-name refusal via retiredNodeType), the PUBLIC_BLOCKS entry, the docs page and the catalog example; regenerate the CLI's known types. Claude-Session: https://claude.ai/code/session_01432GBvPzfZ6CsLanp9xzkZ Co-authored-by: Claude <noreply@anthropic.com>
…13449) Lower the CLI's namespaced refused-at-type pin by `ui:html`, pin the strict face's by-name refusal of `html`, and re-derive the DOM-leak sweep's docblock counts without the `ui:html` target. Claude-Session: https://claude.ai/code/session_01432GBvPzfZ6CsLanp9xzkZ Co-authored-by: Claude <noreply@anthropic.com>
…ral (objectui#13449) Add the breaking (minor-marked) changeset with the FROM/TO migration, and declare the `html` FIELD type taught by content/docs/fields/rich-text.mdx as a field-vocabulary exemption in check:doc-types: it passed only because the retired node type registered the same bare key. Claude-Session: https://claude.ai/code/session_01432GBvPzfZ6CsLanp9xzkZ Co-authored-by: Claude <noreply@anthropic.com>
…ctui#13449) Claude-Session: https://claude.ai/code/session_01432GBvPzfZ6CsLanp9xzkZ Co-authored-by: Claude <noreply@anthropic.com>
…gistry map declares (objectui#13449) Claude-Session: https://claude.ai/code/session_01432GBvPzfZ6CsLanp9xzkZ Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs: card objectui#13449 (body and all three comments, the triage grade 6385669346, the claim 6386906709 and the os-dev-report 6387990912), PR objectui#13475 (body, 31-file list, net diff against ① Derived judgments
② Semver level
③ Boundary flagsEach flag in the os-dev-report (comment 6387990912), answered:
Implemented-by: VERDICT: FAIL — one finding, in ②: a breaking narrowing declared without its Generated by Claude Code |
…13449) Claude-Session: https://claude.ai/code/session_01432GBvPzfZ6CsLanp9xzkZ Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs: card objectui#13449 (body and all four comments: the triage grade 6385669346, the amended claim 6386906709, the os-dev-reports 6387990912 and 6393077333), PR objectui#13475 (body, 31-file list, net diff against The delta since the previous record
① Derived judgmentsUnchanged from comment 6393057967, carried by reference — the diff outside the changeset is byte-identical. In one line each: the zod face narrows only, through the unexported ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS — the one finding of the previous record is closed on all three carriers; everything else is the same bytes and the same judgments. Landing waits only on the in-progress check-runs named above turning Generated by Claude Code |
席位核对ACCEPT —
Generated by Claude Code |
Fixes #13449
Clause-②: no (narrowing)
Metadata-protocol: narrowed
What this does
Retires the raw-HTML
htmlnode type, as the triage grade routed it (comment 6385669346: "Route: retire, as recommended"). Its renderer in@object-ui/componentswrote the authoredhtmlstring into adivthroughdangerouslySetInnerHTMLwith no sanitiser, and@objectstack/specdeclares nohtmlcomponent. It is removed, not sanitised; no deprecation alias. Breaking for a page that still names it, so the changeset isminorwith a FROM / TO migration (this repository's version policy forbidsmajor).@object-ui/components: thehtml.tsxregistration (ui:htmlplus the bare key) and its import inrenderers/basic/index.tsare gone.@object-ui/core:htmlleavesPUBLIC_BLOCKS(it sat there as the curated tier's "escape hatch"), sogetPublicConfigs()and the publishedsdui.manifest.jsonno longer carry it.@object-ui/types:HtmlSchemaleaves the package entry,DataDisplaySchemaand theSchemaRegistrymap (soComponentTypeno longer offers'html'). The zod arm becomesRetiredHtmlNodeSchema, aretiredNodeType('html', …)refusal insideDataDisplaySchema, not exported: atype: "html"node is refused BY NAME attype, with the remedy in the message. This is the shape the maintainer ruled for the barekanbankey (objectui#8802) and that objectui#12954 reused forcloud:plan-status.@object-ui/cli:known-schema-types.tsregenerated (node scripts/regenerate-known-schema-types.mjs; its--checkanswers OK), soobjectui checkno longer countshtml/ui:html.content/docs/components/basic/html.mdx(it said "Render raw HTML content safely", which was false), itsmeta.jsonrow and its index link are gone;guide/react-pages.mdsays there is no raw-HTML block on either page tier. The schema-catalog examplecomponents-basic-html/basic-html.jsonis gone and the catalog index regenerated (scripts/regenerate-catalog-index.py;--checkOK).@object-ui/fields:htmlstays inFIELD_TYPES_SKIP_FALLBACK, moved from the "bare-name collision" group to a retired-key note. No behaviour change: the field editor still stands down from the bare key, so a stale node draws the unknown-component panel, never a rich-text editor.The
htmlFIELD type (HtmlFieldMetadata, drawn byfield:html, value displayed through the rehype-sanitize path objectui#13431 landed) is a different layer and is untouched.Reach, measured before and after (not a source-grep claim)
A temporary probe rendered both doors through the real
SchemaRendererand the full console registration graph (@object-ui/componentsplusapps/console/src/register-plugins), with a payload of animgelement carrying anonerrorattribute plus a bold element:a3c0da9{ "type": "html", "html": PAYLOAD }imgwith itsonerrorattribute is in the DOMkind: 'html'page whose source writes anhtmltag carrying anhtmlattributeimgwith itsonerrorattribute is in the DOMforbidden-tag,unknown-component); the page draws its compile reportgetPublicConfigs()nameshtmlThe second row is worse than the card stated: the tier
guide/react-pages.mddocuments as "Untrusted OK", parsed and never executed, could inject raw markup, because the page compiler whitelists every registered type. The probe was replaced by a committed pin (below) and deleted.Dispatch hypotheses, answered
HtmlSchemaand the zod literal sat where the claim read them ata3c0da9.content/docs/fields/rich-text.mdxnamestype: 'html'twice, and both are the FIELD type (HtmlFieldMetadatain thetsblock; "HTML Mode" of the rich-text widget in prose). Both stay.check:doc-typeshad passed the code-block one only because the retired node registered the same bare key, so it now carries a field-vocabulary exemption inscripts/check-doc-component-types.mjs, worded like therichtextrow beside it.apps/console/distis not tracked, so nothing in-tree to regenerate for the manifest; the two tracked generated files that named the type are regenerated by their own generators (above). objectstack's trackedsdui.manifest.jsonstill nameshtml: it is generated from objectui's pinned build and its staleness gate forces a regenerate on the next.objectui-shabump (see Acceptance notes).git grepfor a quotedhtmlover every tracked file, read hit by hit: apart from what this PR removes (the docs page, the catalog example, thedata-display-examples.jsonentry, tests), every hit is the field type, akind: 'html'page, a report export format, a code-editor language, or an HTML tag name.File surface beyond the claim's list
The claim named
renderers/basic/**, the two types files, the docs and tests naming the component, and the changeset. The dispatch's suggested route asked for every other list naminghtmlas a component type; those are:packages/core/src/registry/public-blocks.ts,packages/types/src/{registry.ts,index.ts,zod/index.zod.ts,zod/README.md},packages/types/examples/data-display-examples.json,packages/cli/src/utils/known-schema-types.ts(generated),packages/fields/src/index.tsx(regroup and comment only), the catalog example and its generated index,scripts/check-doc-component-types.mjs(one exemption),content/docs/guide/react-pages.md, and count / list pins inapps/console,packages/app-shell,packages/cli,packages/fields,packages/types. No governed surface:node scripts/check-governed-queue-guard.mjs --testover all 31 paths answers "NOT GOVERNED — 31 path(s) checked against 6 governed surface(s); none matched."Pins (each red on the pre-fix implementation, see Ablation)
apps/console/src/__tests__/html-node-retired-13449.test.tsx— thekind: 'html'page refuses the tag and renders no authored markup; the JSON node renders no authored markup; neither registry key survives whilefield:htmldoes; CONTROL: the same page without the tag renders itsptext.packages/types/src/__tests__/html-node-retired-13449.test.ts— compile-time: nohtmlmember inDataDisplaySchema, no'html'inComponentType, with non-vacuity siblings; runtime: acustomissue attypenaming the retired type and both replacements; CONTROLS: a sibling node still parses, an unknown literal takes the generic path without the retirement text, and a form with anhtmlFIELD still validates.packages/cli/src/__tests__/registered-types-validate-ratchet-10859.test.ts— the strict face (objectui validate) refuseshtmlby name; the namespaced refused-at-type pin falls 307 to 306 (ui:htmlleft the registry).packages/components/src/__tests__/basic-renderers.test.tsx,packages/fields/src/__tests__/registry-collision.test.ts— the old "renders HTML" / "bare html is the display widget" assertions became absence pins with lit controls.zod-mirror-parity(pair moved toEXCLUSIONS, 205 to 204),content-channel-family-d-9256(row left, 67 to 66),arm-named-export-8784(ABSENT_BY_DECISIONrow),public-contractandhtml-tier-manifestinapps/console,widget-dom-leak-sweepinapp-shell(target and ledger counts re-derived withoutui:html).Ablation (one-shot, not committed)
With the fix committed at
bfd3722, the nine implementation files were put back froma3c0da9(git checkout a3c0da9 -- …, which restoreshtml.tsxtoo), landing proved on disk by count (register('html'1, theimport './html';line 1, thePUBLIC_BLOCKS'html',row 1,export const HtmlSchema = BaseSchema.extend1,RetiredHtmlNodeSchema0). Tests resolve@object-ui/*through the root vitestresolve.aliastosrc, so no rebuild is in the path. The five pin files:Test Files 5 failed (5),Tests 11 failed | 65 passed (76): every new assertion red, every control green. Restore leg fromHEADinside a trap:git diff HEAD0 bytes,git status --porcelain0 lines,html.tsxabsent, every restored path's hash equals itsHEADblob.Local verification
Union run at
3c6faea(the head before the last commit), each exit captured to a file before reading, under the shared verify lock:vitest run packages/types/ packages/core/ packages/sdui-parser/ packages/cli/ examples/schema-catalog/:Test Files 697 passed (697),Tests 17792 passed | 27 skipped (17819).vitest run packages/components/ packages/fields/:Test Files 705 passed | 2 skipped (707),Tests 8517 passed | 31 skipped (8548).vitest run apps/console/:Test Files 243 passed (243),Tests 2460 passed (2460).PUBLIC_BLOCKS,getPublicConfigs,KNOWN_SCHEMA_TYPES,FIELD_TYPES_SKIP_FALLBACK, the examples file, the catalog,react-pages.md,ui:html, or the edited doc-types gate, plusunit-registry-absence-collision):Test Files 48 passed (48),Tests 1543 passed | 16 skipped (1559).core,components,fields,cliexit 0;typesexit 2 on one vacuous compile-time pin in the new test file ('kbd'is not aSchemaRegistrykey), fixed inbfd3722.Re-run at the final head
bfd3722(it changes only that one test file):type-checkoftypes,core,components,fields,cliall exit 0; the changed fileTest Files 1 passed (1),Tests 6 passed (6). Gates atbfd3722, all exit 0:check:doc-types,check:registry-bare-names,check:handler-key-reads,check:doc-example-ids,check:docs-route-closure,check:prompt-keys,check:entry-guard,check:action-forward-parity,check:spec-symbols,check:new-line-citations(VERDICT 0 new citations),check:control-bytes,check:doc-fences,check:unreferenced-sources,check:side-effects-array,check:doc-example-readers,check:icon-record-names,check:changeset-claims,check:pending-changeset-literals,check:test-path-roots,check:doc-json,check-changeset-presence("22 source file(s) of 7 released package(s) changed, and this change declares 1 changeset(s)"),check-changeset-no-major,regenerate-catalog-index.py --check,regenerate-known-schema-types.mjs --check.NOT MEASURED locally, left to CI:
check:sdui-registration-pins(exit 2, needs a console build),check:readme-exports,check:doc-snippets,check:doc-examples(exit 2 / collapsed population, need the wider package build), and theapps/consoleandpackages/app-shelltype-checks (need the whole build closure).Acceptance notes
sdui.manifest.jsonnameshtml("description": "Raw HTML content"). It is generated from objectui's pinned build byscripts/gen-sdui-manifest-node.mjs, andcheck-sdui-manifest's staleness check forces the regenerate on the next.objectui-shabump, which drops the entry. Carrier: that pin bump. Until then objectstack's server-sidevalidateJsxPagesstill admits the tag, while the console's own compile already refuses it.renderers/basic/html.tsxas the "today unsafe" escape hatch the constrained JSX tier "replaces"; this PR completes that replacement. The ADR is a dated decision record and is left as written.docs/audits/2026-08-zod-to-json-schema-fidelity.mdstill listsHtmlSchema: a dated audit, left as written.Session:
https://claude.ai/code/session_01432GBvPzfZ6CsLanp9xzkZGenerated by Claude Code