Skip to content

fix(components): the semantic sectioning tags and aspect-ratio declare the containment they render — with the full census behind the scope - #6774

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-6764-container-declaration-census
Aug 29, 2026
Merged

fix(components): the semantic sectioning tags and aspect-ratio declare the containment they render — with the full census behind the scope#6774
os-sales merged 2 commits into
mainfrom
claude/issue-6764-container-declaration-census

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

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 SchemaRenderer with one text
child, and the same key put through validateTree on the manifest the app
builds. Of 131 bare authoring tags (292 registry keys once the ns:tag
twins are counted):

population count
renders schema.children (the child text reached the DOM) 58
— of those, declares isContainer 5
— of those, does not — draws not-a-container on a child list it then renders 53
renders no children, correctly keeps the diagnostic 73
— of those, renders schema.body only 10
failed to render (scored on an exception) 0

The control set comes back clean. The 5 that declare it are exactly
flex / grid / card / container / stack — the control the card named. The
zero elsewhere is therefore a reading, not a property of the scan. It also agrees
with the existing layout-containers-declare-containment pin (PR #6762) on
flex: declared, no diagnostic, renders children.

The 53, by the registration that owns them

site count tags
basic/html-elements.tsx (loop factory) 34 h1h6, 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, q
layout/semantic.tsx (loop factory) 7 aside, main, header, nav, footer, section, article
layout/page.tsx (one shared pageMeta) 5 page, app, home, record, utility
layout/aspect-ratio.tsx 1 aspect-ratio
basic/div.tsx · basic/span.tsx · complex/scroll-area.tsx 3 div, span, scroll-area
form/form.tsx · form/toggle.tsx 2 form, toggle
form/button.tsx 1 buttonthe only public-tier member

Nothing 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 is
nothing 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:

  1. tabs — the card's named non-case. Renders items[].content, never
    schema.children. Runtime probe: does not put an authored child list on
    the page, so its not-a-container is true and must survive. It is also
    PUBLIC, so sweeping it in would have deleted its identifier from the JSX
    scope of every kind:'react' page as well.
  2. The void tags img / hr / br — they come out of the same loop
    factory
    as 34 tags that do render children, and the factory skips
    renderChildren for them by design (VOID_TAGS). A census at file
    granularity — the granularity a static reader can reach — would have declared
    all 37 together and told authors that br accepts children.
  3. badge / alert and 8 sidebar-* — they render schema.body and never
    touch schema.children. validateTree's containment branch is guarded by
    node.children?.length alone, so no author has ever drawn a false
    diagnostic 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.tsx drops containers when building the JSX
scope (if (!tag || cfg.isContainer) continue;), but it iterates
getPublicConfigs(), not the whole registry. So the declaration can only
remove an injected identifier for a tag in the curated PUBLIC_BLOCKS contract.

Of the 53: 52 are private → the second consumer is a proven no-op.
1 is public: button. Declaring it would remove Button from every
kind:'react' page scope — a real behaviour change, and button reads children
as 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.ts prints
a "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 render
renderChildren(schema.children || schema.body), neither is in PUBLIC_BLOCKS.
The justification is objectui#3900's, written out at packages/layout/src/index.ts:
children is a base property of every node in the JSON protocol
(BASE_PROPS in sdui-parser/src/validate.ts), not a per-component authoring
key, so the flag widens no spec surface. Omitting it never made children illegal
— nothing on the render path reads it — it made validateTree lie.

The new pin, and its relationship to the existing one

renderers/__tests__/container-declaration-census.test.tsx is 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 to
packages/components/src/renderers/, and the two pins own different facts (that
one owns the ui layout-primitive family from #6740; this one owns the eight
plus the three exception populations above). They agree on flex and on badge.

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-container is
drawn — 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 into
the 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:

  1. The tree's own registration reader cannot name 41 of the 53. semantic.tsx
    and html-elements.tsx register from a loop variable.
    scripts/component-registrations.mjs — this repo's one answer to "which keys
    does this source register?" — refuses computed keys by design. 41 of the 53
    are invisible to it.
  2. File granularity mis-reads page.tsx as compliant. That file contains two
    isContainer tokens, so a file-level scan (the shape of the named reference,
    check-element-data-source-declaration.mjs — note it is not on main; it
    lives on the unmerged claude/issue-6678-... branch, so anyone following the
    pointer 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)."
  3. Which registration is LIVE is a whole-program, import-order fact. A
    source reader scores a file in isolation; the registry keeps whichever
    registration ran last. This tree has two recorded casualties of exactly
    that — ui:kbd and ui:table (objectui#5125), registrations that kept
    compiling, 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 in
    containers.tsx carry skipFallback: true, so they never claim the bare
    keys and there is no collision with semantic.tsx. The hazard is real, this
    particular pair is clean.)
  4. children vs body is not a source distinction a naive predicate keeps.
    The predicate that matters is schema.children alone; a source scan for
    "children or body" over-reports by 10. My own first scan also missed
    schema?.children (optional chaining) in containers.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, ask validateTree.
It has no parsing problem, it sees loop factories and import order for free, and
it distinguishes children from body because it asks the consumer rather than
the 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 isContainer lines (and the eight
assertions they support) would come out.

Verification

  • Union at f341b9d27 (the fix commit): pnpm exec vitest run packages/components/ packages/sdui-parser/219 files, 2091 tests, all passed (VERDICT command-exit 0, 6m29s).
  • The second commit a99813cc2 is 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 — plus check:control-bytes ✅ and eslint on the file (0 errors, 0 warnings). Nothing else in the derived gate set reads a docblock.
  • Earlier targeted run, before the union: packages/components/src/renderers/__tests__/ + the layout-containers-declare-containment and react-page-scope pins + packages/sdui-parser/ — 16 files, 195 tests, all passed.
  • Reverse verification. Removing the two isContainer: true lines from HEAD gives 8 failed | 14 passed (22) — and the direction is the point: the 8 failures are all and only the "draws no not-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 from HEAD); restore confirmed after (git diff HEAD empty and byte-for-byte hash equality with the HEAD blobs). 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
  • eslint on the three touched files: 0 errors. The 4 no-explicit-any warnings are pre-existing, on lines this diff does not touch (semantic.tsx:22, aspect-ratio.tsx:15 — both the forwardRef annotations).
  • check:sdui-registration-pins reports 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 lintexit 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 --showConfig resolves 209 files and ./src/renderers/__tests__/container-declaration-census.test.tsx is one of them (counted, not inferred from the include glob). eslint for the package: 938 problems (0 errors, 938 warnings) — all pre-existing.
    • ⚠️ The first attempt at this reported 336 error TS and 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.ts registers (292 registry
keys). For the rest of the tree the reading is a source scan: across
packages/, 118 files make 216 ComponentRegistry.register calls, and every
file that reads schema.children / schema.body is under
packages/components/src/renderers/. apps/ and examples/ make zero
non-lazy register calls — verified as a reading, not a blind scan: the same
grep finds 32 registerLazy stubs there, whose renderers live in
packages/plugin-* and were covered by the packages/ pass. So there is
nothing to report out of fence for this predicate.

Filed while measuring


Generated by Claude Code

claude added 2 commits August 29, 2026 09:46
…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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 48 chunks) 3180.2 KB 3222.7 KB
Main entry chunk (gzip) 148.2 KB 350 KB
Entry file index-DzlGMuif.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 11.89KB 4.50KB
app-shell (runtime-config.js) 20.61KB 7.35KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 511.71KB 116.30KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 173.10KB 47.96KB
fields (index.js) 240.93KB 60.76KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.95KB 10.97KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.89KB 12.91KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 190.33KB 45.10KB
plugin-dashboard (index.js) 133.44KB 34.48KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 245.43KB 62.45KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.96KB 32.63KB
plugin-gantt (index.js) 165.20KB 40.37KB
plugin-grid (index.js) 201.53KB 54.54KB
plugin-kanban (index.js) 53.11KB 14.62KB
plugin-list (index.js) 113.11KB 27.58KB
plugin-map (index.js) 20.17KB 6.66KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 27.19KB 7.82KB
plugin-tree (index.js) 9.00KB 3.08KB
plugin-view (index.js) 85.87KB 21.12KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 73.09KB 24.34KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 2.44KB 1.21KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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

Projects

None yet

2 participants