From 974840acbd3c347a95915391263a67475995c0c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 12:26:58 +0000 Subject: [PATCH 1/4] =?UTF-8?q?feat(core,components):=20the=20html=20tier?= =?UTF-8?q?=20registers=20and=20declares=20code=20=E2=80=94=20an=20html=20?= =?UTF-8?q?page's=20code=20tag=20renders=20its=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a kind:'html' page, `inline` resolved to the bare `code` key, which was the `field:code` widget's namespace fallback — a code editor reading `value`, no declared inputs, no child slot — so the authored text was dropped while `pre`, `strong`, `em` and `span` rendered theirs through the passthrough. The published manifest left `code` undeclared for the same reason. - components: `code` joins the `TAGS` loop of `html-elements.tsx` (registered `ui:code`, sanitised like its siblings: `className` + the `children` slot); the header no longer calls it "registered elsewhere". - fields: `code` joins `FIELD_TYPES_SKIP_FALLBACK` — the widget keeps `field:code` and stands down from the bare key, so `ui:code` is the one bare claimant (the bare-name collision gate stays at zero contested keys). - core: `code` joins `HTML_TIER_INTRINSICS`; `getPublicConfigs()` projects it stamped `tier: 'html'` — the regenerated manifest grows from 106 to 107 components (48 html-tier), 0 curated entries changed, `div` / `kbd` absent. - types: `HtmlElementSchema` (zod + TS) names `code`, keeping the JSON-surface arm equal to the registration as the objectui#8499 parity pin requires (its count guards move 37 -> 38). - pins: the console renders the text in a `code` element and compiles the tag cleanly against the live page manifest; `field:code` still renders the widget; the manifest declares `code`; the react-page scope injects no `Code` wrapper; the console ledger drops `code`, the core roster test carries it. - generated: `known-schema-types.ts` gains `ui:code`; `body-dialect-census.mjs` mirrors the tag list; two pending changesets take dated notes (47 -> 48, 37 -> 38); one new changeset declares minor for components/core/types/fields. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN --- .changeset/10735-html-tier-manifest.md | 2 + .changeset/10756-html-tier-code-element.md | 41 ++++++ .changeset/8499-node-slot-registered-arms.md | 3 + .../html-tier-code-element-10756.test.tsx | 132 ++++++++++++++++++ .../src/__tests__/html-tier-manifest.test.ts | 30 ++-- .../src/__tests__/public-contract.test.ts | 4 +- packages/cli/src/utils/known-schema-types.ts | 1 + .../src/__tests__/react-page-scope.test.tsx | 17 +++ .../container-declaration-census.test.tsx | 4 +- .../src/renderers/basic/html-elements.tsx | 14 +- .../__tests__/html-tier-intrinsics.test.ts | 40 +++++- .../core/src/registry/html-tier-intrinsics.ts | 16 ++- .../src/__tests__/registry-collision.test.ts | 12 +- packages/fields/src/index.tsx | 5 + .../node-slot-registered-arms-8499.test.ts | 12 +- packages/types/src/layout.ts | 2 +- packages/types/src/zod/layout.zod.ts | 2 +- scripts/body-dialect-census.mjs | 2 +- 18 files changed, 295 insertions(+), 44 deletions(-) create mode 100644 .changeset/10756-html-tier-code-element.md create mode 100644 apps/console/src/__tests__/html-tier-code-element-10756.test.tsx diff --git a/.changeset/10735-html-tier-manifest.md b/.changeset/10735-html-tier-manifest.md index 2bdcd4e0d3..28dc8acd9f 100644 --- a/.changeset/10735-html-tier-manifest.md +++ b/.changeset/10735-html-tier-manifest.md @@ -43,6 +43,8 @@ the `field:code` widget's namespace fallback, not an element renderer — nor is `kbd`; both are recorded, with reasons, in the console's exclusion ledger. No registration changes; `html-elements.tsx` is untouched. +⚠️ **Dated note, 2026-09-27 — `code` has since joined the roster — objectui#10756.** At this change the roster held 47 tags and `code` was undeclared because the bare key was the `field:code` widget's fallback; `html-elements.tsx` now registers `code` as a sanitised passthrough (`ui:code`), `@object-ui/fields` registers the widget with `skipFallback` so it keeps only `field:code`, and the manifest declares `code` with `tier: 'html'` — 48 html-tier entries. `div` and `kbd` are still out. The rest of this entry is kept as the reading of this change. + The manifest the framework ships regenerates from objectui's built tree at the pin (its `gen-sdui-manifest-node.mjs`); its lockstep copy of `manifestFromConfigs` must take this port for the stamp to reach that file — until then the tags are diff --git a/.changeset/10756-html-tier-code-element.md b/.changeset/10756-html-tier-code-element.md new file mode 100644 index 0000000000..b22ed875db --- /dev/null +++ b/.changeset/10756-html-tier-code-element.md @@ -0,0 +1,41 @@ +--- +'@object-ui/components': minor +'@object-ui/core': minor +'@object-ui/types': minor +'@object-ui/fields': minor +--- + +feat(core,components): the html tier registers and declares `code` — `inline` on a `kind:'html'` page renders its text instead of the `field:code` editor + +`renderers/basic/html-elements.tsx` called `code` "registered elsewhere", but the +only bare `code` registration was the `field:code` widget's namespace fallback: a +code editor reading `value`, with no declared inputs and no child slot. The +console's html-tier compile whitelists `ComponentRegistry.getKnownTypes()`, so +`inline` on an html page compiled, resolved to the editor, and the +authored text was dropped — while `pre`, `strong`, `em` and `span` rendered their +children through the passthrough. The published manifest (objectui#10735) left +`code` undeclared for the same reason, so the objectstack gate refused the tag +the renderer mis-drew. + +**What moves for a consumer.** + +- `@object-ui/components` — `code` joins the `TAGS` loop of `html-elements.tsx`: + registered `ui:code`, sanitised like its siblings, declaring `className` and the + `children` slot. An html author's `` now renders a real `code` element with + its text. +- `@object-ui/fields` — `code` joins `FIELD_TYPES_SKIP_FALLBACK`: the widget is + registered `field:code` only and no longer claims the bare `code` key. Forms + resolve widgets through the `field:` namespace, so a field of type `code` renders + the same editor as before; a JSON node authored as bare `{ "type": "code" }` now + renders the element passthrough, not the editor — spell the widget `field:code`. +- `@object-ui/core` — `code` joins `HTML_TIER_INTRINSICS`, so `getPublicConfigs()` + projects it stamped `tier: 'html'` and the regenerated `sdui.manifest.json` + grows by one entry (the html tier declares 48 tags where objectui#10735 + declared 47); `div` and `kbd` stay undeclared. +- `@object-ui/types` — `HtmlElementSchema` (zod and TS) names `code`, keeping the + JSON-surface declaration equal to the registration, as the objectui#8499 pin + requires. + +The `kind:'react'` scope skips stamped entries, so no `Code` wrapper is injected +on react pages. The framework's tracked manifest regenerates at its next +`.objectui-sha` bump (objectstack#20112's port list); nothing there changes here. diff --git a/.changeset/8499-node-slot-registered-arms.md b/.changeset/8499-node-slot-registered-arms.md index 7128a6df7d..0434fec402 100644 --- a/.changeset/8499-node-slot-registered-arms.md +++ b/.changeset/8499-node-slot-registered-arms.md @@ -37,6 +37,9 @@ from 107 to 154: `renderers/basic/html-elements.tsx` registers (`h1`…`h6`, `p`, `a`, `ul`, `img`, …), plus the per-tag keys that module forwards to the DOM (`href`, `target`, `rel`, `title`, `src`, `alt`, `width`, `height`, `dateTime`, `cite`). + ⚠️ **Dated note, 2026-09-27 — that set has since gained `code` — objectui#10756.** + At this change `TAGS` and this arm both named 37 tags; both now name 38, and the + parity pin counts 38. The rest of this entry is kept as the reading of this change. - `InputShorthandSchema` (`zod/form.zod.ts`) — `email` / `password`, the two aliases `renderers/form/input.tsx` registers onto the `input` renderer with `inputType` pinned. `inputType` is deliberately NOT declared on this arm: the wrapper spreads diff --git a/apps/console/src/__tests__/html-tier-code-element-10756.test.tsx b/apps/console/src/__tests__/html-tier-code-element-10756.test.tsx new file mode 100644 index 0000000000..6d2c4530ee --- /dev/null +++ b/apps/console/src/__tests__/html-tier-code-element-10756.test.tsx @@ -0,0 +1,132 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * objectui#10756 — on a `kind:'html'` page, `inline` renders the + * text inside a real `code` element; the `field:code` widget still renders as + * the widget under its own key. + * + * ## The defect + * + * `html-elements.tsx`'s header called `code` "registered elsewhere", but the + * only bare `code` registration was the `field:code` widget's namespace + * fallback: a code EDITOR reading `value`, with no declared inputs and no child + * slot. The console's html-tier compile whitelists + * `ComponentRegistry.getKnownTypes()`, so `inline` compiled, the + * renderer resolved the bare key to the editor, and the authored text was + * dropped. The lit controls — `pre`, `strong`, `em`, `span` — rendered their + * children through the passthrough all along. + * + * ## The fix, and what each pin holds + * + * `code` joins `TAGS` in `html-elements.tsx` (registered `ui:code`, className + + * child slot), joins `HTML_TIER_INTRINSICS`, and `@object-ui/fields` registers + * `code` with `skipFallback` so the passthrough is the one bare claimant. + * + * 1. the html page renders the text in a `code` element (red on the base: + * the editor rendered, no `code` element carried the text); + * 2. the live compile reports nothing for `` with children (red on the + * base: `not-a-container` — the widget declared no slot); + * 3. CONTROL — `field:code` still resolves to the widget and renders it; + * 4. the bare key is owned by the passthrough, the namespaced key by the widget. + * + * It lives in `apps/console` because the claim is about the whole registration + * graph: `@object-ui/components` registers the passthrough and `plugin-form` + * pulls in `@object-ui/fields`, which registers the widget — the pair whose + * import ORDER decided the bare key before the widget stood down. + */ +import { describe, it, expect } from 'vitest'; +import { render, waitFor } from '@testing-library/react'; +import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react'; +import { ComponentRegistry } from '@object-ui/core'; +import { compile, manifestFromConfigs } from '@object-ui/sdui-parser'; + +// The full registration graph — the pair `dev/manifest-dump.tsx` builds the +// published artifacts from, and the pair every live-path suite here reads. +import '@object-ui/components'; +import '../register-plugins'; + +const dataSource = { + find: async () => ({ data: [], total: 0, hasMore: false }), + findOne: async () => null, + create: async () => ({}), + update: async () => ({}), + delete: async () => ({}), + count: async () => 0, + getObjectSchema: async (name: string) => ({ name, label: name, fields: {} }), + getObjects: async () => [], + onMutation: () => () => {}, +} as any; + +/** + * The manifest an html-kind page validates against, built the way the renderer + * builds it — `ComponentRegistry.getKnownTypes()` + each type's declared + * `inputs`, mirroring `page.tsx`'s `getJsxManifest()`. + */ +const livePageManifest = () => + manifestFromConfigs( + ComponentRegistry.getKnownTypes().map((t) => { + const meta = ComponentRegistry.getMeta(t); + return { type: t, namespace: meta?.namespace, isContainer: meta?.isContainer, inputs: meta?.inputs }; + }) as unknown as Parameters[0], + ); + +function renderNode(schema: Record) { + return render( + + + , + ); +} + +const HTML_SOURCE = '

Read inline here.

'; + +describe("kind:'html' — `` renders its text (objectui#10756)", () => { + it('renders the authored text inside a `code` element, with the authored className', async () => { + const { container } = renderNode({ type: 'page', kind: 'html', name: 'code_page', source: HTML_SOURCE }); + await waitFor(() => expect(container.querySelector('code')).toBeTruthy()); + const code = container.querySelector('code')!; + expect(code.textContent).toBe('inline'); + expect(code.classList.contains('k')).toBe(true); + expect(code.closest('p')?.textContent).toBe('Read inline here.'); + // The editor the bare key used to resolve to is not what an html author gets. + expect(container.querySelector('textarea')).toBeNull(); + }); + + it('compiles cleanly against the live page manifest — no `forbidden-tag`, no `not-a-container`', () => { + const diagnostics = compile(HTML_SOURCE, livePageManifest()).diagnostics; + expect(diagnostics.map((d) => `${d.code}:${d.tag ?? ''}`)).toEqual([]); + }); + + it('lit control — `pre` and `strong` render their children the same way', async () => { + const { container } = renderNode({ + type: 'page', + kind: 'html', + name: 'control_page', + source: '
block

bold

', + }); + await waitFor(() => expect(container.querySelector('pre')).toBeTruthy()); + expect(container.querySelector('pre')!.textContent).toBe('block'); + expect(container.querySelector('strong')!.textContent).toBe('bold'); + }); +}); + +describe('`field:code` still renders the widget (objectui#10756 control)', () => { + it('resolves to the field widget under its namespaced key, while the bare key is the passthrough', () => { + expect(ComponentRegistry.getConfig('field:code')?.type).toBe('field:code'); + expect(ComponentRegistry.getConfig('code')?.type).toBe('ui:code'); + expect(ComponentRegistry.get('code')).not.toBe(ComponentRegistry.get('field:code')); + // The passthrough declares what the manifest carries; the widget declares nothing. + expect((ComponentRegistry.getMeta('code')?.inputs ?? []).map((i) => i.name)).toEqual(['className', 'children']); + }); + + it('renders the widget — read-only, `value` comes out in the widget\'s own `pre` block', async () => { + const { container } = renderNode({ type: 'field:code', value: 'const answer = 42;', readonly: true }); + // `CodeField` is lazy; its read-only branch is a `pre` carrying the value. + await waitFor(() => expect(container.querySelector('pre')).toBeTruthy()); + expect(container.querySelector('pre')!.textContent).toBe('const answer = 42;'); + }); +}); diff --git a/apps/console/src/__tests__/html-tier-manifest.test.ts b/apps/console/src/__tests__/html-tier-manifest.test.ts index 8b7374c401..eafdadbe9b 100644 --- a/apps/console/src/__tests__/html-tier-manifest.test.ts +++ b/apps/console/src/__tests__/html-tier-manifest.test.ts @@ -80,11 +80,6 @@ const EXCLUDED_HTML_NAMED: Record = { 'published contract by the objectstack#20112 ruling A: the gate that reads the manifest refuses `
` on ' + 'an html page. The renderer still registers it with a `json`-only deprecation (objectui#4000); reconciling ' + 'that runtime exemption with this contract is a separate card, not a roster edit.', - code: - 'the bare fallback key of the `field:code` widget (namespace `field`, no declared inputs, no child slot) — a ' + - 'code editor reading `value`, not an html element renderer. The ruling listed `code` as "registered elsewhere"; ' + - 'measured, there is no element registration to declare, and declaring the widget would teach a tag whose ' + - 'children the runtime drops.', summary: 'the bare fallback key of the `field:summary` widget — a field, not an element renderer.', object: 'the bare fallback key of the `field:object` widget — a field, not an element renderer.', view: 'the bare `view` registration of `@object-ui/plugin-view`, which happens to share its name with the SVG element.', @@ -176,13 +171,28 @@ describe('the manifest carries the html tier exactly as the registry declares it } }); - it('leaves `div` out, and the two ledgered look-alikes `code` and `kbd` with it', () => { - for (const tag of ['div', 'code', 'kbd']) { + it('leaves `div` out, and the ledgered look-alike `kbd` with it', () => { + for (const tag of ['div', 'kbd']) { // Registered — the tier resolves them at runtime — and deliberately undeclared. expect(ComponentRegistry.getKnownTypes(), `\`${tag}\` is no longer registered; re-read its ledger entry`).toContain(tag); expect(published.components[tag], `\`${tag}\` reached the manifest`).toBeUndefined(); } }); + + it('declares `code` — the element passthrough, not the `field:code` widget the bare key used to fall back to (objectui#10756)', () => { + // The ledger held `code` while the only bare registration was the field + // widget's namespace fallback. `html-elements.tsx` now registers it from + // `TAGS` and the widget stands down from the bare key, so the manifest + // carries the passthrough's own declaration: `ui`, a className, a child slot. + const comp = published.components.code; + expect(comp).toBeDefined(); + expect(comp.tier).toBe('html'); + expect(comp.namespace).toBe('ui'); + expect(comp.inputs.map((i) => `${i.name}:${i.type}`)).toEqual(['className:string', 'children:slot']); + // The registration behind it is the passthrough, and the widget is still its own key. + expect(ComponentRegistry.getConfig('code')?.type).toBe('ui:code'); + expect(ComponentRegistry.getConfig('field:code')?.type).toBe('field:code'); + }); }); describe('the curated tier is unmoved — the JSON-surface vocabulary did not grow (objectui#10735)', () => { @@ -269,8 +279,10 @@ describe('what an html-tier author gets from the gate that reads this manifest ( expect(compile('
hi
', published).ok).toBe(false); }); - it('refuses `` — the ledgered field-widget fallback is not declared', () => { - expect(codes('

x

')).toContain('forbidden-tag:code'); + it('accepts `` with text — declared with its child slot, so neither `forbidden-tag` nor `not-a-container` fires (objectui#10756)', () => { + expect(errors('

Read inline here.

')).toEqual([]); + expect(codes('

x

')).not.toContain('forbidden-tag:code'); + expect(codes('

x

')).not.toContain('not-a-container:code'); }); it('judges a `label` authored with children by its registration — `text` is required and there is no slot', () => { diff --git a/apps/console/src/__tests__/public-contract.test.ts b/apps/console/src/__tests__/public-contract.test.ts index 91a04c6e4e..0d6466f748 100644 --- a/apps/console/src/__tests__/public-contract.test.ts +++ b/apps/console/src/__tests__/public-contract.test.ts @@ -404,7 +404,7 @@ describe('PUBLIC_BLOCKS ↔ console coverage (reverse direction)', () => { * Scope, stated plainly: this covers the CONTAINER half of the layout * vocabulary. The non-container arms (`span`, `separator`, `scroll-area`, * `resizable`, `page`, the deprecated `div`, and — since objectui#8499 armed - * them — the 37 flow/inline HTML tags of `HtmlElementSchema`, not one of which + * them — the 38 flow/inline HTML tags of `HtmlElementSchema`, not one of which * declares containment) are outside it because curating any of them is an * unruled question of its own, and a ledger is a forcing function, not a place * to park four of those at once. @@ -456,7 +456,7 @@ describe('PUBLIC_BLOCKS ↔ console coverage (reverse direction)', () => { * the first is what objectui#8499 broke here. A `z.literal` arm carries one * spelling on `.value`; a `z.enum` arm carries a whole registered family on * `.options` — `SemanticElementSchema`'s seven sectioning tags, and - * `HtmlElementSchema`'s 37 flow/inline tags. A `.value`-only read resolved + * `HtmlElementSchema`'s 38 flow/inline tags. A `.value`-only read resolved * neither, and the anti-vacuity case below reported it as 19 arms yielding 17 * literals, which is precisely the job that case exists to do. * diff --git a/packages/cli/src/utils/known-schema-types.ts b/packages/cli/src/utils/known-schema-types.ts index bc010c7b33..3447617c5d 100644 --- a/packages/cli/src/utils/known-schema-types.ts +++ b/packages/cli/src/utils/known-schema-types.ts @@ -528,6 +528,7 @@ export const KNOWN_SCHEMA_TYPES: readonly string[] = [ 'ui:carousel', 'ui:checkbox', 'ui:cite', + 'ui:code', 'ui:collapsible', 'ui:combobox', 'ui:command', diff --git a/packages/components/src/__tests__/react-page-scope.test.tsx b/packages/components/src/__tests__/react-page-scope.test.tsx index 81e613e1e5..05941770c0 100644 --- a/packages/components/src/__tests__/react-page-scope.test.tsx +++ b/packages/components/src/__tests__/react-page-scope.test.tsx @@ -239,6 +239,23 @@ function Page() { expect(await findByText('React page error')).toBeTruthy(); await waitFor(() => expect(container.textContent).toContain('Img is not defined')); }); + + it('`code` joined the roster without a `Code` wrapper — stamped `tier: html`, skipped by the scope (objectui#10756)', async () => { + // `code` is a leaf like `img`; without the stamp the container branch would + // inject a `` wrapper the moment the passthrough was registered. + const cfg = ComponentRegistry.getPublicConfigs().find((c) => c.type === 'code'); + expect(cfg).toBeTruthy(); + expect(cfg!.tier).toBe('html'); + expect(cfg!.isContainer).toBeFalsy(); + + const source = ` +function Page() { + return inline; +}`; + const { container, findByText } = renderReactPage(source); + expect(await findByText('React page error')).toBeTruthy(); + await waitFor(() => expect(container.textContent).toContain('Code is not defined')); + }); }); // --------------------------------------------------------------------------- diff --git a/packages/components/src/renderers/__tests__/container-declaration-census.test.tsx b/packages/components/src/renderers/__tests__/container-declaration-census.test.tsx index 5ae5bbe93e..e7887d9215 100644 --- a/packages/components/src/renderers/__tests__/container-declaration-census.test.tsx +++ b/packages/components/src/renderers/__tests__/container-declaration-census.test.tsx @@ -173,10 +173,10 @@ describe('the declaration is confined to what was measured (objectui#6764)', () it.each(['img', 'hr', 'br'])('leaves the void tag `%s` alone', async (type) => { // The second half of the same control, inside a LOOP FACTORY. `img`/`hr`/ - // `br` come out of the same `basic/html-elements.tsx` loop as 34 tags that + // `br` come out of the same `basic/html-elements.tsx` loop as 35 tags that // DO render children, and the factory skips `renderChildren` for them by // design (`VOID_TAGS`). A census that worked at file granularity — the - // granularity a static reader can reach — would have declared all 37 + // granularity a static reader can reach — would have declared all 38 // together and told authors that `
` accepts children. expect(await rendersChildren(type)).toBe(false); expect(diagnose(withChildren(type)).map((d) => d.code)).toContain(CONTAINMENT); diff --git a/packages/components/src/renderers/basic/html-elements.tsx b/packages/components/src/renderers/basic/html-elements.tsx index 70c0757526..201a259de5 100644 --- a/packages/components/src/renderers/basic/html-elements.tsx +++ b/packages/components/src/renderers/basic/html-elements.tsx @@ -11,9 +11,13 @@ * PARSED (never executed) into the SDUI tree. For that tier to live up to its * name, the everyday HTML tags an author reaches for — headings, paragraphs, * lists, links, images, emphasis — must each resolve to a renderer (otherwise - * the parser flags them `unknown-component`). div / span / table / code / label - * and the semantic sectioning tags are registered elsewhere; this module fills - * in the rest of the safe flow/inline set. + * the parser flags them `unknown-component`). div / span / table / label and + * the semantic sectioning tags are registered elsewhere; this module fills in + * the rest of the safe flow/inline set — `code` included (objectui#10756): the + * bare `code` key used to be the `field:code` widget's namespace fallback, so + * `inline` on an html page drew a code EDITOR and dropped its + * text. The widget now stands down from the bare key (`FIELD_TYPES_SKIP_FALLBACK` + * in `@object-ui/fields`) and stays reachable as `field:code`. * * Safety: these render real DOM, but the html tier never executes JS, so props * are static literals from the parser. As defense-in-depth the passthrough @@ -33,7 +37,7 @@ type AnyProps = { schema: any; className?: string; [key: string]: any }; const VOID_TAGS = new Set(['img', 'hr', 'br']); // The safe set we own here. Deliberately excludes anything already registered -// (div, span, table, code, label, kbd, the semantic sectioning tags, html) and +// (div, span, table, label, kbd, the semantic sectioning tags, html) and // anything that can execute or escape (script, style, iframe, object, embed, // link, meta, form, input, button — button is the shadcn component). // @@ -44,7 +48,7 @@ const VOID_TAGS = new Set(['img', 'hr', 'br']); // any tag added here that another renderer already registers. const TAGS = [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', - 'p', 'a', 'blockquote', 'pre', + 'p', 'a', 'blockquote', 'pre', 'code', 'strong', 'em', 'b', 'i', 'u', 'small', 'mark', 'sub', 'sup', 'del', 'ins', 'abbr', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'figure', 'figcaption', 'img', 'hr', 'br', 'time', 'address', 'cite', 'q', diff --git a/packages/core/src/registry/__tests__/html-tier-intrinsics.test.ts b/packages/core/src/registry/__tests__/html-tier-intrinsics.test.ts index 65027496ca..0775543027 100644 --- a/packages/core/src/registry/__tests__/html-tier-intrinsics.test.ts +++ b/packages/core/src/registry/__tests__/html-tier-intrinsics.test.ts @@ -83,11 +83,13 @@ describe('HTML_TIER_INTRINSICS — the roster (objectui#10735)', () => { expect(HTML_TIER_INTRINSIC_SET.has('box')).toBe(false); }); - it('leaves `code` and `kbd` out — a field-widget fallback and an unruled component, not element renderers', () => { - // `code` is the bare fallback of the `field:code` widget (no inputs, no - // child slot); `kbd` is a `ui` component reading `keys` / `label`. Both are - // recorded in the console test's exclusion ledger with their reasons. - expect(HTML_TIER_INTRINSIC_SET.has('code')).toBe(false); + it('carries `code` and leaves `kbd` out — an element passthrough since objectui#10756, and an unruled component', () => { + // `code` used to be the bare fallback of the `field:code` widget (no inputs, + // no child slot) and was ledgered out; `html-elements.tsx` now registers it + // from `TAGS` and the widget stands down from the bare key. `kbd` is a `ui` + // component reading `keys` / `label`, still recorded in the console test's + // exclusion ledger with its reason. + expect(HTML_TIER_INTRINSIC_SET.has('code')).toBe(true); expect(HTML_TIER_INTRINSIC_SET.has('kbd')).toBe(false); }); }); @@ -139,12 +141,36 @@ describe('getPublicConfigs — the html tier rides the contract read, stamped (o expect(types).toEqual(['span']); }); - it('does not admit a field widget squatting on an html tag name', () => { + it('does not admit a field widget squatting on an html tag name that is not on the roster', () => { + // `summary` is the console's live instance: the `field:summary` widget's + // bare fallback shares its name with the HTML element and is ledgered out. const r = new Registry(); - r.register('code', C, { namespace: 'field' }); + r.register('summary', C, { namespace: 'field' }); expect(r.getPublicConfigs()).toEqual([]); }); + it('projects a roster tag from whatever holds its bare key — why `field:code` stands down (objectui#10756)', () => { + // The roster names the TAG; the projection copies the registration the bare + // key resolves to. Were the field widget still the bare `code` owner, the + // manifest would declare a code editor under the tag — the defect this card + // closes — so `@object-ui/fields` registers `code` with `skipFallback` and + // `html-elements.tsx`'s `ui:code` is the one bare claimant. Both halves, + // in one registry: + const r = new Registry(); + r.register('code', C, { namespace: 'field', skipFallback: true }); + expect(r.getPublicConfigs()).toEqual([]); // the widget declined the bare key: nothing to project + const Passthrough = () => null; + r.register('code', Passthrough, { + namespace: 'ui', + inputs: [{ name: 'className', type: 'string' }, { name: 'children', type: 'slot' }], + }); + const cfg = r.getPublicConfigs().find((c) => c.type === 'code'); + expect(cfg?.tier).toBe('html'); + expect(cfg?.namespace).toBe('ui'); + expect(cfg?.component).toBe(Passthrough); + expect(r.getConfig('field:code')?.component).toBe(C); // the widget is still its own key + }); + it('resolves a lazily registered roster tag as a stamped stub', () => { const r = new Registry(); r.registerLazy('p', () => Promise.resolve(), { namespace: 'ui' }); diff --git a/packages/core/src/registry/html-tier-intrinsics.ts b/packages/core/src/registry/html-tier-intrinsics.ts index 6956ea3939..2eb3564f67 100644 --- a/packages/core/src/registry/html-tier-intrinsics.ts +++ b/packages/core/src/registry/html-tier-intrinsics.ts @@ -52,11 +52,6 @@ * page. The renderer still registers it and its declared deprecation still * names only the `json` surface (objectui#4000); reconciling that runtime * exemption with this contract is a separate card, not a roster edit. - * - `code` — the ruling's list names it as "registered elsewhere", but the - * bare `code` key the registry holds is the `field:code` widget's - * namespace fallback (a code editor / `pre` block reading `value`, with no - * declared `inputs` and no child slot), not an html element renderer. - * Declaring it would teach a tag whose children the runtime drops. * - `kbd` — a `ui` component under an HTML tag name (renders `keys` / * `label`), not named by the ruling. Admitting it is additive and cheap on * a named need; retracting a declared tag is a narrowing. @@ -68,6 +63,15 @@ * - `button`, `text`, `image`, `html` — HTML tag names that are ALREADY * curated in `PUBLIC_BLOCKS`; they reach the manifest from there, unmarked. * + * `code` was on that list when the roster landed: the ruling named it + * "registered elsewhere", but the only bare `code` registration was the + * `field:code` widget's namespace fallback — a code editor reading `value`, no + * child slot — so an html author's `inline` drew the editor and lost + * its text. objectui#10756 registers `code` as a sanitised passthrough in + * `html-elements.tsx`'s `TAGS` and has the widget stand down from the bare key + * (`FIELD_TYPES_SKIP_FALLBACK` in `@object-ui/fields`; it stays `field:code`), + * so the tag is declared here exactly as its siblings are. + * * The console's `html-tier-manifest` test derives the population of * HTML-named registrations from `@types/react`'s `IntrinsicElements` and * holds every member to one of: this roster, `PUBLIC_BLOCKS`, or its exclusion @@ -83,7 +87,7 @@ export const HTML_TIER_INTRINSICS: readonly string[] = [ // ── `renderers/basic/html-elements.tsx` `TAGS` — the safe flow/inline set ── 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', - 'p', 'a', 'blockquote', 'pre', + 'p', 'a', 'blockquote', 'pre', 'code', 'strong', 'em', 'b', 'i', 'u', 'small', 'mark', 'sub', 'sup', 'del', 'ins', 'abbr', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'figure', 'figcaption', 'img', 'hr', 'br', 'time', 'address', 'cite', 'q', diff --git a/packages/fields/src/__tests__/registry-collision.test.ts b/packages/fields/src/__tests__/registry-collision.test.ts index 25251c069f..ccea217591 100644 --- a/packages/fields/src/__tests__/registry-collision.test.ts +++ b/packages/fields/src/__tests__/registry-collision.test.ts @@ -10,12 +10,14 @@ * Registry collision guard. * * `@object-ui/components` registers display widgets (`text`, `image`, - * `avatar`, `html`, `grid`) under their bare names. `@object-ui/fields` + * `avatar`, `html`, `grid`) under their bare names, and the html tier's + * inline `code` element passthrough (objectui#10756). `@object-ui/fields` * registers form-input widgets that — by name — collide with those. * - * The fix: field registrations for these 5 types pass `skipFallback: true`, - * so the bare lookup keeps returning the display widget while the field - * input is still reachable via the `field:` namespace. + * The fix: field registrations for these types pass `skipFallback: true`, + * so the bare lookup keeps returning the display widget (or, for `code`, the + * element passthrough) while the field input is still reachable via the + * `field:` namespace. * * Regression contract: * 1. `FIELD_TYPES_SKIP_FALLBACK` must contain every colliding name. @@ -35,7 +37,7 @@ import { ComponentRegistry } from '@object-ui/core'; import '@object-ui/components'; import { registerAllFields } from '../index'; -const COLLIDING_TYPES = ['text', 'html', 'image', 'avatar', 'grid'] as const; +const COLLIDING_TYPES = ['text', 'html', 'image', 'avatar', 'grid', 'code'] as const; describe('registry collision (fields ↔ components)', () => { beforeAll(() => { diff --git a/packages/fields/src/index.tsx b/packages/fields/src/index.tsx index 6fdf855a20..b899f552bd 100644 --- a/packages/fields/src/index.tsx +++ b/packages/fields/src/index.tsx @@ -4137,6 +4137,11 @@ const FIELD_TYPES_SKIP_FALLBACK = new Set([ 'slider', // Display renderer owned by `plugin-markdown:markdown`. 'markdown', + // The html tier's inline `code` element passthrough, owned by `ui:code` in + // `renderers/basic/html-elements.tsx` (objectui#10756). Before this line the + // bare `code` key was THIS widget's fallback, so `inline` on a + // `kind:'html'` page drew a code editor and dropped its text. + 'code', // No other package owns the bare `time`/`address` key, but `registerField` // wraps each call in a fresh `React.lazy(...)`, so re-registration (HMR, // re-import) fails the registry's identity check every time and logs the diff --git a/packages/types/src/__tests__/node-slot-registered-arms-8499.test.ts b/packages/types/src/__tests__/node-slot-registered-arms-8499.test.ts index f0a9721622..c9040be735 100644 --- a/packages/types/src/__tests__/node-slot-registered-arms-8499.test.ts +++ b/packages/types/src/__tests__/node-slot-registered-arms-8499.test.ts @@ -181,7 +181,8 @@ describe('objectui#8499 — the controls that keep the acceptances honest', () = ...literalsOf(InputShorthandSchema), ...literalsOf(UiCalendarSchema), ]; - expect(added.length).toBe(7 + 37 + 2 + 1); + // 38 flow/inline tags: the 37 this card armed plus `code` (objectui#10756). + expect(added.length).toBe(7 + 38 + 2 + 1); expect(new Set(added).size).toBe(added.length); const all = literalsOf(AnyComponentSchema); expect(new Set(all).size).toBe(all.length); @@ -209,7 +210,7 @@ describe('objectui#8499 — the family arms are compared against their registrat it('`HtmlElementSchema` names exactly the tags `html-elements.tsx` registers', () => { const tags = sourceArray(read(HTML_RENDERER), 'const TAGS = '); - expect(tags.length, 'the source read went vacuous — check the declaration name').toBe(37); + expect(tags.length, 'the source read went vacuous — check the declaration name').toBe(38); expect([...literalsOf(HtmlElementSchema)].sort()).toEqual([...tags].sort()); }); @@ -220,11 +221,11 @@ describe('objectui#8499 — the family arms are compared against their registrat // touching a registration. Both halves below run the REAL instrument. // // A real mismatch, from a real registration site: `html-elements.tsx` - // registers 37 tags and `SemanticElementSchema` arms none of them, so the + // registers 38 tags and `SemanticElementSchema` arms none of them, so the // equality the two legs above perform must come out false here. const htmlTags = sourceArray(read(HTML_RENDERER), 'const TAGS = '); const semanticArmed = [...literalsOf(SemanticElementSchema)].sort(); - expect(htmlTags.length, 'the registration read went vacuous').toBe(37); + expect(htmlTags.length, 'the registration read went vacuous').toBe(38); expect(semanticArmed.length, 'the arm read went vacuous').toBe(7); expect(semanticArmed).not.toEqual([...htmlTags].sort()); @@ -232,7 +233,8 @@ describe('objectui#8499 — the family arms are compared against their registrat // pass — which is what makes the `toBe(7)` / `toBe(37)` guards above real // guards. A declaration name that is not in the file throws rather than // quietly yielding [], so the non-vacuity checks cannot be satisfied by a - // reader that has stopped reading. + // reader that has stopped reading. (The `toBe(38)` guards count `code`, + // admitted to `TAGS` and to this arm together by objectui#10756.) expect(() => sourceArray(read(SEMANTIC_RENDERER), 'const notADeclaration = ')).toThrow( /declaration not found/, ); diff --git a/packages/types/src/layout.ts b/packages/types/src/layout.ts index 2d2b1dc05d..9e0276e109 100644 --- a/packages/types/src/layout.ts +++ b/packages/types/src/layout.ts @@ -1706,7 +1706,7 @@ export interface SemanticElementSchema extends BaseSchema { export interface HtmlElementSchema extends BaseSchema { type: | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' - | 'p' | 'a' | 'blockquote' | 'pre' + | 'p' | 'a' | 'blockquote' | 'pre' | 'code' | 'strong' | 'em' | 'b' | 'i' | 'u' | 'small' | 'mark' | 'sub' | 'sup' | 'del' | 'ins' | 'abbr' | 'ul' | 'ol' | 'li' | 'dl' | 'dt' | 'dd' | 'figure' | 'figcaption' | 'img' | 'hr' | 'br' | 'time' | 'address' | 'cite' | 'q'; diff --git a/packages/types/src/zod/layout.zod.ts b/packages/types/src/zod/layout.zod.ts index a69b99b43d..b23db79a32 100644 --- a/packages/types/src/zod/layout.zod.ts +++ b/packages/types/src/zod/layout.zod.ts @@ -941,7 +941,7 @@ export const SemanticElementSchema = BaseSchema.extend({ export const HtmlElementSchema = BaseSchema.extend({ type: z.enum([ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', - 'p', 'a', 'blockquote', 'pre', + 'p', 'a', 'blockquote', 'pre', 'code', 'strong', 'em', 'b', 'i', 'u', 'small', 'mark', 'sub', 'sup', 'del', 'ins', 'abbr', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'figure', 'figcaption', 'img', 'hr', 'br', 'time', 'address', 'cite', 'q', diff --git a/scripts/body-dialect-census.mjs b/scripts/body-dialect-census.mjs index 32392eaa4b..c83b721c56 100644 --- a/scripts/body-dialect-census.mjs +++ b/scripts/body-dialect-census.mjs @@ -153,7 +153,7 @@ export const BODY_ONLY_UNRULED = ['tooltip']; const SEMANTIC_TAGS = ['aside', 'main', 'header', 'nav', 'footer', 'section', 'article']; const HTML_ELEMENT_TAGS = [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', - 'p', 'a', 'blockquote', 'pre', + 'p', 'a', 'blockquote', 'pre', 'code', 'strong', 'em', 'b', 'i', 'u', 'small', 'mark', 'sub', 'sup', 'del', 'ins', 'abbr', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'figure', 'figcaption', 'time', 'address', 'cite', 'q', From 2fb626af0d83cc2c0da938ea9eb9fbf89e041ad6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 12:59:14 +0000 Subject: [PATCH 2/4] test(app-shell): enrol ui:code in the DOM-leak sweep beside its html-elements siblings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sweep's completeness case ("components: every widget it registers is swept") derives the registered set from the registry and compares it with the enumerated targets, so the `ui:code` passthrough registered by the previous commit went red there: expected [ 'action:bar', …(158) ] to deeply equal [ 'action:bar', …(159) ], missing "ui:code". `ui:code` joins `COMPONENTS_PLAIN_TYPES` (rendered with the shared readiness class and scanned like `ui:cite` / `ui:strong`) and the BARE_SPREAD ledger group those siblings sit in — the same `html-elements.tsx` factory forwards its whole prop bag, so it leaks the same fourteen attributes. The docblock's hand-written counts and the sentences the file pins them against move with it (targets 159 -> 160, plain 140 -> 141, ledgered 89 -> 90, ready 156 -> 157, all targets 182 -> 183, all ledgered 91 -> 92); the clean count stays 70. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN --- .../__tests__/widget-dom-leak-sweep.test.tsx | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx b/packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx index 76f8f62ca0..6bed2c87dc 100644 --- a/packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx +++ b/packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx @@ -37,9 +37,9 @@ * | plugin-calendar | 3 | 0 | 0 | * | plugin-chatbot | 3 | 0 | 0 | * | plugin-dashboard | 8 | 2 | 7 / 9 | - * | components | 159 | 89 | 12 .. 15 | + * | components | 160 | 90 | 12 .. 15 | * - * **91 of 182 targets leak.** The `components` row is objectui#5574 and is + * **92 of 183 targets leak.** The `components` row is objectui#5574 and is * covered in its own section below; the two `plugin-dashboard` rows are the * older tail. Both are in {@link LEAK_LEDGER}: * `plugin-dashboard:metric` and `plugin-dashboard:metric-card`, the open tail @@ -122,7 +122,7 @@ * have failed. These two differ in exactly one thing: which namespace the extra * widget went into. * - * ### The reading — 119 of 158 ON ARRIVAL, in seven shapes; 89 today + * ### The reading — 119 of 158 ON ARRIVAL, in seven shapes; 90 today * * The card named four candidates (`flex`, `stack`, `container`, `text`) and was * careful to call them candidates. All four leaked. So did 115 others, and the @@ -139,8 +139,8 @@ * so their rows had to go for the gate to pass — the two-way expiry below, * working exactly once it had something to expire. Later slices took more, * and objectui#5632's `ui:sidebar-trigger` took the last shape that named a - * single renderer: **89 rows in FOUR shapes** remain, on a target set that has - * itself grown to 159. What the arrival reading measured is preserved here in prose and in + * single renderer: **90 rows in FOUR shapes** remain, on a target set that has + * itself grown to 160. What the arrival reading measured is preserved here in prose and in * the burn-down note on {@link COMPONENTS_LEAK_GROUPS}; what the gate asserts * is always current truth, which is the whole point of not writing dates into * a ledger. @@ -595,7 +595,7 @@ const CALENDAR_OBJECT_EXTRAS = { * an ordinary authored prop while doing it. * * Measured on the tree this landed on: 152 of 158 targets matched it. - * Today 156 of 159 targets match it; the other three are in + * Today 157 of 160 targets match it; the other three are in * {@link READY_OVERRIDE_REASONS}, each with the reason it cannot — * a recorded limitation with its own two-way assertion below, never a quiet * exemption (the `omitCanaries` discipline, applied to readiness). @@ -747,7 +747,7 @@ const COMPONENTS_PLAIN_TYPES: readonly string[] = [ 'protocol-placeholder:nav:menu', 'ui:a', 'ui:abbr', 'ui:accordion', 'ui:address', 'ui:alert', 'ui:app', 'ui:article', 'ui:aside', 'ui:aspect-ratio', 'ui:avatar', 'ui:b', 'ui:badge', 'ui:blockquote', 'ui:box', 'ui:br', 'ui:breadcrumb', 'ui:button', 'ui:button-group', - 'ui:calendar', 'ui:card', 'ui:carousel', 'ui:checkbox', 'ui:cite', 'ui:collapsible', + 'ui:calendar', 'ui:card', 'ui:carousel', 'ui:checkbox', 'ui:cite', 'ui:code', 'ui:collapsible', 'ui:combobox', 'ui:command', 'ui:container', 'ui:context-menu', 'ui:data-table', 'ui:date-picker', 'ui:dd', 'ui:del', 'ui:div', 'ui:dl', 'ui:dt', 'ui:em', 'ui:email', 'ui:empty', 'ui:figcaption', 'ui:figure', 'ui:file-upload', 'ui:filter-builder', 'ui:flex', @@ -884,8 +884,8 @@ const TARGETS: Readonly> = { // key onto this package's own namespace; the renderer is the same one. { type: 'plugin-dashboard:dashboard', ready: '.grid.auto-rows-min' }, ], - // objectui#5574 — 159 targets, built above rather than spelled here because - // 140 of them need nothing but the shared readiness class. + // objectui#5574 — 160 targets, built above rather than spelled here because + // 141 of them need nothing but the shared readiness class. components: COMPONENTS_TARGETS, }; @@ -978,7 +978,7 @@ interface LedgerEntry { /* ── objectui#5574: the `packages/components` reading, as a LEDGER ─────────── */ /** - * 89 of the 159 `packages/components` targets leak, and they do it in exactly + * 90 of the 160 `packages/components` targets leak, and they do it in exactly * FOUR shapes (119 targets in seven shapes did on arrival; see the burn-down * note below — and note the arrival count of shapes read `eight` here until * objectui#5632 counted them: the groups were seven, and the card's own table @@ -1081,8 +1081,8 @@ interface LedgerEntry { * leaking a NINTH attribute the gate fails, because the measured set no longer * equals the recorded one; if it stops leaking, the gate ALSO fails until the * row goes. An allowlist has neither property. Nothing below is skipped, - * `it.skip`-ed, quarantined or excluded from the sweep — all 159 targets render - * and all 159 are scanned on every run, the 70 clean ones included. + * `it.skip`-ed, quarantined or excluded from the sweep — all 160 targets render + * and all 160 are scanned on every run, the 70 clean ones included. */ /** @@ -1148,7 +1148,7 @@ const COMPONENTS_LEAK_GROUPS: readonly LedgerGroup[] = [ 'action:bar', 'ui:a', 'ui:abbr', 'ui:accordion', 'ui:address', 'ui:alert', 'ui:article', 'ui:aside', 'ui:aspect-ratio', 'ui:avatar', 'ui:b', 'ui:badge', 'ui:blockquote', 'ui:br', 'ui:breadcrumb', 'ui:button-group', 'ui:card', 'ui:carousel', - 'ui:cite', 'ui:collapsible', 'ui:command', 'ui:dd', 'ui:del', 'ui:div', + 'ui:cite', 'ui:code', 'ui:collapsible', 'ui:command', 'ui:dd', 'ui:del', 'ui:div', 'ui:dl', 'ui:dt', 'ui:em', 'ui:empty', 'ui:figcaption', 'ui:figure', 'ui:footer', 'ui:h1', 'ui:h2', 'ui:h3', 'ui:h4', 'ui:h5', 'ui:h6', 'ui:header', 'ui:hr', 'ui:html', 'ui:i', 'ui:image', 'ui:img', 'ui:ins', 'ui:kbd', @@ -1225,7 +1225,7 @@ const LEAK_LEDGER: Readonly> = { issue: 'objectui#4425', }, - /* ── packages/components: 89 of 159 targets, in four measured shapes ───── */ + /* ── packages/components: 90 of 160 targets, in four measured shapes ───── */ ...Object.fromEntries( COMPONENTS_LEAK_GROUPS.flatMap((group) => group.targets.map((type) => [ @@ -1805,11 +1805,11 @@ const wordForCount = (count: number): string => COUNT_WORDS[count] ?? String(cou */ const DOCBLOCK_COUNTS = { /** `packages/components` targets swept: the plain types plus the specials. */ - componentsTargets: 159, + componentsTargets: 160, /** Of those, the ones needing nothing but the shared readiness class. */ - componentsPlainTypes: 140, + componentsPlainTypes: 141, /** Ledgered `packages/components` rows — targets with a recorded leak. */ - componentsLedgered: 89, + componentsLedgered: 90, /** The complement: swept, scanned and clean. */ componentsClean: 70, /** Measured shapes the ledgered rows fall into. */ @@ -1817,15 +1817,15 @@ const DOCBLOCK_COUNTS = { /** Registry prefixes `packages/components` owns. */ componentsPrefixes: 5, /** Targets the shared readiness selector reaches. */ - componentsReadyMatched: 156, + componentsReadyMatched: 157, /** The rest, each with a recorded reason it cannot. */ componentsReadyOverrides: 3, /** Attributes leaked by the shape with the most members. */ commonestShapeAttributes: 14, /** Every target this sweep renders, all five packages. */ - allTargets: 182, + allTargets: 183, /** Every ledgered row, `plugin-dashboard`'s open tail included. */ - allLedgered: 91, + allLedgered: 92, } as const; type CountName = keyof typeof DOCBLOCK_COUNTS; From b62fca804404d1deb32a00d61f75a157e2a8101f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 13:12:56 +0000 Subject: [PATCH 3/4] test(console,types): two prose counts this branch left at 37 read 38 with `code` in the html tier The branch moved `HtmlElementSchema` from 37 to 38 tags and updated the count guards and most of the prose beside them, but two sentences still read 37: `public-contract.test.ts`'s anti-vacuity note ("a single enum arm contributes 37 spellings") and the 8499 parity pin's note on its own `toBe(...)` guards. Both now read 38; comment-only, no assertion changes. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN --- apps/console/src/__tests__/public-contract.test.ts | 2 +- .../src/__tests__/node-slot-registered-arms-8499.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/console/src/__tests__/public-contract.test.ts b/apps/console/src/__tests__/public-contract.test.ts index 0d6466f748..23088f6f0c 100644 --- a/apps/console/src/__tests__/public-contract.test.ts +++ b/apps/console/src/__tests__/public-contract.test.ts @@ -602,7 +602,7 @@ describe('PUBLIC_BLOCKS ↔ the declared layout containers (derived, objectui#68 // // ⚠️ Counting literals against arms — what this line did before // objectui#8499 — is NOT the same assertion and cannot be restored: a single - // enum arm contributes 37 spellings, so the two numbers are no longer meant + // enum arm contributes 38 spellings, so the two numbers are no longer meant // to match. What still holds one-for-one is that no arm contributes ZERO. expect(LAYOUT_UNION_ARMS.length).toBeGreaterThan(0); expect(LAYOUT_ARM_LITERALS.filter((literals) => literals.length > 0)).toHaveLength( diff --git a/packages/types/src/__tests__/node-slot-registered-arms-8499.test.ts b/packages/types/src/__tests__/node-slot-registered-arms-8499.test.ts index c9040be735..d079dbb1cf 100644 --- a/packages/types/src/__tests__/node-slot-registered-arms-8499.test.ts +++ b/packages/types/src/__tests__/node-slot-registered-arms-8499.test.ts @@ -230,11 +230,11 @@ describe('objectui#8499 — the family arms are compared against their registrat expect(semanticArmed).not.toEqual([...htmlTags].sort()); // And the reader must be able to come back EMPTY rather than fabricate a - // pass — which is what makes the `toBe(7)` / `toBe(37)` guards above real + // pass — which is what makes the `toBe(7)` / `toBe(38)` guards above real // guards. A declaration name that is not in the file throws rather than // quietly yielding [], so the non-vacuity checks cannot be satisfied by a - // reader that has stopped reading. (The `toBe(38)` guards count `code`, - // admitted to `TAGS` and to this arm together by objectui#10756.) + // reader that has stopped reading. (The `toBe(38)` guards read 37 when this + // card landed; `code` joined `TAGS` and this arm together in objectui#10756.) expect(() => sourceArray(read(SEMANTIC_RENDERER), 'const notADeclaration = ')).toThrow( /declaration not found/, ); From d601b5c1d2bd51adff60e3a481c51b6b30b80931 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 13:58:17 +0000 Subject: [PATCH 4/4] docs(changeset): the fields stand-down is BREAKING (authoring), and cli names ui:code Contract review 5856453707 (VERDICT: FAIL, text only) on objectui#10756: - The `@object-ui/fields` bullet gets the BREAKING (authoring) banner the protocol and the objectui#9533 precedent require for a bare-key resolution change, with the `field:code` migration sentence, and stays `minor` (a breaking change never declares `major`). - The `@object-ui/types` bullet gains the `AnyComponentSchema` / `objectui validate` accept-set-widening clause. - The intro's "`pre`, `strong`, `em` and `span` rendered their children through the passthrough" corrects `span`'s own renderer (`span.tsx:149`). - `@object-ui/cli` joins the frontmatter as `minor` (`known-schema-types.ts` gains `ui:code`; the objectui#9533 and objectui#6416 precedents name it in the fixed group either way) with a body bullet. No code, test or manifest changes; check:changeset-presence, check:changeset-no-major, check:changeset-overwrite (report-only) and check:changeset-fixed all exit 0, as do check:control-bytes and check:new-line-citations. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN --- .changeset/10756-html-tier-code-element.md | 30 ++++++++++++++-------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/.changeset/10756-html-tier-code-element.md b/.changeset/10756-html-tier-code-element.md index b22ed875db..4d324e7762 100644 --- a/.changeset/10756-html-tier-code-element.md +++ b/.changeset/10756-html-tier-code-element.md @@ -3,6 +3,7 @@ '@object-ui/core': minor '@object-ui/types': minor '@object-ui/fields': minor +'@object-ui/cli': minor --- feat(core,components): the html tier registers and declares `code` — `inline` on a `kind:'html'` page renders its text instead of the `field:code` editor @@ -12,10 +13,10 @@ only bare `code` registration was the `field:code` widget's namespace fallback: code editor reading `value`, with no declared inputs and no child slot. The console's html-tier compile whitelists `ComponentRegistry.getKnownTypes()`, so `inline` on an html page compiled, resolved to the editor, and the -authored text was dropped — while `pre`, `strong`, `em` and `span` rendered their -children through the passthrough. The published manifest (objectui#10735) left -`code` undeclared for the same reason, so the objectstack gate refused the tag -the renderer mis-drew. +authored text was dropped — while `pre`, `strong` and `em` rendered their +children through the passthrough, and `span` through its own renderer. The +published manifest (objectui#10735) left `code` undeclared for the same reason, +so the objectstack gate refused the tag the renderer mis-drew. **What moves for a consumer.** @@ -23,18 +24,27 @@ the renderer mis-drew. registered `ui:code`, sanitised like its siblings, declaring `className` and the `children` slot. An html author's `` now renders a real `code` element with its text. -- `@object-ui/fields` — `code` joins `FIELD_TYPES_SKIP_FALLBACK`: the widget is - registered `field:code` only and no longer claims the bare `code` key. Forms - resolve widgets through the `field:` namespace, so a field of type `code` renders - the same editor as before; a JSON node authored as bare `{ "type": "code" }` now - renders the element passthrough, not the editor — spell the widget `field:code`. +- `@object-ui/fields` — **BREAKING (authoring): the bare `code` key no longer + resolves to the code-editor widget.** `code` joins `FIELD_TYPES_SKIP_FALLBACK`, + so the widget is registered `field:code` only. A node authored as bare + `{ "type": "code", "value": … }` now renders the html tier's `code` element (it + draws `children`, not `value`); in a registry without `@object-ui/components` + the bare key resolves to nothing. Migration: `{ "type": "code" }` becomes + `{ "type": "field:code" }`. Form fields of type `code` resolve through the + `field:` namespace and render the same editor as before. Released as `minor` + under objectui's version policy; a breaking change never declares `major`. - `@object-ui/core` — `code` joins `HTML_TIER_INTRINSICS`, so `getPublicConfigs()` projects it stamped `tier: 'html'` and the regenerated `sdui.manifest.json` grows by one entry (the html tier declares 48 tags where objectui#10735 declared 47); `div` and `kbd` stay undeclared. - `@object-ui/types` — `HtmlElementSchema` (zod and TS) names `code`, keeping the JSON-surface declaration equal to the registration, as the objectui#8499 pin - requires. + requires — so `AnyComponentSchema` (and `objectui validate`) now accepts + `{ "type": "code" }`, which it refused before. +- `@object-ui/cli` — `packages/cli/src/utils/known-schema-types.ts` regenerates + from the registrations and gains `ui:code`, so `objectui validate` and + `objectui check` recognise it as a known schema type (the objectui#9533 and + objectui#6416 precedent). The `kind:'react'` scope skips stamped entries, so no `Code` wrapper is injected on react pages. The framework's tracked manifest regenerates at its next