Skip to content

fix(plugin-dashboard): metric cards spread only the toDomProps whitelist on the renderer door (objectui#4425) - #11668

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-4425-metric-dom-whitelist
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-4425-metric-dom-whitelist

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #4425. This PR lands the DOM half of the card and deletes both leak-ledger rows that name it. The authored-label half goes back to the decision inbox, measured, with options (see "Open: the authored label half" below). That is why this PR does not close the card.

Clause-②: no

What changed

  • MetricWidget and MetricCard (@object-ui/plugin-dashboard) now decide their host spread by door, through one internal helper, hostDomProps, in src/schemaHostProps.ts:
    • Renderer door. This is a render through SchemaRenderer, which injects schema on every render. It covers every dashboard KPI tile, and an app that registers the exported component under its own key. The Card receives only what toDomProps from @object-ui/core passes. That is the phase-2 widget contract (ruling comment 5270759246). It is the same executor DashboardRenderer's grid and plugin-chatbot's registrations use, and it adds no new list.
    • Direct React door. The exported components rendered directly keep the declared React.HTMLAttributes pass-through (objectui#4426), unchanged.
  • packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx: the plugin-dashboard:metric and plugin-dashboard:metric-card rows are deleted. The docblock reading moves with them: the plugin-dashboard row reads 0 targets leaking, the whole sweep reads 82 of 168, and DOCBLOCK_COUNTS.allLedgered is 82. No new row was added.
  • New pins: MetricWidget.sduiDomWhitelist-4425.test.tsx. On the renderer door, every attribute on the host must be whitelist-shaped, and no planted key may arrive: the seven MetricWidget / MetricCard spread ...props onto the DOM, emitting a schema="[object Object]" attribute on every KPI card #4357 keys, name, the zzcanary open tail, props contents, or a card's label. The deliberate pass-through must still arrive: id, data-testid from testId, aria-label from ariaLabel, and className. On the direct door, title and lang must still reach the element. A control case shows the same keys stopping on the renderer door. MetricWidget.domProps.test.tsx case (e) now asserts that name does NOT reach a div. It used to pin the deny-list's leak as a feature.
  • Docs: content/docs/guide/plugin-development.md gains "What Reaches the DOM". It states the plugin-widget contract once, cites the ruling and names the sweep as the gate, and holds a compiled tsx example. content/docs/fields/widget-props.mdx points there from its DOM pass-through paragraph.
  • Changeset .changeset/4425-metric-dom-whitelist.md: @object-ui/plugin-dashboard patch, with the behaviour change written out.

Why door-discriminated, and not the DashboardRenderer shape

The suggested route was to call toDomProps unconditionally, as DashboardRenderer does. DashboardRenderer could do that because objectui#4432 also narrowed its props interface to Pick of HTMLAttributes over SduiDomPassThroughKey, so its declaration equals what it delivers. This card's fence forbids that narrowing for MetricWidgetProps and MetricCardProps, which both extend React.HTMLAttributes. An unconditional whitelist would therefore type-check a direct consumer's title, lang or onMouseEnter and then drop it. That is declared but not delivered, created by this change. The door split gives the renderer door exactly the ruled contract and leaves the direct door's declaration true. plugin-chatbot's conversion (objectui#4431) behaves the same way: its registrations whitelist, while the exported Chatbot keeps its declared pass-through. That conversion does it with a registration wrapper. A wrapper would have changed the dashboardComponents map values and src/index.tsx, which is outside the claim's file surface. It would also have left the README's documented "register the exported component under your own key" path unwhitelisted. The discriminator schema is the one key the renderer injects on every render, and it is declared only on SchemaHostProps, the renderer's private door. If it ever stopped firing, the sweep's canaries would land again and the gate would go red.

Evidence (all on final head 9e36c2d unless stated)

  • Ledger expiry, measured, before the rows were deleted (component fix committed, rows still present). The sweep went red on exactly the two targets: plugin-dashboard:metric expected 7 attributes (name, reference_to, zzcanary, zzcanarycamel, zzcanarynum, zzcanaryobj, zzcanaryprop) and received []; plugin-dashboard:metric-card expected 9 (the same 7 plus colorvariant and label) and received []. With the rows deleted: Tests 215 passed (215).
  • Ablations go through objectstack's scripts/ablation-replace.mjs, which verifies the anchor hit, the blob change and a restore to HEAD (empty git diff HEAD) on every leg:
    • hostDomProps returning the raw rest is the pre-change runtime. Red: domProps (e), whitelist pins (a)x2, (c) and (e), and sweep metric and metric-card, 7 failed in total. Green: (b) and (d).
    • hostDomProps whitelisting both doors. Red: (d) only, so MetricWidget.domPassthrough.test.tsx does not see the direct door's beyond-the-whitelist half.
    • The docs snippet's import renamed to a non-export: check-doc-snippet-types reports plugin-development.md TS2305, 1 failed of 778. So the new block is compiled.
  • Exported surface. plugin-dashboard was built at the base sources (fc3c2cc, restore proven by blob hash) and at head. dist/index.d.ts is byte-identical. MetricWidget.d.ts and MetricCard.d.ts differ in doc comments only. schemaHostProps.d.ts gains the internal hostDomProps declaration, which is not re-exported from the entry; the package exports map serves . only.
  • Gates. Each was run on head 9e36c2d with the exit code captured before any pipe:
    • pnpm --filter '@object-ui/plugin-dashboard^...' closure build: exit 0
    • pnpm --filter @object-ui/plugin-dashboard build: exit 0
    • type-check (tsc --noEmit && tsc -p tsconfig.test.json): exit 0
    • lint: exit 0, with 0 errors
    • pnpm exec vitest run packages/plugin-dashboard/: exit 0, Test Files 171 passed (171), Tests 1691 passed, 6 skipped (1697)
    • the leak sweep: 215 passed (215)
    • check:doc-snippets, after building its --build-filter closure: exit 0, 778 of 778 block(s) judged, 0 failed
    • lockless gates, all exit 0: check:new-line-citations (0 new), check:control-bytes, check:doc-types, check:doc-fences, check:doc-example-ids, check:changeset-claims, check:pending-changeset-literals, check:test-path-roots, check:phantom-deps, check:self-import, check:component-surface-parity, check:unreferenced-sources, check:vi-mock-specifiers, check:handler-key-reads, check:doc-example-readers, check:i18n-keys, check-doc-links, check-doc-expression-carriage, check-changeset-no-major, check-changeset-fixed, check-changeset-presence
  • origin/main (f9f4a62) was merged in once, as a merge commit. Everything above ran after that merge.

Open: the authored label half (needs a decision)

The triage line reads: "An authored label on a metric card either renders as the heading or is refused loudly at authoring. ⛔ Never a DOM attribute." This PR delivers the third sentence and stops on the first. Measured:

  • objectui validate on { type: 'dashboard', widgets: [{ type: 'metric-card', id: 'kpi', value: '42', label: 'Revenue' }] } exits 0 with "Schema is valid!". The control, the same card without value, is refused at widgets → 0 → value with exit 1. The strict authoring face accepts the card too. The TypeScript twin DashboardWidgetSlotComponentSchema accepts it as well. On both Zod faces, label is a BaseSchema member the slot arm inherits, so the strict face's unknown-key closing never sees it. On the TypeScript face, it is BaseSchema.label.
  • At render, before this PR, the card drew no heading and carried label="Revenue" on its element. After this PR it draws no heading and carries no attribute. The key is silently dropped.
  • @objectstack/spec 17.6.0 declares no metric-card (its two MetricCard mentions are code comments). The registration declares title as the heading input. Every in-repo producer writes title: all eleven metric-card entries in examples/schema-catalog do.

Either way the triage line asks for, the fix leaves this card's fence:

  • A. Refuse it by name. Add a label refusal tombstone on the slot's component arm in @object-ui/types (zod), naming title as the heading. This is the objectui#9256 pattern for body / children on the same arm. It also needs label?: never on the exported DashboardWidgetSlotComponentSchema. That makes it an exported-type change, Clause-② yes, and a narrowing of the tolerant face that objectui validate runs.
  • B. Render it as the heading. This would add label as a second heading input on the registration, MetricCardProps and the slot arm. It is a new prop and input, and two spellings for one heading.
  • C. Leave it. The key would stay accepted and silently dropped.
Axis A (refuse) B (render) C (leave)
Measured business need Real error class. The sibling metric node takes label, so a mixed-up author writes it on a card. No producer writes label on a card, so this would be a speculative second spelling. —
Long-term soundness One spelling (contract-first), refused at the producer An alias accreted in the consumer (AGENTS.md #0.1) A declared-but-inert key
Keeps an AI author from writing it wrong Loud refusal at objectui validate that names title Tolerance that hides the mix-up A silent drop: the card renders headless with no warning
Startup-stage scope Narrows the surface, adds no capability and no new gate Widens the surface Nothing

Recommendation: A. A wider variant of A is a scoping question for the same decision. It would also refuse the other BaseSchema members MetricCard never reads, as objectui#9256 did for body / children. After this PR those members are equally silent on the renderer door: name, placeholder, style and data.

Acceptance notes

  • Behaviour change (also in the changeset). An authored style or name on either node, or a title on a metric node, no longer reaches the card. This follows the ruling: everything the renderer hands a widget is consumed or whitelisted. It matches what objectui#4432 did to the dashboard grid. No in-repo producer writes style, name or title onto these nodes. That was checked against the catalog metric-card entries; the widget→node mapping in DashboardRenderer builds metric nodes from options plus label, description and value.
  • Dispatch assumption corrected: toDomProps for SDUI widgets is @object-ui/core's (lifted there by objectui#4431). The @object-ui/fields export is the field-widget twin, which also passes name and disabled. This PR imports the core one, as DashboardRenderer does.
  • Observation, not filed: MetricWidget's variant: 'bare' root spreads nothing on either door. id, aria-* and data-obj-* never reach it, although MetricWidgetProps declares the pass-through. Its reach is zero: no producer emits variant: 'bare' outside MetricWidget and its ObjectMetricWidget pass-through. Owner: none.
  • NOT MEASURED: live console dashboard in Chromium (optional per dispatch; the sweep is the binding reading). Also not measured: check:docs-route-closure and the repo-wide pnpm lint / pnpm test farm, which CI runs.

Generated by Claude Code

claude added 7 commits October 6, 2026 01:26
…ist on the renderer door (objectui#4425)

`MetricWidget` and `MetricCard` destructured seven measured non-DOM props and
spread the rest onto the Card, so the open tail of authored keys neither
declares (and `name`) still reached the DOM. Rendered through SchemaRenderer
(the `schema` prop the renderer injects on every render), the host now
receives only what `@object-ui/core`'s `toDomProps` passes. A direct React
render keeps the declared HTMLAttributes pass-through unchanged, so no
exported prop surface changes.

Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL
Co-authored-by: Claude <noreply@anthropic.com>
…i#4425)

Both `plugin-dashboard:metric` and `plugin-dashboard:metric-card` now measure
an empty leak set through the real SDUI path, so the two-way ratchet held the
gate red on exactly those targets until their rows went. The docblock reading
moves with them: 82 of 168 targets leak, all in the components family.

Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL
Co-authored-by: Claude <noreply@anthropic.com>
…ad (objectui#4425)

The renderer door spreads only whitelist-shaped attributes (no open tail,
no `name`, no authored `label` on a card) while the deliberate pass-through
(`id`, `data-testid`, `aria-label`, `className`) still arrives; the direct
React door keeps delivering declared attributes beyond the whitelist.

Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL
Co-authored-by: Claude <noreply@anthropic.com>
…e door pins (objectui#4425)

Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL
Co-authored-by: Claude <noreply@anthropic.com>
…e (objectui#4425)

The custom plugin guide now says what a registered widget's host element may
receive (the `toDomProps` whitelist from `@object-ui/core`), how deliberate
pass-through is declared, and which gate holds it; the field-widget page
points there. Adds the plugin-dashboard changeset.

Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL
Co-authored-by: Claude <noreply@anthropic.com>
…nding name (objectui#4425)

`MetricWidget`'s emitted declaration prints its parameter's binding pattern, so
reading the injected `schema` under its old `_schema` binding keeps the exported
signature's declaration text identical to the base build's.

Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation plugin tests package: app-shell labels Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3319.2 KB 3330.4 KB
Main entry chunk (gzip) 152.4 KB 350 KB
Entry file index-D3Sav3xS.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) 17.22KB 6.37KB
app-shell (runtime-config.js) 22.52KB 7.86KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
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) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 574.72KB 137.94KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 232.57KB 64.51KB
fields (index.js) 262.75KB 66.62KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.18KB 11.71KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
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) 16.04KB 3.92KB
plugin-calendar (index.js) 53.17KB 15.46KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 198.39KB 47.02KB
plugin-dashboard (index.js) 143.75KB 38.87KB
plugin-designer (index.js) 231.41KB 48.84KB
plugin-detail (index.js) 247.23KB 65.04KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 238.48KB 65.51KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.72KB 29.10KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.80KB 11.71KB
plugin-tree (index.js) 14.51KB 5.15KB
plugin-view (index.js) 90.23KB 22.73KB
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.81KB 3.58KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 1.13KB 0.65KB
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) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT: PR objectui#11668, head 9e36c2d. It lands when every check on this head is green; it does not close objectui#4425

domain:ui execution seat 1 @ objectui · session_015W8GBu6sBiqus2L2xjMsAL (os-steve) · 2026-10-06T03:06Z. Reviewed against GitHub and origin/main, not against the report's prose (report 6008482196 on objectui#4425).

  • Shape.
    • The PR is a draft against main, and its assignee is os-steve. Its first line is Part of #4425, and no line pairs a closing keyword with an issue number, so the card stays open for its other half. Clause-②: no is on its own line.
    • The diff is 9 files, +434/−83, inside the claim's surface: MetricWidget.tsx, MetricCard.tsx, schemaHostProps.ts, two pin files, the leak sweep, two docs pages (content/docs/guide/plugin-development.md and fields/widget-props.mdx, not releases/) and the changeset.
  • The ruling as written (5270759246).
    • On the renderer's door, both components now hand the card only what @object-ui/core's toDomProps passes. That is the executor DashboardRenderer and plugin-chatbot already use; no third list is added.
    • The door is told by the injected schema, the one key SchemaRenderer injects on every render.
    • The direct React door keeps the declared HTMLAttributes pass-through (objectui#4426). Narrowing it at runtime would make a declared prop not delivered, and narrowing the interface is outside this card's fence.
    • The dev measured the base and head dist/index.d.ts as byte-identical. The new helper hostDomProps is not re-exported.
  • The ledger. Both LEAK_LEDGER rows naming objectui#4425 are deleted, and the sweep is 215/215 with no new row. With the fix committed and the rows still present, the sweep failed on exactly those two rows, so the deletion is earned rather than assumed.
  • Ablation.
    • Returning the raw rest turns 7 red, the sweep's two rows among them.
    • Whitelisting both doors turns only the direct-door pin red, so that pin alone holds the declared pass-through.
    • Breaking the new docs snippet's import fails the doc snippet type-check, so the new block is compiled.
  • Behaviour change, stated in the changeset, accepted. On a dashboard node, an authored name, style, or a title on a metric node no longer reaches the card element. That is the contract the ruling chose (SDUI_DOM_PASS_THROUGH_KEYS is id, className, role, tabIndex, autoFocus and three handlers, plus aria-*/data-*), and DashboardRenderer's grid container has worked this way since objectui#4432.
  • Changeset, sentence by sentence. It is @object-ui/plugin-dashboard: patch. The whitelist, the dropped keys, the unchanged direct door and the Clause-②: no line match the diff.
  • The other half of the card goes to the maintainer, measured.
    • An authored label on a metric-card is no longer a DOM attribute, but it is still not the heading.
    • objectui validate, the strict face and the TypeScript twin all accept it, because label is a BaseSchema member the slot arm inherits.
    • Refusing it or rendering it both need an exported-type or registration-input change, which this card's fence excludes. The dev stopped there, as the dispatch required.
    • After this PR lands, the seat releases the card to that decision (pm:retriage → the decision inbox), with the dev's options and four-axis reading from 6008482196.

Left as noted, not filed: MetricWidget with variant: 'bare' spreads nothing on either door, so its declared pass-through does not reach that root. The dev measured no producer of variant: 'bare' outside the component itself. This is recorded in the PR's acceptance notes.

Landing: on all-green checks on this head, ready, then auto-merge into the merge queue.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 6, 2026 03:18
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 6, 2026 03:18
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit a600924 Oct 6, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-4425-metric-dom-whitelist branch October 6, 2026 03:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation package: app-shell plugin tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants