fix(components): the semantic sectioning tags and aspect-ratio declare the containment they render — with the full census behind the scope - #6774
Merged
Conversation
…e the containment they render `renderers/layout/semantic.tsx` registers `aside`, `main`, `header`, `nav`, `footer`, `section` and `article`; `renderers/layout/aspect-ratio.tsx` registers one more. All eight call `renderChildren(schema.children || schema.body)` and none declared `isContainer`, so `validateTree` warned `not-a-container` on a child list the renderer then rendered — a warning that lies, on the tier built to accept AI-authored pages. Scoped by a census, not by a sweep: every registered key was rendered through the real `SchemaRenderer` with one child and put through `validateTree`. Of 131 bare authoring tags, 58 render `schema.children`, 5 declared the flag, 53 did not. These 8 are the subset whose second consumer is provably unaffected — `react-page.tsx` drops containers from the `kind:'react'` JSX scope but reads `getPublicConfigs()`, and none of the 8 is in the curated public contract. `renderers/__tests__/container-declaration-census.test.tsx` pins the eight plus the four exception populations the census separated: `tabs` (renders `items[].content`), the void tags from the same loop factory as 34 that do render children, and `badge`/`alert` (read `schema.body`, a key the containment check never inspects). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
…pin's docblock Comment-only. The docblock said "two of the four populations below", which matches nothing in the file: there are three exception populations, and each has its own control -- `tabs`, the void tags, and the `schema.body` readers. Naming them is the whole point of that sentence, since it is what stops the next reader treating the predicate as universal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
Contributor
✅ 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
|
os-sales
marked this pull request as ready for review
August 29, 2026 11:09
This was referenced Aug 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6764
The card's point is the recurrence, so the census came first. It is complete, and
it is bigger than eight: 53 registrations, not 8. This PR fixes the 8 the card
names — the subset where every reading is unambiguous — and reports the other 45
rather than sweeping them in.
1. The census
Measured at runtime, over the live registry, not by reading source. Every
registered key was rendered through the real
SchemaRendererwith onetextchild, and the same key put through
validateTreeon the manifest the appbuilds. Of 131 bare authoring tags (292 registry keys once the
ns:tagtwins are counted):
schema.children(the child text reached the DOM)isContainernot-a-containeron a child list it then rendersschema.bodyonlyThe control set comes back clean. The 5 that declare it are exactly
flex/grid/card/container/stack— the control the card named. Thezero elsewhere is therefore a reading, not a property of the scan. It also agrees
with the existing
layout-containers-declare-containmentpin (PR #6762) onflex: declared, no diagnostic, renders children.The 53, by the registration that owns them
basic/html-elements.tsx(loop factory)h1–h6,p,a,blockquote,pre,strong,em,b,i,u,small,mark,sub,sup,del,ins,abbr,ul,ol,li,dl,dt,dd,figure,figcaption,time,address,cite,qlayout/semantic.tsx(loop factory)aside,main,header,nav,footer,section,articlelayout/page.tsx(one sharedpageMeta)page,app,home,record,utilitylayout/aspect-ratio.tsxaspect-ratiobasic/div.tsx·basic/span.tsx·complex/scroll-area.tsxdiv,span,scroll-areaform/form.tsx·form/toggle.tsxform,toggleform/button.tsxbutton— the only public-tier memberNothing outside
packages/components/src/renderers/is in the population.Every registration in the repo that renders a child list lives under that path;
the sibling-held areas (
packages/plugin-timeline/,packages/react/src/,apps/console/,packages/fields/src/widgets/) contribute zero, so there isnothing to report out of fence.
The two populations really are two — three separators, all measured
The card was right that collapsing them would break the second group. Three
distinct exception shapes turned up, and each is now a control in the new pin:
tabs— the card's named non-case. Rendersitems[].content, neverschema.children. Runtime probe: does not put an authored child list onthe page, so its
not-a-containeris true and must survive. It is alsoPUBLIC, so sweeping it in would have deleted its identifier from the JSXscope of every
kind:'react'page as well.img/hr/br— they come out of the same loopfactory as 34 tags that do render children, and the factory skips
renderChildrenfor them by design (VOID_TAGS). A census at filegranularity — the granularity a static reader can reach — would have declared
all 37 together and told authors that
braccepts children.badge/alertand 8sidebar-*— they renderschema.bodyand nevertouch
schema.children.validateTree's containment branch is guarded bynode.children?.lengthalone, so no author has ever drawn a falsediagnostic from them. A "renders children or body" predicate — the
spelling the card itself uses — over-reports by 10.
2. The second consumer, measured per registration
This is the question the card recorded as unmeasured. It has a mechanical
answer:
renderers/layout/react-page.tsxdrops containers when building the JSXscope (
if (!tag || cfg.isContainer) continue;), but it iteratesgetPublicConfigs(), not the whole registry. So the declaration can onlyremove an injected identifier for a tag in the curated
PUBLIC_BLOCKScontract.Of the 53: 52 are private → the second consumer is a proven no-op.
1 is public:
button. Declaring it would removeButtonfrom everykind:'react'page scope — a real behaviour change, andbuttonreads childrenas a label fallback (
schema.label || renderChildren(schema.body || schema.children)),not as layout containment. It is left alone, deliberately.
A third consumer exists and is also a no-op here:
sdui-parser/codegen.tsprintsa "container" column, and it too reads the public manifest only.
3. What this PR changes
Two meta objects, eight registrations:
renderers/layout/semantic.tsx— one factory, 7 tags.renderers/layout/aspect-ratio.tsx— 1.Both live under
renderers/layout/, both renderrenderChildren(schema.children || schema.body), neither is inPUBLIC_BLOCKS.The justification is objectui#3900's, written out at
packages/layout/src/index.ts:childrenis a base property of every node in the JSON protocol(
BASE_PROPSinsdui-parser/src/validate.ts), not a per-component authoringkey, so the flag widens no spec surface. Omitting it never made children illegal
— nothing on the render path reads it — it made
validateTreelie.The new pin, and its relationship to the existing one
renderers/__tests__/container-declaration-census.test.tsxis a new file.packages/components/src/__tests__/layout-containers-declare-containment.test.tsx(PR #6762) is NOT modified — not a ratchet, not a weakening, byte-identical to
origin/main. Two reasons: this round's fence confines edits topackages/components/src/renderers/, and the two pins own different facts (thatone owns the
uilayout-primitive family from #6740; this one owns the eightplus the three exception populations above). They agree on
flexand onbadge.The new pin asserts, per tag: the renderer really does put an authored child list
on the page (so the declaration is honest), then that no
not-a-containerisdrawn — with reachability controls before the absence. Then the three exception
controls, then the premise that made the change safe: none of the 8 is in
getPublicConfigs(), pinned with a direction control so that promoting one intothe public contract re-opens the question here instead of silently dropping a
tag from every react page.
4. The gate — verdict: the predicate is NOT decidable on the source
The card asks for a gate "if and only if the census shows the predicate is
mechanically decidable on the source." It is not, and the census measured
four independent reasons — each one a case where a source reader fails in the
direction that reads as compliance:
semantic.tsxand
html-elements.tsxregister from a loop variable.scripts/component-registrations.mjs— this repo's one answer to "which keysdoes this source register?" — refuses computed keys by design. 41 of the 53
are invisible to it.
page.tsxas compliant. That file contains twoisContainertokens, so a file-level scan (the shape of the named reference,check-element-data-source-declaration.mjs— note it is not onmain; itlives on the unmerged
claude/issue-6678-...branch, so anyone following thepointer should read it there) calls it declared. Both tokens are
in a manifest-builder helper; all five of its registrations lack the flag.
The reference script itself declines per-registration matching: "a reader that
guesses would fail in the direction that matters (a wrong pairing reads as
compliance)."
source reader scores a file in isolation; the registry keeps whichever
registration ran last. This tree has two recorded casualties of exactly
that —
ui:kbdandui:table(objectui#5125), registrations that keptcompiling, kept passing type-check and never ran. So a static reader can
score a registration that is not live, or credit a file whose declaring
registration is the one that loses. (I checked the instance I expected to
find here and it is NOT one: all seven
page:*registrations incontainers.tsxcarryskipFallback: true, so they never claim the barekeys and there is no collision with
semantic.tsx. The hazard is real, thisparticular pair is clean.)
childrenvsbodyis not a source distinction a naive predicate keeps.The predicate that matters is
schema.childrenalone; a source scan for"children or body" over-reports by 10. My own first scan also missed
schema?.children(optional chaining) incontainers.tsx— an under-report,silently green.
What is decidable is the runtime form — exactly what produced the numbers
above: iterate
getKnownTypes(), render each with one child, askvalidateTree.It has no parsing problem, it sees loop factories and import order for free, and
it distinguishes
childrenfrombodybecause it asks the consumer rather thanthe source. That is also the mechanism the existing #6740 pin already uses — the
difference is that its covered set is a literal 4-element array, which is
precisely why the class regenerated past it.
⛔ A gate is deliberately NOT added in this PR. Making that runtime probe
universal is red today on 53 tags, so landing it requires either fixing all 53 or
carrying a ledger of accepted ones — and that is the design call the triage
comment reserved for the decision box, on the grounds that it could not be made
well without a denominator. The denominator is now attached: 53, of which 1 is
public and 3 exception shapes are real.
Scope note — the two orders on this card differ, flagged rather than resolved
The triage comment (2026-08-29 08:26) fenced this round to the census alone:
⛔ 不修那 8 个, ⛔ 不建门禁. The dispatch order (09:27) re-scoped to census →
unambiguous subset → gate-iff-decidable. This PR follows the dispatch order for
the fix (kept to exactly the 8 the card names, each with all four readings
unambiguous and the census complete in the same PR) and the triage comment for
the gate (not built; delivered as a decision with the denominator). If that
reading is wrong, the census is not lost with it: it lives in this PR body and in
the new pin's docblock, and only the two
isContainerlines (and the eightassertions they support) would come out.
Verification
f341b9d27(the fix commit):pnpm exec vitest run packages/components/ packages/sdui-parser/— 219 files, 2091 tests, all passed (VERDICT command-exit 0, 6m29s).a99813cc2is comment-only (a docblock sentence that named the wrong number of exception populations). Re-run on that head rather than assumed: the census pin itself — 1 file, 22 tests, passed — pluscheck:control-bytes✅ andeslinton the file (0 errors, 0 warnings). Nothing else in the derived gate set reads a docblock.packages/components/src/renderers/__tests__/+ thelayout-containers-declare-containmentandreact-page-scopepins +packages/sdui-parser/— 16 files, 195 tests, all passed.isContainer: truelines fromHEADgives 8 failed | 14 passed (22) — and the direction is the point: the 8 failures are all and only the "draws nonot-a-container" assertions. The 8 "renders an authored child list" assertions stay green, which is the packages/layout:page-header注册缺isContainer: true,而PageHeader有意渲染schema.children—— sdui-parser 对文档认可的写法报not-a-container#3900 claim measured rather than quoted: the render path does not read the flag. Every exception control and the public-tier premise stay green too. Mutation confirmed on disk before the run (grep -c 'isContainer: true'1 → 0 in each file, blob hashes differ fromHEAD); restore confirmed after (git diff HEADempty and byte-for-byte hash equality with theHEADblobs). No build sits between the mutation and the reading — the test imports the mutated module relatively from source, which the ablation itself demonstrates by moving the result with zero build steps.check:control-bytes✅ ·check:vi-mock-specifiers✅ ·check-changeset-no-major✅ ·check-changeset-fixed✅ ·check-changeset-presence✅ (1 changeset for 3 published source files) ·check-changeset-overwrite✅eslinton the three touched files: 0 errors. The 4no-explicit-anywarnings are pre-existing, on lines this diff does not touch (semantic.tsx:22,aspect-ratio.tsx:15— both theforwardRefannotations).check:sdui-registration-pinsreports exit 2 — prerequisite not met ("No console build to weigh"), i.e. NOT MEASURED locally, not a failure. It pins that registration keys survive the bundle; this diff adds no key and removes none. CI runs it.pnpm --filter @object-ui/components run type-check && … run lint— exit 0 (&&-joined, so the status covers both).tsc --noEmit && tsc -p tsconfig.test.json: 0 errors, and the new pin is genuinely inside that number rather than excluded —tsc -p tsconfig.test.json --showConfigresolves 209 files and./src/renderers/__tests__/container-declaration-census.test.tsxis one of them (counted, not inferred from theincludeglob). eslint for the package:938 problems (0 errors, 938 warnings)— all pre-existing.error TSand was not a red:TS2307 Cannot find module '@object-ui/core', i.e. the dependency closure was not built. Recorded here rather than quietly re-run:pnpm --filter '@object-ui/components^...' build(8 packages, exit 0) is what made the number meaningful.Scope of the census outside this package
The runtime census covers everything
renderers/index.tsregisters (292 registrykeys). For the rest of the tree the reading is a source scan: across
packages/, 118 files make 216ComponentRegistry.registercalls, and everyfile that reads
schema.children/schema.bodyis underpackages/components/src/renderers/.apps/andexamples/make zeronon-lazy
registercalls — verified as a reading, not a blind scan: the samegrep finds 32
registerLazystubs there, whose renderers live inpackages/plugin-*and were covered by thepackages/pass. So there isnothing to report out of fence for this predicate.
Filed while measuring
aspect-ratiodocs demos authorcontent, a key the renderer never reads — every demo on the published page renders an empty ratio box #6773 —finding(docs,examples): all 5 shippedaspect-ratiodemosauthor
content, a key the renderer never reads, so every demo on thepublished docs page renders an empty ratio box — and the page's own Schema
block publishes
contentwhile omittingimage/alt, which the renderer doesread. The contrast that makes it a reading: all 9
components-layout-semanticfixtures authorchildren, which those renderersdo read (and which, until this PR, drew the false
not-a-containeron everyone of them — the repo's own published demos were the warning's victims, which
is objectui#3900's shape exactly).
unknown-proponbody— the one child-list key 10+ registrations actually honour #6771 —finding(sdui-parser/components):bodyis not inBASE_PROPS, so the tier answers an authoredbodywithunknown-prop: badge has no prop "body"— on the one child-list key 10registrations actually honour. Measured against the built parser. A different
defect with a different fix (and two opposite candidate directions), so it is
filed rather than swept in here.
Generated by Claude Code