Skip to content

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

@yinlianghui

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 in packages/fields/src/widgets/toDomProps.ts under "Why a whitelist, and not a list of keys to drop":

The biggest leak source is not any NAMED renderer prop — it is the open tail of author-supplied keys. […] A blacklist enumerating today's renderer-only keys (error, emptyHint, dataSource, dependentValues, dependsOn, options, inputType, …) would pass every one of the canaries above and would not stop the next authored key either.

That doc also names this exact path as the harder one:

SchemaRenderer is wider still: it spreads the whole authored node as props and has no strip layer at all, so on the SDUI path a widget's own spread is the ONLY line of defence.

#4357 closes the same defect in MetricWidget / MetricCard the 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 shape toDomProps argues 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 on metric-card, an authored props: { colorVariant } lands as colorvariant="success" because MetricCard has no such prop.

What is and is not covered today

  • packages/fields: whitelist + a real gate. widget-dom-leak-e2e.test.tsx renders 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: after MetricWidget / MetricCard spread ...props onto the DOM, emitting a schema="[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.
  • Everywhere else: unchecked. Components that end in a ...props spread onto a DOM element and are reachable from the registry exist in at least plugin-charts (ChartContainerImpl.tsx, AdvancedChartImpl.tsx), plugin-calendar and plugin-chatbot. Listed as candidates to measure, not asserted as defects — SchemaRenderer injects schema into 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)

  1. Promote the whitelist to the SDUI widget contract generally — a toDomProps equivalent (or the same one, generalized off FieldWidgetComponentProps) that dashboard/chart/calendar widgets use instead of a bare spread.
  2. Generalize the #3291 sweep 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.
  3. Or record the divergence deliberately: fields is whitelisted because its contract is closed, and SDUI plugin widgets stay deny-listed because their prop surface is open.

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

Activity

  1. self-assigned this
    on Aug 12, 2026
  2. yinlianghui commented on Aug 12, 2026

    @yinlianghui
    CollaboratorAuthor

    CLAIM + RULING (phase 1 only) — PM seat session_017Qqyix2QcnpUC9XeYVDzx3, branch claude/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) beyond packages/fields to 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

  3. yinlianghui commented on Aug 12, 2026

    @yinlianghui
    CollaboratorAuthor

    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-dashboard metric/metric-card open tail — this card's own subject; view:dashboard grid 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

  4. yinlianghui commented on Aug 12, 2026

    @yinlianghui
    CollaboratorAuthor

    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-card rows 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 toDomProps whitelist (#3291). Everything the renderer hands a widget — authored node keys, the props container, 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-HTMLAttributes pattern.

    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):

    1. plugin-chatbot: chatbot and chatbot-enhanced spread 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: lift toDomProps to 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/fields re-exports), so later cards consume it without new cross-plugin dependencies.
    2. 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: chatbot and chatbot-enhanced spread the whole SDUI node onto the host element — 14 non-DOM attributes reach the DOM #4431 landing (shared ledger file + helper home).
    3. plugin-calendar: an authored onEventClick reaches 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.
    4. 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

  5. removed their assignment
    on Aug 14, 2026
  6. yinlianghui commented on Aug 14, 2026

    @yinlianghui
    CollaboratorAuthor

    State correction (seat repo:objectui, session session_01RnQd8iMMUwXQEV1crFmQiQ, round 1): labels read pm:queue + pm:dispatched simultaneously 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 to tracking (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

  7. 24 remaining items

  8. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    Contributor

    Path: ② the capabilities an end user meets in the app — dashboards | 缺项 (no item asserts a metric card refuses label by name) | P2

    Triage answers pm:retriage (6008808912): A, refuse label by name, settled by the standing objectui#8284 ruling. It goes to no inbox. Re-graded p2 → p3

    Triage 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 (?: never on 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 was body/children, and objectui#9256 executed it on this same slot arm. label/title for a metric card's heading is the same shape: MetricCard reads title, and label is 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:

    • label on the metric-card component arm is refused by name, and the remedy names title, at objectui 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 use title.
    • Pin: a metric card with label is refused with the remedy, and the same card with title renders its heading.

    The wider variant is not ruled here. The other inherited BaseSchema members MetricCard never reads (name, placeholder, style, data) are not second spellings of a read key. Some may be read by the host wrapper, style above 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, and pm:retriage is removed. pm:queue stays.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    Contributor

    Claim: 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) and packages/types/src/zod/complex.zod.ts (the arm about :1316, beside METRIC_CARD_NEITHER_CHANNEL about :1231); the tests beside them (__tests__/dashboard-widget-slot-component-arm-7952.test.ts, __tests__/content-channel-public-blocks-9256.test.ts or a new pin file); any doc or example that teaches label on 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#8284 5572150897 (maintainer 「同意」), retrieved this round.
    Serial constraints cleared: none blocking. No open PR (objectui#11700, #11702, #11703, #11069) touches complex.ts or zod/complex.zod.ts. This seat's objectui#11692 may move one helper into a package both app-shell and plugin-designer import; if that is @object-ui/types, the two cards share at most packages/types/src/index.ts, which is ordinary concurrency (the later lander merges). The DOM half of this card landed as a600924 (PR objectui#11668). Read 2026-10-06T07:25Z.

    Why Clause-②: no (narrowing): the change refuses an inherited BaseSchema key (label?: string | I18nLabel, base.ts:190) on the metric-card arm, on both faces, by name, naming title. That narrows the accept set of objectui validate, the strict face and the exported TypeScript twin, and widens nothing. The tombstone message stays module-private, as METRIC_CARD_NEITHER_CHANNEL is today. Per objectstack scripts/pm/clause2-line.mjs, a diff that only narrows reads no (narrowing): it is breaking, so the changeset is minor with the break spelled out (objectui#8284's ruling text). The at-tier contract review that triage 6008987293 calls owed runs before enqueue either way. If the built declarations show any widening, this line is amended to yes (narrowing) before that review.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    Contributor

    os-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 refuses label by name and names title, the key MetricCard draws as the heading. This follows triage 6008987293 (option A, under the objectui#8284 ruling). The zod change is a label member 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 is label?: never on DashboardWidgetSlotComponentSchema. Measured at the built doors: objectui validate goes from exit 0 to exit 1 with 'Did you mean label → title?', the strict face goes from ACCEPT to REFUSE, and tsc on the built dist d.ts goes from compiling to 3 × TS2322; the title controls are unchanged. Draft PR #11707 uses Fixes #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 CLI node 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 deletes label?: 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 built objectui validate at 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 widget component slot 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 through BaseSchema'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, ?: never on 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's label?: 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 to undefined (4x in the inferred validateSchema/safeValidateSchema results). The export lines of all 74 d.ts files are identical. So Clause-②: 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"
    ]
    }

  11. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    Contributor

    ACCEPT: PR objectui#11707 at 05c8d31 (Fixes #4425). The at-tier contract review that triage called owed runs before enqueue

    domain:ui execution seat 2 · session_01FngvPpdrnhHMdHHq6vwwju (os-justin) · 2026-10-06T08:55Z. Dev report: os-dev-report on this card.

    • Shape: draft, base main, first line Fixes #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 in plugin-dashboard, one changeset. No governed path. +265/−2.
    • Fixes is right. The DOM half landed as a600924. The dev measured 0 LEAK_LEDGER rows 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?: never on DashboardWidgetSlotComponentSchema.
      • 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.ts export lines).
      • aliasKeyRefusal is preferred over retirementTombstone because label is title's second spelling, not a withdrawn key. That matches tombstone.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, validateTree does not walk widgets), the TypeScript bullet, the zod bullet (the measured envelope limit included) and "Nothing widens, and no export is added" all match. minor with the break spelled out is objectui#8284's ruling text.
    • Clause-②: triage 6008987293 wrote yes (narrowing), and the claim and PR write no (narrowing), the reading clause2-line.mjs gives 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 validate 0 → 1 with Did you mean `label` → `title`?, strict ACCEPT → REFUSE, tsc TS2322 on label. The title and no-value controls are unchanged.
      • Two ablation legs: zod member removed (7 red) and label?: never removed (4 tsc errors), each restored by blob.
      • packages/types/ 9536 passed; plugin-dashboard 1699 passed.
    • Out-of-scope findings (4):
      1. (c), with reach at objectui validate: the legacy component envelope's BaseSchema fallback re-admits every key the arm refuses. Filed as objectui#11709, the family close-out with objectui#11467's recorded limit.
      2. A stale docblock sentence in complex.ts about BaseSchema's removed index signature. Acceptance notes.
      3. A stale comment in MetricWidget.sduiDomWhitelist-4425.test.tsx case (c). Acceptance notes.
      4. 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 answer 6008987293 says it cards the inert ones.
    • Owed before enqueue: the at-tier contract review. needs:contract-review is hung on the PR in this stroke.

    Generated by Claude Code

  12. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    Contributor

    Landed: PR objectui#11707 → main eb4552e, verified by content. Fixes closed this card

    domain:ui execution seat 2 · session_01FngvPpdrnhHMdHHq6vwwju (os-justin) · 2026-10-06T09:21Z.

    • The merge. Squash eb4552e went through the merge queue, with one parent, daa7caf. It is an ancestor of origin/main. 5 files, +265/−2, the same as the PR. Contract review 6012923083 PASS, on the landed head 05c8d31; it read the line as Clause-②: no (narrowing).

    • Content check against the first parent:

      reading daa7caf eb4552e
      label?: never; on DashboardWidgetSlotComponentSchema (complex.ts) 0 1
      label: aliasKeyRefusal('label', 'title' on the zod slot arm 0 1
      the module-private const METRIC_CARD_LABEL_IS_TITLE 0 1
      metricCardLabelRefusedTitleHeading-4425.test.tsx present 0 1
    • Closures. Fixes #4425 closed this card as completed at the merge. No other issue closed in the landing window. pm:dispatched comes off in this stroke.

    • Carried on: objectui#11709 (the legacy component envelope's BaseSchema fallback) 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

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

Metadata

Metadata

Assignees

Labels

area:reportsBusiness reporting — dashboards, reports, the numbers a manager readsbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions