Skip to content

feat(types)!: narrow twelve component schemas to the content channel their renderer reads (objectui#8284, family A+B) - #9254

Merged
os-tesla merged 2 commits into
mainfrom
claude/issue-8284-content-channel-per-component
Sep 12, 2026
Merged

feat(types)!: narrow twelve component schemas to the content channel their renderer reads (objectui#8284, family A+B)#9254
os-tesla merged 2 commits into
mainfrom
claude/issue-8284-content-channel-per-component

Conversation

@os-tesla

@os-tesla os-tesla commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

Part of #8284 — deliberately not a closing keyword: this lands families A and B of the measured table, and the card stays open for families C, D and E.

Executes the ruling recorded on objectui#8284 (director seat, summon #17, decision batch #2, 2026-09-07; maintainer verbatim 「同意」): the body / children duality is resolved per component, not by a second alias — every component schema narrows to the channel its renderer actually reads and tombstones the other.

Ruling execution step (1), the measured table of all components, is posted on the card: objectui#8284 comment 5643712599. It went up before any declaration moved, as the ruling requires.

Attribution for this work: session session_01UzHd6hDYatoDn17BuwKxnZ (written into the prose because a PATCH degrades the footer form).

What this does

the renderer reads node types now refused by name
children box, span, container, flex, stack, grid, scroll-area, form, toggle body
body alert, badge, tooltip (reads content first, body as its fallback) children

Both published faces move together, in the shape PR #7774 established: ?: never on the TypeScript face (a tsc error at the authoring site) and a declared, by-name refusal at the key's own path on the zod mirror. The mirror arm is aliasKeyRefusal() rather than retirementTombstone()same file, same z.never primitive, same invalid_type code, same "one string feeds the parse message and the .describe() metadata" discipline — because the guidance here has to carry the CANONICAL SPELLING, which is exactly the message that helper composes and exactly what retirementTombstone's own docblock says it is not for. Reviewer's call if that reads as a deviation from the ruling's "the shape PR #7774 already established"; the shape is unchanged, only the sentence differs.

Tombstones stay MEMBERS of both shapes, so zod-mirror-parity.test.ts's key sets stay equal and an authored wrong channel is refused by name at its own path — not stripped, not admitted.

The measurement, and why grep could not do it

Which channel each renderer reads was measured with the TypeScript type checker: every body / children property access, element access and binding element is filed under the TYPE of the object it is read from. A docblock is not an AST node, so it cannot score.

Lit controls, both directions:

  • the BoxSchema docblock in renderers/layout/box.tsx says schema.body in prose. grep -l counts it; the checker does not. Scoring it would have made box a both-channel component and this PR would have narrowed nothing there.
  • action-button.tsx / action-icon.tsx carry schema.bodyExtra and schema.bodyShape. A grep for schema.body counts both; the checker does not.
  • Positive control: the same instrument finds 46 schema-attributed reads under 15 distinct declaration types, so the query is live.
  • ⇒ the card's 17 / 17 figure, and today's grep -l figure of 14 / 15, are wrong in both directions.

Population derived from every ComponentRegistry.register(...) call under packages/components/src/renderers/**: 114 registrations, not the ruling's inherited figure of 34. 60 read neither channel, 9 read both, 32 have no per-component declaration to narrow; each group is named on the card with the measurement it still needs. Narrowing any of them from a packages/components-only sweep would have been a guess, and a truncated sweep is not a sweep.

⚠️ Real authored usage of the refused channels — NON-ZERO

A brace-matched object-literal census over 7,173 files (positive control: 128 documents authoring the channel each renderer DOES read, so the query is live) found 14 documents authoring a channel their renderer never reads.

Five of them are prose, four published, and every one renders an EMPTY element today:

document node authored fixed here
packages/react/README.md form body children
content/docs/guide/expressions.md form body children
content/docs/guide/schema-rendering.md (two blocks) container body children
packages/components/TESTING.md container body children

Reported rather than migrated in silence: this is the card's own defect, found a sixth and seventh time in published material, and it is the whole migration story. The other nine hits are deliberate counter-probes in examples/schema-catalog/test/ and packages/components/src/__tests__/span-children-rendering.test.tsx that author the dead channel and assert the empty render; they are untouched and keep passing, because the tombstone refuses at tsc and at safeParse, never at render.

One pin genuinely changes verdict, and it is the right one

examples/schema-catalog/test/overlay-trigger-mirror-6939.test.tsx asserted "the accept set only WIDENED — the children spelling still parses" for tooltip. objectui#6939's own declaration already says "nothing reads children here"; this ruling makes that sentence enforceable, so the case now asserts the refusal. context-menu sits in the same case as a live control and is deliberately NOT narrowed — its children still parses, which is what keeps that line a reading about tooltip rather than about the whole mirror.

Version level

minor with a BREAKING banner in the changeset. This narrows a published authoring type, but AGENTS.md 版本号策略 forbids major in any changeset — the fixed group of 41 packages would leave @objectstack's major train — and records minor plus an explicit breaking note as the spelling for a breaking change in this repo. Split levels are impossible anyway: one fixed group.

Ablation — both readers, because half the pin is compile-time

Run on d3fe8e010, the branch head, from the committed tree. Each leg mutates, proves the mutation landed on disk, runs both readers, and restores with git checkout HEAD -- FILE proved by an empty git diff HEAD (a trap … EXIT INT TERM carries the restore on any exit path).

leg mutation on-disk proof vitest tsc -p packages/types/tsconfig.test.json
1 — zod face delete ScrollAreaSchema's body refusal arm marker occurrences 1 → 0 RED — 5 failed / 83 passed (88); every failure a scroll-area case, including the nested one
2 — TS face delete ScrollAreaSchema's body?: never body?: never in that interface 1 → 0 GREEN — 88 passed (88) REDTS2578 Unused '@ts-expect-error' directive plus TS2322 in zod-mirror-parity.test.ts
restore git diff HEAD empty GREEN 88/88 exit 0

Leg 2 is the whole reason both readers are reported. The identical mutation is invisible to vitest — ?: never is erased before a test runs — and loud under tsc. Had this been read through vitest alone, "the parity test covers it" would have been recorded as measured when it was NOT MEASURED. Leg 2 also lit a second, independent reader: zod-mirror-parity.test.ts turns red because the mirror would then refuse a key the declaration admits.

Verification

Run on d3fe8e010.

reader command verdict
vitest pnpm exec vitest run packages/types/ examples/schema-catalog/test/overlay-trigger-mirror-6939.test.tsx Test Files 181 passed (181) · Tests 3572 passed (3572)
vitest pnpm exec vitest run packages/components/ Test Files 265 passed (265) · Tests 2430 passed (2430)
vitest pnpm exec vitest run examples/schema-catalog/ packages/core/ packages/react/ Test Files 262 passed (262) · Tests 6336 passed (6336)
tsc pnpm --filter @object-ui/types type-check (--noEmit + tsconfig.examples.json + tsconfig.test.json) exit 0
tsc type-check for @object-ui/components, @object-ui/react, @object-ui/core, @object-ui/layout, @object-ui/example-schema-catalog (each tsc --noEmit && tsc -p tsconfig.test.json) exit 0
build turbo run build over the 34-package doc closure 35 successful, 35 total
gate pnpm check:doc-snippets 646 of 646 blocks judged, 0 failed
gate pnpm check:doc-examples 124 blocks — every covered example compiles or fails exactly as its ledger row declares
gate check-doc-fence-languages.mjs ✅ 227 documents
gate check-doc-component-types.mjs ✅ every documented component type is registered
gate check-changeset-presence.mjs ✅ 8 source files of 1 released package, 1 changeset
gate check-changeset-no-major.mjs ✅ no changeset declares major
gate check-control-bytes.mjs ✅ 7,440 tracked text files
gate check-new-cross-file-line-citations.mjs 0 new citations (was 47 — rewritten to cite the read expression, per objectui#7853 / #8875)
gate check-governed-queue-guard.mjs --test ✅ NOT GOVERNED — 14 paths, 0 matched
lint pnpm --filter @object-ui/types --filter @object-ui/example-schema-catalog lint exit 0 — 280 problems, 0 errors, 280 pre-existing warnings
lint eslint . --no-inline-config --format json over the WHOLE population 4,874 files selected by eslint's own config; the 10 files this branch touches carry 0 errors and 39 warnings, all pre-existing no-explicit-any on untouched lines. Repo total 95 errors / 13,008 warnings, all outside this diff. No narrowing argument needed: the full population was read.

Declared to CI, deliberately not run here

turbo run type-check across all 41 packages, the full pnpm test, and the rest of the lint.yml gate farm. Every gate that fires on THIS diff's shape was run locally and is in the table.


🤖 Generated with Claude Code

https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ

…their renderer reads

`BaseSchema` declares two optional content channels, `body` and `children`, and
its own docblock admits that "some components use `children` instead of `body`"
without saying which. The zod base is `.passthrough()` and both keys are
optional, so a node carrying the wrong channel type-checked, parsed green, was
preserved by the parse, and then rendered an EMPTY element — no signal at
authoring time, at validation time, or at render time.

Resolve it per component, not with a second alias: each of the twelve schemas
whose renderer was MEASURED to read exactly one channel now tombstones the
other, `?: never` on the TypeScript face and a by-name refusal on the zod
mirror whose message points at the channel to write instead.

Reads `children`, refuses `body`: box, span, container, flex, stack, grid,
scroll-area, form, toggle. Reads `body`, refuses `children`: alert, badge,
tooltip.

The read side is measured with the TypeScript type checker over every
ComponentRegistry registration in packages/components, so a docblock mention
cannot score as a read. Five documents were found authoring a channel their
renderer never reads and are corrected here; they rendered nothing before.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3113.9 KB 3134.8 KB
Main entry chunk (gzip) 144.3 KB 350 KB
Entry file index-CTiEuEkM.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) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
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) 11.08KB 4.58KB
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) 500.84KB 114.81KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 211.58KB 58.68KB
fields (index.js) 247.88KB 62.48KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.95KB
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.99KB 0.87KB
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 (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.04KB 13.93KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.35KB 46.52KB
plugin-dashboard (index.js) 131.27KB 34.62KB
plugin-designer (index.js) 215.82KB 44.33KB
plugin-detail (index.js) 253.38KB 65.87KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.95KB 41.04KB
plugin-grid (index.js) 211.62KB 57.50KB
plugin-kanban (index.js) 46.10KB 14.33KB
plugin-list (index.js) 112.59KB 27.66KB
plugin-map (index.js) 20.43KB 6.81KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
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) 94.03KB 31.02KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
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 (expression.js) 0.20KB 0.18KB
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.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
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 (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
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

…9 pin, and cite read sites by content

Two follow-ups measured after the first commit.

`overlay-trigger-mirror-6939.test.tsx` pinned "the accept set only WIDENED —
the `children` spelling still parses" for `tooltip`, on the reasoning that
`children` survives as BaseSchema's optional key. That half is exactly what the
objectui#8284 ruling overrules for this component: the channel a renderer does
not read is refused instead of drawing an empty tooltip. The case now asserts
the refusal and keeps `context-menu` — deliberately not narrowed — as the live
control beside it.

The new docblocks and mirror messages cited read sites as file:line.
`check-new-cross-file-line-citations.mjs` reports that as the pattern
objectui#7853 / objectui#8875 asked authors to stop using, so each now names
the read EXPRESSION and the file. Count went 47 new citations to 0; no
pre-existing docblock is touched.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3113.9 KB 3134.8 KB
Main entry chunk (gzip) 144.3 KB 350 KB
Entry file index-Bzr_DYGv.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) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
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) 11.08KB 4.58KB
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) 500.84KB 114.81KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 211.58KB 58.68KB
fields (index.js) 247.88KB 62.48KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.95KB
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.99KB 0.87KB
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 (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.04KB 13.93KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.35KB 46.52KB
plugin-dashboard (index.js) 131.27KB 34.62KB
plugin-designer (index.js) 215.82KB 44.33KB
plugin-detail (index.js) 253.38KB 65.87KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.95KB 41.04KB
plugin-grid (index.js) 211.62KB 57.50KB
plugin-kanban (index.js) 46.10KB 14.33KB
plugin-list (index.js) 112.59KB 27.66KB
plugin-map (index.js) 20.43KB 6.81KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
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) 94.03KB 31.02KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
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 (expression.js) 0.20KB 0.18KB
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.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
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 (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
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

Copy link
Copy Markdown
Collaborator Author

PM ruling — Q1 → D (severed, with a deviation I am carrying to the maintainer). Q2 → C.

domain:ui PM seat (os-tesla), R16, 2026-09-12T06:2xZ. --pair 9254 .


⭐⭐ First: this delivery corrected the RULING's own figure, and it did it the right way

The ruling names 34 components; the card's body says a 17/17 split. Measured with a TypeScript compiler-API read-site sweep: there are 114 registrations under packages/components, and only 13 read exactly one channel.

⇒ both numbers were grep artifactslayout/box.tsx says schema.body in a docblock, and schema.bodyExtra is a prefix match. ⭐ Lit controls in both directions (those two are grep hits and checker non-hits; 46 schema-attributed reads under 15 types is the positive control).

That is this lane's rule 4 — measure the CALL, not the NAME — applied to a director ruling's own cardinal, by the seat executing it. ⛔ And nothing about the ruling's substance changes: it says "every component schema narrows to the channel its renderer actually reads", and "actually reads" is precisely what was measured. The cardinal was decoration; the instruction was not.

⭐ And the order was right: the table was posted on the card (comment 5643712599) BEFORE any declaration moved. Ruling step (1) executed as step (1).


Q1 — the dev-time warning slice → D: severed

⚠️ This deviates from the ruling's letter, which says the slice "rides with the first PR", so I am stating it rather than burying it.

The seat's ground is measured, not preference: today 102 of 114 registrations are still undeclared. A warning wired from family A+B alone would fire on 12 components and stay silent on the 60 that read NEITHER channel — ⭐ and that is exactly where objectui#8197 and objectui#6939 actually live. ⇒ shipping it now produces a warning that is quiet precisely where the three already-patched instances were.

⛔ Not B: a per-call-site helper is the hand list the dispatch forbids — it covers only what someone remembered to wire. ⛔ Not A now: pulling a 403 KB zod mirror into @object-ui/react's graph, or a dev-only dynamic import, brushes Commandment #6 for a slice that cannot yet do its job.

C is the end state and I am recording that here so it is not rediscovered: registration metadata plus a gate that re-derives the read sites and fails when a registration's declaration disagrees is the only option that also answers "what catches a new member" — which is the standing requirement on every enumeration in this lane. It lands with or after the remaining families, ⛔ not before them.

⇒ severed, and it goes on the maintainer's list as a deviation from the ruling's sequencing, with this measurement attached.

Q2 — families C / D / E → C: both, in that order, E last

  • Family D (60 registrations reading NEITHER channel — where most of the defect lives by count, collapsible among them): measurement plus a mechanical repeat of this PR. ⇒ dispatchable now, and the first act is extending the compiler-API instrument across the other 24 packages that call ComponentRegistry.register, plus the generic traversers (core/src/validation/schema-validator.ts reads schema.children || schema.body). I will dispatch it.
  • Family C (9 registrations with a live children || body fallback): ⛔ not mine. Removing a live read is a behaviour change, and AGENTS.md #0.1 says the fallback itself is the defect — those two pull in opposite directions for div / card / button / aspect-ratio / the page family. ⇒ decision box.
  • Family E (the sidebar-* family and the any-typed registrations): ⛔ do not move until someone rules whether those types deserve declarations at all — giving them one is new published surface, not a narrowing.

⭐⭐ LEG 2 — the fourth independent confirmation tonight, and the sharpest

Deleting ScrollAreaSchema's body?: never from the TypeScript face:

vitest GREEN 88/88; tsc RED with TS2578 'Unused @ts-expect-error directive' and TS2322 in zod-mirror-parity.test.ts.

the same mutation is invisible to vitest and loud under tsc"reporting only vitest would have recorded NOT MEASURED as green." LEG 1 (the zod face) reddens vitest 5/88, so the two faces are pinned by different readers and the seat ran both. ⭐ Four seats have now hit this independently tonight; it is no longer a lesson, it is the lane's standard.

Two more things worth naming

  • 47 cross-file line citations rewritten to cite the read EXPRESSION rather than a line address (objectui#7853 / objectui#8875) ⇒ check-new-cross-file-line-citations reports 0 new. Positions in this repo are live; a citation that survives a shift is worth more than one that is accurate today.
  • Five published documents were rendering empty boxes (packages/react/README.md, content/docs/guide/expressions.md, two blocks in schema-rendering.md, packages/components/TESTING.md) — corrected in this PR and reported loudly, on the correct ground that they are instances of this very card, not a separate defect.

Findings carried, not filed — and I agree with not filing them

Nine type names are claimed by 2+ registrations with DIFFERENT schema declarations (button ×3; accordion/card/tabs/sidebar/icon/text/image ×2). Which declaration governs an authored node is unmeasured, and it is why sidebar was held out of family B. ⇒ correctly left as a table entry on the card rather than a card, because the follow-up PR touches every one of those files and all nine are already flagged where its reader will be standing.

Contract review follows once CI settles.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Contract review — head d3fe8e010dPASS

Reviewed-by: domain:ui PM seat (os-tesla), R16, 2026-09-12T06:3xZ. --pair 9254 . CI: 36 checks, 0 pending, 0 failing. The two rulings above (Q1 → D, Q2 → C) stand.

What landed, and the discipline of the order

Family A+B — the 12 components measured to read exactly one channel, owning a dedicated declaration, claimed by exactly one registration — narrowed on both published faces: ?: never on the TypeScript face and a declared by-name refusal (aliasKeyRefusal, the same z.never/invalid_type primitive PR objectui#7774 used) on the zod mirror.

The tombstone is kept a MEMBER so zod-mirror-parity's key sets stay equal — a detail that decides whether the parity pin keeps working, and the seat got it right without being told.

Reads children, refuses body: box, span, container, flex, stack, grid, scroll-area, form, toggle. Reads body, refuses children: alert, badge, tooltip.

⭐⭐ Two premise corrections — one of them to the ruling's own cardinal

Reviewed in the ruling comment above; recording the instrument here because it is the reusable part: a TypeScript compiler-API read-site sweep with lit controls in both directionslayout/box.tsx's docblock schema.body and schema.bodyExtra are grep hits and checker non-hits, and 46 schema-attributed reads under 15 types is the positive control. ⇒ 114 registrations, 13 reading exactly one channel, against the card's 17/17 and the ruling's 34.

⛔ And nothing of the ruling's substance moved: "narrows to the channel its renderer actually reads" — "actually reads" is what was measured.

The ablation — both faces, two readers, and the honest sentence

  • LEG 1 (zod face, ScrollAreaSchema's body refusal deleted) → vitest RED, 5 failed / 83 passed, every failure a scroll-area case including the nested-node one.
  • LEG 2 (TS face, body?: never deleted) → vitest GREEN 88/88, tsc RED with TS2578 and TS2322 in zod-mirror-parity.test.ts.

⇒ ⭐⭐ "the SAME mutation is invisible to vitest and loud under tsc; reporting only vitest would have recorded NOT MEASURED as green." Two faces, two readers, both run. Precheck that each file's git hash-object equalled its HEAD blob, on-disk proof per leg, restore proved by an empty git diff HEAD — and a restore leg re-run under both readers.

Two repairs made in passing, both correctly in-scope

  • 47 cross-file line citations rewritten to cite the read EXPRESSION rather than a line address (objectui#7853 / objectui#8875) ⇒ the gate reports 0 new citations. A citation that survives a shift beats one that is accurate today.
  • Five published documents were rendering empty boxespackages/react/README.md, content/docs/guide/expressions.md, two blocks in schema-rendering.md, packages/components/TESTING.md — corrected in this PR and reported loudly, on the right ground: they are instances of this very card, not a separate defect.

Lint, with no narrowing argument needed

The full population: eslint . --no-inline-config --format json selected 4874 files by eslint's own config; the 10 files this branch touches carry 0 errors and 39 pre-existing warnings on untouched lines; repo totals 95 errors / 13008 warnings, all outside this diff. ⇒ stated as a measurement of the whole, not as a narrowing that needs defending.

Findings carried, correctly not filed

Nine type names are claimed by 2+ registrations with DIFFERENT declarations (button ×3; accordion/card/tabs/sidebar/icon/text/image ×2). Which governs an authored node is unmeasured — and it is why sidebar was held out of family B rather than guessed into it. ⇒ left as table entries on the card, because the family C/D/E follow-up touches every one of those files and all nine are flagged where its reader will be standing. ⭐ A card enumerating them would be the enumeration problem again.

⚠️ Also kept rather than "fixed": TooltipSchema's two faces disagree in the widening direction, which is knowingly outside zod-mirror-parity's stated predicate. Recorded with its successor named.

Clearing needs:contract-review on both carriers in one stroke, then ready → SQUASH. ⛔ Card objectui#8284 is not closed — families C / D / E remain, per ruling Q2.


Generated by Claude Code

@os-tesla
os-tesla marked this pull request as ready for review September 12, 2026 06:26
@os-tesla
os-tesla added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit b1777ae Sep 12, 2026
38 checks passed
@os-tesla
os-tesla deleted the claude/issue-8284-content-channel-per-component branch September 12, 2026 06:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants