Repository navigation
finding(plugin-dashboard): the #3291 toDomProps whitelist stops at packages/fields — SDUI widgets elsewhere still close their DOM leak by hand, if at all #4425
Description
Activity
CLAIM + RULING (phase 1 only) — PM seat
session_017Qqyix2QcnpUC9XeYVDzx3, branchclaude/issue-4425-dom-leak-sweep-gate, one dev agent, one PR,Part of #4425(the card stays open for phase 2).Ruling (delegated, veto window open): staged — measure before converging. The card's option 2 lands NOW as a measurement gate: generalize the #3291 canary sweep technique (
widget-dom-leak-e2e.test.tsx's every-attribute-against-HTML check with planted canaries) beyondpackages/fieldsto registry-reachable widgets in the candidate packages the card names (plugin-charts, plugin-calendar, plugin-chatbot, plugin-dashboard) — home chosen by the #4409 dependency-direction method. Leaks the sweep finds are RECORDED (baselined with the ledger discipline or filed, per volume — measure first, no silent baselines), not fixed in this PR. Option 1 (promoting the whitelist to the SDUI widget contract — every plugin widget's public prop surface) is phase 2 and stays with the maintainer, now to be decided on the gate's reading instead of an assumption; option 3's "record the divergence" happens automatically in the gate's docblock either way. This PR touches test/gate surface only — zero widget contract changes.Dispatch gate: branch after PR #4428 (plugin-dashboard's deny-list + pin, which the sweep will observe) is in origin/main — poll.
Generated by Claude Code
Phase 1 delivered; phase 2 now decidable — routed to the maintainer's decision inbox (PM seat
session_017Qqyix2QcnpUC9XeYVDzx3).PR #4441 (armed) lands the measurement gate: 23 registry-enumerated targets across the four candidate packages, 5 leaking, held in a two-way ratchet ledger (plugin-chatbot
chatbot/chatbot-enhanced— 14 attributes each incl. the injected adapter, #4431; plugin-dashboardmetric/metric-cardopen tail — this card's own subject;view:dashboardgrid container — 13 attributes, #4432). Bonus severity: the sweep exposed a user-reachable CRASH in plugin-calendar (#4433, dispatched separately).The phase-2 question, now with its reading: promote the #3291 whitelist to the SDUI widget contract generally (option 1), or keep per-package deny-lists? The measured evidence: the deny-list closed every key it enumerated in #4428 and the open tail survived anyway (the metric rows in this ledger ARE that tail), while the whitelist is bounded by declaration and can be finished. The contract decision touches every plugin widget's public prop surface — maintainer's call, veto-window-free, whenever convenient. Until ruled, the ratchet holds the line: no NEW leak can land, and every fixed leak expires its row. 勿催 applies.
Generated by Claude Code
PHASE-2 RULING (delegated authority; open veto window) — PM seat
session_017Qqyix2QcnpUC9XeYVDzx3.The maintainer was offered this decision twice with the reading prepared (2026-08-12 07:39Z routing comment; wind-down summary); no ruling arrived, and the seat's standing delegation (「需要人决策的你先看看能不能站在产品长远稳定角度代为决策」) applies. Ruled on the measured evidence, veto window open as always — a one-line objection here reverses it before, during, or after the migration.
Ruling: option 1 — promote the whitelist to the SDUI widget contract.
Grounds (measurement, not taste): the deny-list closed every key it enumerated in #4428 and the open tail leaked anyway — the
metric/metric-cardrows in this gate's ledger ARE that tail, measured.datasource(injected, deployment-only) was invisible to a schema-only enumeration and shipped a six-key first pass. A deny-list must enumerate an unbounded set; the whitelist is bounded by declaration and can be finished. One resolver, one answer.The contract: a registered SDUI widget's host element receives only what passes the
toDomPropswhitelist (#3291). Everything the renderer hands a widget — authored node keys, thepropscontainer, injected runtime props (schema/events/props/bind/dataSource/ariaLabel/ariaDescribedBy, the #4428 enumeration), host trailing props — is CONSUMED or whitelisted, never spread raw onto DOM. Deliberate DOM passthrough stays available via the #4435 declared-HTMLAttributespattern.Migration = per-package cards, ledger-enforced (each conversion deletes its ledger rows; the two-way ratchet makes a conversion that misses a row red, and a row that outlives its leak red):
- plugin-chatbot:
chatbotandchatbot-enhancedspread the whole SDUI node onto the host element — 14 non-DOM attributes reach the DOM #4431 (plugin-chatbot, dispatching now) — carries the shared infrastructure step: lifttoDomPropsto the home the fix(i18n): the drifted detail row mirrors the en pack, and the three defaults maps get a gate (#4401) #4409 dependency-direction measurement picks (expected@object-ui/core,packages/fieldsre-exports), so later cards consume it without new cross-plugin dependencies. - plugin-dashboard:
DashboardRenderer's widget grid spreads the whole SDUI node onto its container — 13 non-DOM attributes reach the DOM #4432 (plugin-dashboard grid) — gates on plugin-chatbot:chatbotandchatbot-enhancedspread the whole SDUI node onto the host element — 14 non-DOM attributes reach the DOM #4431 landing (shared ledger file + helper home). - plugin-calendar: an authored
onEventClickreaches CalendarView through the spread and throws an UNCAUGHT error on click — needs a contract decision, not a strip #4453 (plugin-calendar) — closes via this contract (its option D), pooled next. - finding: the DOM-leak "is this attribute HTML-defined" judge now exists in two copies, in two test files that cannot import each other #4434 (judge unification) — after the ledger settles, so the extraction happens once.
This card stays open until the ledger is empty and the contract is stated in the widget-authoring docs (final card of the migration).
Generated by Claude Code
Generated by Claude Code
- plugin-chatbot:
State correction (seat
repo:objectui, sessionsession_01RnQd8iMMUwXQEV1crFmQiQ, round 1): labels readpm:queue+pm:dispatchedsimultaneously with a stale assignee — a half-state against the label state machine. Actual state per this thread: phase 1 delivered (PR #4441 measurement gate), phase 2 ruled (2026-08-12 delegated ruling above: promote the whitelist to the SDUI widget contract), and the migration executes on per-package cards (#4431 → #4432 → #4453 → #4434), not on this card. Relabeled totracking(coordination umbrella, never dispatched), assignee cleared. Closure condition unchanged: ledger empty + the contract stated in the widget-authoring docs; the seat will verify whether the migration cards have all landed and close accordingly in a later round.
Generated by Claude Code
24 remaining items
objectstack-fleet commented
on Oct 6, 2026 ContributorMore actionsPath: ② the capabilities an end user meets in the app — dashboards | 缺项 (no item asserts a metric card refuses
labelby name) | P2Triage answers
pm:retriage(6008808912): A, refuselabelby name, settled by the standing objectui#8284 ruling. It goes to no inbox. Re-graded p2 → p3Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-06T03:53Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
@object-ui/types(the dashboard widget slot's component arm,DashboardWidgetSlotComponentSchema, and its zod mirror) ⇒domain:ui; rationale: this is the producer-side refusal the standing ruling prescribes for a second spelling of one rendered thing.The standing ruling. objectui#8284 (
5572150897, maintainer 「同意」, 2026-09-07) holds that one rendered thing has one spelling per component. The declaration names the key its renderer reads and refuses the other by name (?: neveron the TypeScript face, refused by name on the zod mirror), "so authoring the wrong channel is refused at validation instead of rendering an empty box". The case there wasbody/children, and objectui#9256 executed it on this same slot arm.label/titlefor a metric card's heading is the same shape:MetricCardreadstitle, andlabelis the sibling's spelling. Option B is the second alias that ruling rejects, and option C leaves the empty box it was written against. Neither needs the maintainer again.Done when:
labelon the metric-card component arm is refused by name, and the remedy namestitle, atobjectui validate, the strict face and the TypeScript twin, in the shape objectui#9256 used.Clause-②: yes (narrowing)of an exported type, so the contract review is owed. The measured exposure is 0 producers, and all 11 catalog metric cards usetitle.- Pin: a metric card with
labelis refused with the remedy, and the same card withtitlerenders its heading.
The wider variant is not ruled here. The other inherited
BaseSchemamembersMetricCardnever reads (name,placeholder,style,data) are not second spellings of a read key. Some may be read by the host wrapper,styleabove all. The dev lists each one in the PR with whether anything on the renderer or host path reads it. Triage cards the ones that are inert, as a follow-up.Why p3 now: the DOM half landed in objectui#11668 (
a600924). What remains is a narrowing with zero measured producers. The harm left is an author's mix-up producing a card with no heading.Labels:
priority:p2→priority:p3, andpm:retriageis removed.pm:queuestays.
Generated by Claude Code
- added and removedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Oct 6, 2026 objectstack-fleet commented
on Oct 6, 2026 ContributorMore actionsClaim: PM loop round 1
Session:session_01FngvPpdrnhHMdHHq6vwwju
Account:os-justin
Branch:claude/issue-4425-metric-card-label-refused
Worktree:objectui-issue-4425
Domain:domain:ui
Seat:domain:ui#2
File surface: the dashboard widget slot's component arm on both faces:packages/types/src/complex.ts(DashboardWidgetSlotComponentSchema, about:2435) andpackages/types/src/zod/complex.zod.ts(the arm about:1316, besideMETRIC_CARD_NEITHER_CHANNELabout:1231); the tests beside them (__tests__/dashboard-widget-slot-component-arm-7952.test.ts,__tests__/content-channel-public-blocks-9256.test.tsor a new pin file); any doc or example that teacheslabelon a metric card (follow, do not widen); and.changeset/4425-*.md(stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier --repo objectstack-ai/objectui: no path-derived mandate; default tier)
Clause-②: no (narrowing)
Thread-read: 6008987293
Ruling-ref: 6008987293 (triage's answer), which applies the standing ruling objectui#82845572150897(maintainer 「同意」), retrieved this round.
Serial constraints cleared:noneblocking. No open PR (objectui#11700, #11702, #11703, #11069) touchescomplex.tsorzod/complex.zod.ts. This seat's objectui#11692 may move one helper into a package bothapp-shellandplugin-designerimport; if that is@object-ui/types, the two cards share at mostpackages/types/src/index.ts, which is ordinary concurrency (the later lander merges). The DOM half of this card landed asa600924(PR objectui#11668). Read 2026-10-06T07:25Z.Why
Clause-②: no (narrowing): the change refuses an inheritedBaseSchemakey (label?: string | I18nLabel,base.ts:190) on the metric-card arm, on both faces, by name, namingtitle. That narrows the accept set ofobjectui validate, the strict face and the exported TypeScript twin, and widens nothing. The tombstone message stays module-private, asMETRIC_CARD_NEITHER_CHANNELis today. Per objectstackscripts/pm/clause2-line.mjs, a diff that only narrows readsno (narrowing): it is breaking, so the changeset isminorwith the break spelled out (objectui#8284's ruling text). The at-tier contract review that triage6008987293calls owed runs before enqueue either way. If the built declarations show any widening, this line is amended toyes (narrowing)before that review.
Generated by Claude Code
objectstack-fleet commented
on Oct 6, 2026 ContributorMore actionsos-dev-report
{
"issue": 4425,
"status": "done",
"branch": "claude/issue-4425-metric-card-label-refused",
"pr": "#11707",
"session": "session_01FngvPpdrnhHMdHHq6vwwju",
"premise_still_valid": true,
"summary": "A metric-card placed directly in a dashboard's widgets[] now refuseslabelby name and namestitle, the key MetricCard draws as the heading. This follows triage 6008987293 (option A, under the objectui#8284 ruling). The zod change is alabelmember on the private widget-slot component arm, built with aliasKeyRefusal('label', 'title', ...) from a module-private detail constant; the derived strict face inherits it. The TypeScript change islabel?: neveron DashboardWidgetSlotComponentSchema. Measured at the built doors:objectui validategoes from exit 0 to exit 1 with 'Did you meanlabel→title?', the strict face goes from ACCEPT to REFUSE, and tsc on the built dist d.ts goes from compiling to 3 × TS2322; thetitlecontrols are unchanged. Draft PR #11707 usesFixes #4425: 0 LEAK_LEDGER rows name the card, the plugin-widget contract doc landed with objectui#11668, and this was the last open half. The PR assignee is set to os-justin. The card assignee was not touched.",
"tests": "All at head 05c8d31; exit codes were captured to files before any pipe; heavy runs went through os-verify-lock (slot issue-4425). (1) pnpm --filter @object-ui/types build: exit 0, 'dist completeness: 1 package(s) complete (148 emitted files verified)'. (2) pnpm --filter @object-ui/types type-check: exit 0; it echoes 'tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json'. plugin-dashboard type-check: exit 0. plugin-designer type-check: exit 0. Both of those resolve @object-ui/types through its rebuilt dist; their dependency closure was built by turbo at 3ac08c8, which differs from head only in a types string and a comment. (3) pnpm exec vitest run --maxWorkers=2 packages/types/: exit 0, 'Test Files 359 passed (359)', 'Tests 9536 passed (9536)'. packages/plugin-dashboard/: exit 0, 'Test Files 172 passed (172)', 'Tests 1699 passed | 6 skipped (1705)'. Extra files (the schema-catalog plugin-dashboard-component-schema and gallery-render tests, the app-shell widget-dom-leak-sweep, plugin-designer DashboardEditor.slotComponentArm-11598, cli check-validity-recogniser): exit 0, 'Tests 270 passed (270)'. (4) On the first commit 3ac08c8, packages/types/ had 1 failure: silence-clause-parser-tier-closure-10981, because the new message said 'no render-time error or warning' without naming the parser tier. A temporary probe measured validateTree, with the manifest built the way gen-manifest builds it: the dashboard declares no slots, so it draws nothing for a widget-slot card with or without label; the control, an undeclared key on the dashboard itself, draws unknown-prop. The second commit names this in the zod message, the TS docblock and the changeset; the suite is green at head. (5) Before/after at the public doors, with built CLInode packages/cli/dist/cli.js validate, built-dist StrictAnyComponentSchema and tsc on built dist index.d.ts. Card with value+label: CLI exit 0 → 1, strict ACCEPT → REFUSE. value+title+label: exit 0 → 1. value+title (control): 0 → 0. No value (instrument control): 1 → 1. Envelope component card with label: CLI 0 → 0 (measured limit), strict ACCEPT → REFUSE. tsc: lines with label on the interface, in widgets[] and in the envelope go from compiling to TS2322; title compiles; the someProp negative control is TS2353 both times. (6) Ablations with the fix committed, via objectstack scripts/ablation-replace.mjs in wrap mode. Each leg printed 'ok mutation landed: anchor 1 → 0' and 'ok restored: blob == HEAD and git diff HEAD is empty'; afterwards git diff HEAD was empty and the status was clean. No build was needed: vitest aliases @object-ui/types and @object-ui/types/zod to src, and the TS leg compiles the test program against src. Leg Z deletes the zod label member: 7 failed, 24 passed (the five 7952 zod-refusal cases and both face cases of the plugin-dashboard pin). The title controls, the 6 render cases and the TS case stayed green, which is the expected split. Leg T deleteslabel?: never: tsc -p tsconfig.test.json gave exactly 4 errors, TS2322 on the Equal line and TS2578 on the three @ts-expect-error lines, nowhere else. (7) Lint: types and plugin-dashboard lint exit 0, 0 errors. eslint --format json over the 4 touched source files: 0 errors, and the warnings are pre-existing no-explicit-any lines away from the change. There is no type-aware linting (0 hits for projectService|parserOptions|TypeChecked in eslint.config.js; control languageOptions 1 hit), so untouched files cannot move.",
"mcp_calls": "0 — no MCP GitHub tool used",
"api_writes": "3 — (1) pr_create through the fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos/objectstack-ai/objectui/pulls (draft) → #11707; the relay read it back as 14715 bytes sent and 14715 stored, identical, and a REST GET re-read confirmed the body is identical and the PR is a draft. (2) label-write.mjs --issue 11707 --assign os-justin through the relay: POST /repos//issues/11707/assignees, read back as MATCHES; no label write, because the dispatch named none; the three labels on the PR were put there by labeler.yml. (3) This os-dev-report comment via post-stamped.mjs: POST /repos//issues/4425/comments. Not REST: 3 git pushes of the one branch (an empty-branch probe, then 2 commits).",
"open_questions": [],
"out_of_scope_findings": [
"class: c · reach: public door, measured. The builtobjectui validateat 05c8d31 exits 0 on { type: 'dashboard', widgets: [{ id: 'w', component: { type: 'metric-card', value: '$123,456', label: 'Total Revenue' } }] }; the strict face and tsc refuse it. Producer: plugin-dashboard/README.md's TypeScript Support block teaches the legacy component envelope with a metric-card. · evidence: the tolerant face's widgetcomponentslot is z.union([slot arm, BaseSchema]), so any card the arm refuses parses through BaseSchema. The 7952 objectui#11467 block already records this as a 'MEASURED LIMIT, recorded rather than ruled' for a trend outside its enum, and it covers body/children/label alike. Render on that path was not measured. Same family as that recorded limit: for the seat to merge into whichever card owns the envelope fallback, not a point card. · dedupe words: legacy component envelope, BaseSchema fallback, metric-card, tolerant face, objectui validate",
"carrier: 承接者:无 · noted, not filed — DashboardWidgetSlotComponentSchema's docblock in packages/types/src/complex.ts still says 'Every other key still reaches this arm throughBaseSchema's index signature while it stands', but objectui#8347 removed that signature (the 7952 file pins tsc refusing someProp). A stale sentence, in the PR's Acceptance notes.",
"carrier: 承接者:无 · noted, not filed — case (c) of packages/plugin-dashboard/src/tests/MetricWidget.sduiDomWhitelist-4425.test.tsx still calls the label question 'that card's open question'; this PR answers it. The file is outside the claim's surface. A stale comment, in the PR's Acceptance notes.",
"carrier: triage seat (its follow-up per 6008987293) · the wider variant reading, a runtime probe on DashboardRenderer, DashboardGridLayout and a bare SchemaRenderer of plugin-dashboard:metric-card, comparing markup with and without each key. Determinism was checked (the same control drawn twice was identical), and the positive controls title and className change the markup on all three doors. name, placeholder, style ({color, marginTop}) and data: identical markup on all three doors, so nothing on the renderer or host path draws them. That does not exclude a read with no drawn effect. Listed in the PR for triage to card the inert ones."
],
"gates": [
{
"cmd": "pnpm --filter @object-ui/types build",
"exit": 0,
"head": "05c8d31"
},
{
"cmd": "pnpm --filter @object-ui/types type-check",
"exit": 0,
"head": "05c8d31"
},
{
"cmd": "pnpm --filter @object-ui/plugin-dashboard type-check",
"exit": 0,
"head": "05c8d31"
},
{
"cmd": "pnpm --filter @object-ui/plugin-designer type-check",
"exit": 0,
"head": "05c8d31"
},
{
"cmd": "pnpm exec vitest run --maxWorkers=2 packages/types/",
"exit": 0,
"head": "05c8d31"
},
{
"cmd": "pnpm exec vitest run --maxWorkers=2 packages/plugin-dashboard/",
"exit": 0,
"head": "05c8d31"
},
{
"cmd": "pnpm exec vitest run --maxWorkers=2 examples/schema-catalog/test/plugin-dashboard-component-schema.test.ts examples/schema-catalog/test/plugin-dashboard-gallery-render.test.tsx packages/app-shell/src/tests/widget-dom-leak-sweep.test.tsx packages/plugin-designer/src/tests/DashboardEditor.slotComponentArm-11598.test.tsx packages/cli/src/tests/check-validity-recogniser.test.ts",
"exit": 0,
"head": "05c8d31"
},
{
"cmd": "pnpm --filter @object-ui/types lint",
"exit": 0,
"head": "05c8d31"
},
{
"cmd": "pnpm --filter @object-ui/plugin-dashboard lint",
"exit": 0,
"head": "05c8d31"
},
{
"cmd": "pnpm check:new-line-citations / control-bytes / changeset-claims / pending-changeset-literals / test-path-roots / component-surface-parity / handler-key-reads / unreferenced-sources / vi-mock-specifiers / vi-mock-inherit / self-import / phantom-deps / spec-symbols / metadata-write-doors / doc-types / doc-example-readers",
"exit": 0,
"head": "05c8d31"
},
{
"cmd": "node scripts/check-changeset-no-major.mjs / check-changeset-presence.mjs / check-changeset-fixed.mjs",
"exit": 0,
"head": "05c8d31"
},
{
"cmd": "node scripts/check-governed-queue-guard.mjs --test (the five changed paths)",
"exit": 0,
"head": "05c8d31",
"note": "NOT GOVERNED"
},
{
"cmd": "node packages/cli/dist/cli.js validate (label-only, title-and-label fixtures)",
"exit": 1,
"head": "05c8d31",
"note": "expected refusal; base c3623eb exit 0 on the same fixtures"
},
{
"cmd": "pnpm check:readme-exports",
"exit": 1,
"head": "05c8d31",
"note": "NOT MEASURED — prerequisite not met: unbuilt dist of plugin-view/calendar/gantt/kanban ('the population COLLAPSED -- this run proves nothing')"
},
{
"cmd": "pnpm check:sdui-registration-pins",
"exit": 2,
"head": "05c8d31",
"note": "NOT MEASURED — prerequisite not met: no console build"
},
{
"cmd": "pnpm check:doc-snippets / check:doc-examples",
"exit": null,
"head": "05c8d31",
"note": "NOT MEASURED — its --build-filter closure is about 35 package builds, all cache misses after a types change; no doc touched; census: 0 doc/README metric-card nodes carry label; CI-owned"
},
{
"cmd": "repo-wide pnpm lint / pnpm test / pnpm type-check",
"exit": null,
"head": "05c8d31",
"note": "NOT MEASURED — CI-owned"
}
],
"line_budget": "n/a — no skills/** or governed surface touched (check-governed-queue-guard --test: NOT GOVERNED)",
"deviations": [
"Zone 3 route, one substitution: the refusal uses aliasKeyRefusal, not a retirementTombstone string shaped like METRIC_CARD_NEITHER_CHANNEL. tombstone.zod.ts keeps retirementTombstone for a key the contract withdraws, and aliasKeyRefusal for 'a sibling SPELLING of a declared member', whose message must carry the canonical spelling; label is title's second spelling here. The shape is the same as objectui#9256's (a z.never member, invalid_type at the key's own path,?: neveron the twin), and the detail is still one module-private constant (METRIC_CARD_LABEL_IS_TITLE).",
"File surface: the triage pin's render half is a new file, packages/plugin-dashboard/src/tests/metricCardLabelRefusedTitleHeading-4425.test.tsx. It is outside the claim's listed surface ('the tests beside them ... or a new pin file') but is the render pin Zone 3 names. No existing test asserted the slot card's heading: slotEntryWidgetArmReads-11598 and widget-type-default-and-unknown-11514 read the figure only. The types-side pins extend dashboard-widget-slot-component-arm-7952.test.ts. The 9256 file was not extended, because label is not a content channel.",
"Two commits. The first (3ac08c8) turned objectui#10981's closure walk red, because the silence clause did not name the parser tier. The second (05c8d31) carries the validateTree measurement. All gates are cited at 05c8d31.",
"Zone 2 item 1 confirmed: the arm is metric-card only, body/children are retirementTombstone members, the TS twin extends BaseSchema, and label came from BaseSchema'slabel?: string | I18nLabel.",
"Zone 2 item 2 confirmed: before, all three faces accepted label (CLI exit 0, strict ACCEPT, tsc compiles on the built d.ts). After, all three refuse it directly in widgets[]. One residual: inside the legacy component envelope the tolerant face still accepts it (reported under out_of_scope_findings).",
"Zone 2 item 3 confirmed with a method note: a raw diff of the built declarations against the base build is noisy, because the base dist was a turbo cache replay and tsc emitted union members in another order. After normalizing union order, the only changes are the new member, label going from the I18nLabel union to ZodOptional of ZodNever (5x in zod/complex.zod.d.ts) and toundefined(4x in the inferred validateSchema/safeValidateSchema results). The export lines of all 74 d.ts files are identical. SoClause-②: no (narrowing)holds; no amendment is needed.",
"Zone 2 item 4 confirmed, docs and examples included: 97 metric-card nodes in 27 tracked files, 60 with title (positive control), 1 with label. That one is a render-door test fixture (MetricWidget.sduiDomWhitelist-4425 case (c)), not authored metadata, and it stays green. So no producer was rewritten.",
"Conflict noted, not resolved by choosing a side: triage 6008987293 writes 'Clause-②: yes (narrowing)', and the claim 6011484417 writes 'Clause-②: no (narrowing)' per clause2-line.mjs. The PR body copies the claim's line, and the built declarations show no widening.",
"Consumer sweep direction: I type-checked the two downstream packages that read widget-slot entries (plugin-dashboard, plugin-designer), not the whole downstream set '...@object-ui/types', which is every package. For app-shell DashboardPreview and core dashboard-filters, the only other packages that name the narrowed types, a source read finds no read of label off a slot entry. That is a read-only inference. CI type-checks the rest."
],
"files_changed": [
".changeset/4425-metric-card-label-refused.md",
"packages/plugin-dashboard/src/tests/metricCardLabelRefusedTitleHeading-4425.test.tsx",
"packages/types/src/tests/dashboard-widget-slot-component-arm-7952.test.ts",
"packages/types/src/complex.ts",
"packages/types/src/zod/complex.zod.ts"
]
}objectstack-fleet commented
on Oct 6, 2026 ContributorMore actionsACCEPT: PR objectui#11707 at
05c8d31(Fixes #4425). The at-tier contract review that triage called owed runs before enqueuedomain:uiexecution seat 2 ·session_01FngvPpdrnhHMdHHq6vwwju(os-justin) · 2026-10-06T08:55Z. Dev report:os-dev-reporton this card.- Shape: draft, base
main, first lineFixes #4425, and the body has no other closing keyword.Clause-②: no (narrowing)is at the start of a line, copied from the claim. 5 files:complex.ts,zod/complex.zod.ts, the 7952 pin file, a new render pin inplugin-dashboard, one changeset. No governed path. +265/−2. Fixesis right. The DOM half landed asa600924. The dev measured 0LEAK_LEDGERrows naming this card. The plugin-widget contract doc landed with objectui#11668. This PR is the last open half.- Seat's own reading of the diff:
label?: neveronDashboardWidgetSlotComponentSchema.- On the private zod arm,
label: aliasKeyRefusal('label', 'title', …)with a module-private message constant. - No export line moves (the dev normalised the union order and compared all 74
.d.tsexport lines). aliasKeyRefusalis preferred overretirementTombstonebecauselabelistitle's second spelling, not a withdrawn key. That matchestombstone.zod.ts's own split.
- Changeset prose (
@object-ui/types:minor, with the BREAKING line and the FROM/TO pair), checked sentence by sentence against the diff: the refusal sentence, the "why" paragraph (no heading, no warning,validateTreedoes not walkwidgets), the TypeScript bullet, the zod bullet (the measured envelope limit included) and "Nothing widens, and no export is added" all match.minorwith the break spelled out is objectui#8284's ruling text. Clause-②: triage6008987293wroteyes (narrowing), and the claim and PR writeno (narrowing), the readingclause2-line.mjsgives a pure narrowing. The built declarations show no widening. The at-tier review judges the line.- Tests:
- At the built doors before and after:
objectui validate0 → 1 withDid you mean `label` → `title`?, strict ACCEPT → REFUSE,tscTS2322 onlabel. Thetitleand no-valuecontrols are unchanged. - Two ablation legs: zod member removed (7 red) and
label?: neverremoved (4 tsc errors), each restored by blob. packages/types/9536 passed;plugin-dashboard1699 passed.
- At the built doors before and after:
- Out-of-scope findings (4):
- (c), with reach at
objectui validate: the legacycomponentenvelope'sBaseSchemafallback re-admits every key the arm refuses. Filed as objectui#11709, the family close-out with objectui#11467's recorded limit. - A stale docblock sentence in
complex.tsaboutBaseSchema's removed index signature. Acceptance notes. - A stale comment in
MetricWidget.sduiDomWhitelist-4425.test.tsxcase (c). Acceptance notes. - The wider variant (
name,placeholder,style,data): the dev's probe found identical markup on all three render doors, and the reading is listed in the PR. Carrier: triage, whose answer6008987293says it cards the inert ones.
- (c), with reach at
- Owed before enqueue: the at-tier contract review.
needs:contract-reviewis hung on the PR in this stroke.
Generated by Claude Code
- Shape: draft, base
objectstack-fleet commented
on Oct 6, 2026 ContributorMore actionsLanded: PR objectui#11707 →
maineb4552e, verified by content.Fixesclosed this carddomain:uiexecution seat 2 ·session_01FngvPpdrnhHMdHHq6vwwju(os-justin) · 2026-10-06T09:21Z.-
The merge. Squash
eb4552ewent through the merge queue, with one parent,daa7caf. It is an ancestor oforigin/main. 5 files, +265/−2, the same as the PR. Contract review6012923083PASS, on the landed head05c8d31; it read the line asClause-②: no (narrowing). -
Content check against the first parent:
reading daa7cafeb4552elabel?: never;onDashboardWidgetSlotComponentSchema(complex.ts)0 1 label: aliasKeyRefusal('label', 'title'on the zod slot arm0 1 the module-private const METRIC_CARD_LABEL_IS_TITLE0 1 metricCardLabelRefusedTitleHeading-4425.test.tsxpresent0 1 -
Closures.
Fixes #4425closed this card as completed at the merge. No other issue closed in the landing window.pm:dispatchedcomes off in this stroke. -
Carried on: objectui#11709 (the legacy
componentenvelope'sBaseSchemafallback) and triage's follow-up on the inert inherited keys (name,placeholder,style,data; the reading is in the PR body).
Generated by Claude Code
-
Observation-class finding, recorded while implementing #4357. Nothing a user sees today beyond what #4357 fixes; filed so the next agent closing a DOM-prop leak knows this repo already has a decided answer, and so the divergence is a deliberate choice rather than an accident.
The two answers, both live
objectui#3291 / PR #3313 closed exactly this class in
packages/fields, and the reasoning is written out inpackages/fields/src/widgets/toDomProps.tsunder "Why a whitelist, and not a list of keys to drop":That doc also names this exact path as the harder one:
#4357 closes the same defect in
MetricWidget/MetricCardthe other way — a deny-list, destructured out (packages/plugin-dashboard/src/schemaHostProps.ts), per the ruling on that card. It is correct for the seven measured props and it is verified, but it is the shapetoDomPropsargues against, and it demonstrates the predicted weakness in miniature: the first pass enumerated six keys from a schema-only measurement and missed the seventh (dataSource, the injected adapter, which only appears on a dashboard that actually loads data). One authored key that a widget does not declare still reaches the DOM — measured onmetric-card, an authoredprops: { colorVariant }lands ascolorvariant="success"becauseMetricCardhas no such prop.What is and is not covered today
packages/fields: whitelist + a real gate.widget-dom-leak-e2e.test.tsxrenders every registered field widget through both hosts and checks every attribute of every element against what HTML defines, with planted canaries. It does not reach any other package.packages/plugin-dashboard: afterMetricWidget/MetricCardspread...propsonto the DOM, emitting aschema="[object Object]"attribute on every KPI card #4357, two components with a measured deny-list and a pin (MetricWidget.domProps.test.tsx). No canary, no sweep....propsspread onto a DOM element and are reachable from the registry exist in at leastplugin-charts(ChartContainerImpl.tsx,AdvancedChartImpl.tsx),plugin-calendarandplugin-chatbot. Listed as candidates to measure, not asserted as defects —SchemaRendererinjectsschemainto every registered component, so the mechanism reaches all of them, but whether a given component spreads that onto a host element was not verified here.Fix shape (needs a decision, do not start)
toDomPropsequivalent (or the same one, generalized offFieldWidgetComponentProps) that dashboard/chart/calendar widgets use instead of a bare spread.#3291sweep so the gate covers registered widgets in every package, not only fields. The canary technique is what catches the open tail; a pin on named keys cannot.The choice belongs with the maintainer — 1 and 2 touch the public prop contract of every plugin widget. No
pm:queue.Generated by Claude Code