Skip to content

finding(core/contract): the seven semantic sectioning tags are declared layout containers the curated vocabulary has never ruled on, either way #8775

Description

@os-bill

Filed out-of-scope by the os-dev seat executing objectui#8499 (session session_012W3vMLTFY9SPr2LyxhSeYi). ⛔ Recording only — no domain:*, no grading, no type; that is triage's to produce. ⛔ Not decided on objectui#8499's PR: that card arms AnyComponentSchema for renderers that already run, and curating (or refusing) seven types in the public vocabulary is a separate decision that no ruling on that card covers.

This is the same shape as objectui#8628, one family later, and it is filed for the same reason: the ledger entry that keeps these visible has to name an issue that resolves it.

How they arrived in the population, measured

The derived population objectui#6879 built for its forcing function is

LayoutSchema.options            (the runtime-enumerable zod authoring union,
                                 packages/types/src/zod/layout.zod.ts)
  ∩ ComponentRegistry meta isContainer === true

objectui#8499 adds SemanticElementSchema — one arm whose type is a z.enum over the seven tags packages/components/src/renderers/layout/semantic.tsx registers. Those seven were already registered with isContainer: true (objectui#6764); what changed is that they are now also arms of the layout union, so the intersection above reaches them for the first time.

Measured on this card's branch, the population goes from 7 members to 14:

type in PUBLIC_BLOCKS?
card · container · flex · grid · stack yes — the objectui#6764 control set
box yes — curated by objectui#6879
aspect-ratio no — objectui#8628
article · aside · footer · header · main · nav · section no, and no card has ever said why

Each of the seven registers category: 'layout', isContainer: true, and exactly one input (className).

Two further readings, so the decision is not taken on a guess:

  • The spec-parity gate cannot see any of them. Object.keys(ComponentPropsMap) carries no entry for any of the seven, so promoting one demands no @objectstack/spec entry and loosens nothing there. (Same answer objectui#6879 measured for box.)
  • They are not unvalidated today. renderers/layout/page.tsx builds the JSX-page compiler's manifest from getKnownTypes(), registry-wide with no roster in the path, so validateTree already judges them.

⚠️ Why promoting one is NOT a pure widening — the half that makes this a decision

CORRECTED 2026-09-09. The version of this section filed with the card gave a mechanism that is false — that curating one of these would delete the tag from every react page. It does not. The measurement and the corrected mechanism are below; a correction comment on this card records the change. ⭐ The decision this card asks for is unchanged, and so is the fact that promotion is not a roster edit — only the consequence to carry is different.

packages/components/src/renderers/layout/semantic.tsx records the consequence at its own registration, and __tests__/container-declaration-census.test.tsx pins the premise:

Safe on the second consumer, and that was MEASURED rather than assumed (objectui#6764): renderers/layout/react-page.tsx drops containers from the kind:'react' JSX scope, but it iterates getPublicConfigs(), and none of these seven tags is in the curated PUBLIC_BLOCKS contract -- so there is no injected Aside/Main/... identifier for this flag to remove. __tests__/container-declaration-census.test.tsx pins that premise, so promoting one of these into the public contract re-opens the question here instead of silently deleting a tag from every react page.

⚠️ That quote is accurate; the inference first drawn from it was not. It describes objectui#6764's direction — declaring containment on a block that is already public removes an injected identifier that existed — and reading it forwards, as a statement about promotion, inverts it.

Measured, on main:

leg reading
react-page.tsx:75-77 if (!tag || cfg.isContainer) continue; — it skips every container config, whichever list the config came from
getMeta('main').isContainer / getConfig('main').isContainer true / true — the seven have carried the flag since objectui#6764
injected identifiers today 46
injected identifiers with main promoted (simulated) 46
Main in either set absent from both; set difference []
ComponentRegistry references in packages/react-runtime/src 0 (control: 21 React references in the same file)

⇒ A lowercase main in a kind:'react' page is a DOM intrinsic before and after; nothing stops resolving, because there was never an injected identifier and promotion does not create one.

What promotion actually costs, and it is still enough to make this a decision rather than a roster edit:

  1. It widens the AI-authoring vocabulary, sdui.manifest.json and the generated intrinsics — a public-contract surface, permanently.
  2. It turns container-declaration-census.test.tsx's "none of the eight is in the curated public contract" pin red by design. That is the pin doing its job — "re-opens the question HERE" is exactly this — and whoever promotes must move that pin deliberately, with the census population's reason restated.

⛔ What it does not cost is any react page.

What the decision looks like

Two coherent answers, and this card needs only one of them chosen:

  1. Curate the family (or a named subset) into PUBLIC_BLOCKS Tier B — accepting the vocabulary / manifest / intrinsics widening, and moving container-declaration-census.test.tsx deliberately rather than discovering it red. ⛔ No react-page.tsx change is required; that premise was the erratum.
  2. Refuse them on stated merits — e.g. "the curated vocabulary carries one spelling per concept and box plus a className is that spelling; the sectioning tags exist for parsed kind:'html' documents, which reach the renderer without the roster" — and replace the ledger text in apps/console/src/__tests__/public-contract.test.ts with those merits.

Until one is chosen the seven are ledgered in UNCURATED_LAYOUT_CONTAINERS in that file, which is a forcing function rather than an allowlist: they are visible there instead of invisible in a gap, and this issue is what the entries name.

Reproduction

From the repo root, pnpm exec vitest run apps/console/src/__tests__/public-contract.test.ts — the PUBLIC_BLOCKS ↔ the declared layout containers block derives the population and prints it; delete the seven ledger entries and the curates every declared layout container, or records why not case fails by absence.


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions