Skip to content

feat(types): refuse label on the widget-slot metric-card, naming title (objectui#4425) - #11707

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-4425-metric-card-label-refused
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-4425-metric-card-label-refused

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #4425

Clause-②: no (narrowing)

Summary

A metric-card placed directly in a dashboard's widgets[] now refuses label by name, and the refusal names title. That is the key MetricCard draws as the card heading. The card inherited label from BaseSchema without reading it. So a card authored with label, which is the sibling metric node's spelling, validated on every face and drew a card with no heading.

This is the card's second half. The DOM half landed as objectui#11668 (a600924). This half executes triage's answer 6008987293: option A, settled by the standing objectui#8284 ruling (5572150897: one spelling per rendered thing, the other refused by name, ?: never on the TypeScript face). After this PR the card has nothing open. No LEAK_LEDGER row names it: issue: 'objectui#4425' has 0 hits in widget-dom-leak-sweep.test.tsx, and the same pattern finds the 4 objectui#5574 rows, so the instrument is not blind. The plugin-widget contract is stated in content/docs/guide/plugin-development.md ("What Reaches the DOM"), and the label half is this PR. So the body uses Fixes.

What changed

  • zod, packages/types/src/zod/complex.zod.ts. The private widget-slot component arm (DashboardWidgetSlotComponentSchema, the first arm of the slot's union) gains a label member built by aliasKeyRefusal('label', 'title', …), with the site detail in a new constant, METRIC_CARD_LABEL_IS_TITLE. The arm keeps label as a member whose z.never refuses at the key's own path with code invalid_type. The message leads with Unrecognized key(s) on this `metric-card` node: `label`. Did you mean `label` → `title`? and goes on to say why. One string feeds both the issue message and .describe(). The detail constant is module-private, like METRIC_CARD_NEITHER_CHANNEL beside it. The derived strict face inherits the member.
  • TypeScript, packages/types/src/complex.ts. DashboardWidgetSlotComponentSchema restates the inherited member as label?: never, with a docblock naming title. This is the same shape the arm's body / children pair took for objectui#9256.
  • Changeset .changeset/4425-metric-card-label-refused.md: minor for @object-ui/types, with the BREAKING banner. objectui never declares major (AGENTS.md section 9).

Measured at the public doors, before (c3623eb) and after (05c8d31)

Fixtures are one-widget dashboards. objectui validate was run as the built CLI (node packages/cli/dist/cli.js validate FILE). The strict face is StrictAnyComponentSchema read from the built dist. The TypeScript face is the built dist/index.d.ts, compiled by a scratch program that is not committed. After the change, the types package was rebuilt before each reading.

face card before after
objectui validate value + label exit 0, Schema is valid! exit 1. 1.1 Unrecognized key(s) on this metric-card node: label. Did you mean label → title? …, at widgets → 0 → label, code invalid_type
objectui validate value + title + label exit 0 exit 1, same refusal
objectui validate value + title (control) exit 0 exit 0
objectui validate no value (instrument control) exit 1 exit 1
objectui validate label inside a widget's legacy component envelope exit 0 exit 0. The measured limit, see Acceptance notes
strict face value + label ACCEPT REFUSE, invalid_type at widgets.0.label, same message
strict face label inside the component envelope ACCEPT REFUSE, invalid_type at widgets.0.component.label
strict face value + title ACCEPT ACCEPT
tsc on the built declarations label on the interface, in widgets[], in the envelope compiles 3 × TS2322 (Type 'string' is not assignable to type 'undefined')
tsc on the built declarations title compiles compiles
tsc on the built declarations someProp (negative control) TS2353 TS2353

No widening, so Clause-②: no (narrowing) holds. The built declarations were compared with the base build after union-member order was normalised. tsc emitted union members in a different order across the two builds, so a raw diff is noise. Three changes remain. complex.d.ts gains the member and its docblock, +20 lines. zod/complex.zod.d.ts has 5 × label going from the I18nLabel union to z.ZodOptional of z.ZodNever. zod/index.zod.d.ts has 4 × label?: going from the I18nLabel union to undefined, in the inferred validateSchema / safeValidateSchema result types. The export lines of all 74 declaration files are identical, and no name is added.

The producer census (Zone 2 item 4)

Instrument: a depth-1 key scan of every object literal whose type is metric-card or plugin-dashboard:metric-card. It ran over all 7511 tracked json / ts / tsx / js / mjs / md / mdx / yaml files at c3623eb. It found 97 nodes in 27 files. 60 carry title, which is the positive control. 1 carries label: case (c) of MetricWidget.sduiDomWhitelist-4425.test.tsx, a render-door fixture on the bare registration node. It pins that label is never a DOM attribute. It is not authored metadata, it does not go through either schema face, and it stays green. The 11 schema-catalog metric cards and the 25 doc and README nodes carry no label. So no producer needed rewriting. This is a reading of one run at the base, and nothing re-derives it. From this PR on, the faces themselves refuse a new one.

The wider variant: the four other inherited BaseSchema members (triage's list, for its follow-up)

Not refused here, per triage. Each was measured at runtime with a temporary probe, not committed. A widget-slot metric-card was drawn with and without the key on three doors: DashboardRenderer, DashboardGridLayout, and a bare SchemaRenderer of plugin-dashboard:metric-card. Then the markup was compared. The run was deterministic: the control drawn twice gave identical markup. Positive controls: title and className change the markup on all three doors.

key value probed markup with vs without, on all three doors
name 'kpi_probe' identical
placeholder 'PLACEHOLDER-PROBE' identical
style { color: 'rgb(1, 2, 3)', marginTop: 7 } identical. The grid cell's own span style is the host's, not the author's
data { value: 'DATA-PROBE', title: 'DATA-TITLE' } identical
label, for reference 'LABEL-PROBE' identical. Dropped since objectui#11668, and refused from this PR on

Identical markup means nothing on the renderer or host path turns the key into anything drawn. It does not exclude a read that draws nothing.

Tests

  • packages/types/src/__tests__/dashboard-widget-slot-component-arm-7952.test.ts (the arm's own pin file) gains an objectui#4425 block. It covers:
    • the title control on both faces, with keys kept;
    • the refusal at widgets.0.label, naming title, on the tolerant face and the strict face, with and without title alongside;
    • every value refused, because the refusal is about the key;
    • the member is in the arm's shape, and its .describe() IS the message;
    • the TypeScript face, as @ts-expect-error lines read by tsc -p tsconfig.test.json, plus Equal of the member and undefined;
    • the envelope: refused by the strict face and tsc, accepted by the tolerant face, as a recorded limit.
  • The new file packages/plugin-dashboard/src/__tests__/metricCardLabelRefusedTitleHeading-4425.test.tsx holds triage's pin where the card renders. Both faces refuse the card with label and name title, and accept the same card with title. On DashboardRenderer, DashboardGridLayout and the bare registration, the title card draws its heading, and the label card draws its figure and no heading. No existing test asserted the slot card's heading: the surface pins in slotEntryWidgetArmReads-11598.test.tsx and widget-type-default-and-unknown-11514.test.tsx read the figure only.

Ablations, run with the fix committed (objectstack scripts/ablation-replace.mjs, wrap mode; each leg printed ok mutation landed and ok restored: blob == HEAD and git diff HEAD is empty; no build was needed, because vitest aliases @object-ui/types and @object-ui/types/zod to src and the TypeScript leg compiles the test program against src):

  • Leg Z deletes the zod label member. 7 tests failed, 24 passed. The five 7952 cases that read the zod refusal failed, and so did both face cases in the plugin-dashboard file. These stayed green: the title controls, the six render cases (they do not depend on the refusal), and the TypeScript case (?: never is erased under vitest). That is the expected split.
  • Leg T deletes label?: never from the TypeScript twin. tsc -p tsconfig.test.json gave exactly 4 errors, all on this block's lines: TS2322 on the Equal line and TS2578 (unused @ts-expect-error) on the three authoring lines. Nothing else in the program went red.

Gates, all at head 05c8d31

Heavy runs went through os-verify-lock.sh, and every exit code was captured before any pipe.

  • pnpm --filter @object-ui/types build: exit 0. dist completeness: 1 package(s) complete (148 emitted files verified).
  • pnpm --filter @object-ui/types type-check: exit 0. The script echoes tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json.
  • pnpm --filter @object-ui/plugin-dashboard type-check: exit 0. pnpm --filter @object-ui/plugin-designer type-check: exit 0. These are the two packages that read slot entries, through isSlotComponentEntry, and they resolve @object-ui/types through its rebuilt dist. Their dependency closure was built once at 3ac08c8. That commit differs from head only in a string and a comment inside @object-ui/types, which was rebuilt at head.
  • pnpm exec vitest run --maxWorkers=2 packages/types/: exit 0, Test Files 359 passed (359), Tests 9536 passed (9536).
  • pnpm exec vitest run --maxWorkers=2 packages/plugin-dashboard/: exit 0, Test Files 172 passed (172), Tests 1699 passed | 6 skipped (1705).
  • The schema-catalog plugin-dashboard-component-schema and plugin-dashboard-gallery-render tests, the app-shell widget-dom-leak-sweep, plugin-designer DashboardEditor.slotComponentArm-11598 and cli check-validity-recogniser: exit 0, Tests 270 passed (270).
  • pnpm --filter @object-ui/types lint and pnpm --filter @object-ui/plugin-dashboard lint: exit 0, 0 errors. eslint --format json over the 4 touched source files: 0 errors, and no warning on a changed line. The lint config has no type-aware linting (projectService / parserOptions 0 hits in eslint.config.js), so this diff cannot move a verdict in an untouched file.
  • Lockless, all exit 0:
    • check:new-line-citations (0 new);
    • check:control-bytes;
    • check:changeset-claims and check:pending-changeset-literals;
    • check:test-path-roots;
    • check:component-surface-parity and check:handler-key-reads;
    • check:unreferenced-sources, check:vi-mock-specifiers and check:vi-mock-inherit;
    • check:self-import and check:phantom-deps;
    • check:spec-symbols, check:metadata-write-doors, check:doc-types and check:doc-example-readers;
    • check-changeset-no-major, check-changeset-presence and check-changeset-fixed;
    • check-governed-queue-guard --test on the five paths: NOT GOVERNED.
  • NOT MEASURED:
    • check:doc-snippets / check:doc-examples. Reason: the gate's --build-filter closure is a build of about 35 packages, all cache misses after a @object-ui/types change. No doc is touched, and the census above counts 0 doc or README metric-card nodes that carry label. CI owns it.
    • check:readme-exports. Reason: prerequisite not met, unbuilt dist of plugin-view / calendar / gantt / kanban.
    • check:sdui-registration-pins. Reason: prerequisite not met, no console build.
    • The repo-wide lint, test and type-check farm. Reason: CI-owned. For the other packages that name the narrowed types, app-shell DashboardPreview and core dashboard-filters, a read of the source shows neither reads label off a slot entry. That is a read-only inference, not a type-check.

Acceptance notes

  • The legacy component envelope keeps a tolerant fallback (reported to the seat, not changed here). Inside { id, component: { type: 'metric-card', … } } the tolerant face's component slot is z.union([slot arm, BaseSchema]). A card the arm refuses therefore still parses through BaseSchema, and objectui validate exits 0 on the envelope fixture above. The strict face and tsc refuse it. This is the limit the arm's objectui#11467 block already records for a trend outside its enum, and it applies to body / children too. The slot itself, widgets[] directly, has no such fallback, and every face refuses.
  • DashboardWidgetSlotComponentSchema's docblock in complex.ts still says "Every other key still reaches this arm through BaseSchema's index signature while it stands". The signature went with objectui#8347: the 7952 file's someProp case pins tsc refusing an undeclared key. A stale sentence, left alone here, not filed.
  • Case (c) of MetricWidget.sduiDomWhitelist-4425.test.tsx still calls the label question "that card's open question". This PR answers it. The file is outside this claim's surface. A stale comment, not filed.
  • Zone 3 route, one substitution. The refusal uses aliasKeyRefusal, not a retirementTombstone string. tombstone.zod.ts keeps the tombstone for a key the contract withdraws, and the alias helper for "a sibling SPELLING of a declared member", whose message must carry the canonical spelling. label here is the second spelling of title. The shape is the same as objectui#9256's: a z.never member, invalid_type at the key's own path, and ?: never on the twin. The detail string is still one module-private constant beside METRIC_CARD_NEITHER_CHANNEL.
  • The silence clause names the parser tier. objectui#10981's closure walk (silence-clause-parser-tier-closure-10981.test.ts) requires it. It went red on the first commit, and the second commit carries the measurement. With the manifest built the way gen-manifest builds it, dashboard declares no slots, so validateTree never reaches a widget-slot card. It draws nothing for { value, label } or for { value, title }. The control, an undeclared key on the dashboard itself, draws unknown-prop.

Generated by Claude Code

claude added 2 commits October 6, 2026 07:37
…`title` (objectui#4425)

`MetricCard` draws `title` as the card heading and reads no `label`, which the
widget slot's component arm inherited from `BaseSchema`. A card authored with
`label` validated on every face and drew no heading. Under the objectui#8284
ruling (one spelling per rendered thing) the arm now refuses `label` by name:

- zod: `aliasKeyRefusal('label', 'title', ...)` on the private slot arm, so the
  tolerant face (`objectui validate`) and the derived strict face refuse it at
  the key's own path, with a message that names `title`;
- TypeScript: `label?: never` on `DashboardWidgetSlotComponentSchema`.

Pins: the faces in `dashboard-widget-slot-component-arm-7952.test.ts`; the
refusal plus "the same card with `title` draws its heading" on both dashboard
surfaces and the bare registration in plugin-dashboard's
`metricCardLabelRefusedTitleHeading-4425.test.tsx`.

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

The objectui#10981 closure walk requires every "no render-time error or
warning" sentence to say what the parser tier does. Measured with the
manifest built the way gen-manifest builds it: `dashboard` declares no
`widgets` slot, so `validateTree` never reaches a widget-slot card and draws
nothing for its `label` (control: an undeclared key on the dashboard itself
draws `unknown-prop`). The zod message, the TypeScript member's docblock and
the changeset now say so.

Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju
Co-authored-by: Claude <noreply@anthropic.com>
@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) 3322.4 KB 3330.4 KB
Main entry chunk (gzip) 152.7 KB 350 KB
Entry file index-CYFYFwCN.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.81KB 47.14KB
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.83KB 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 05c8d3153a9636fee028f2f47af851543efb56f2
Local-runs: none

Inputs: card objectui#4425 (body and all 14 comments, rulings 5270759246 / 6008987293 and the claim 6011484417 included), PR objectui#11707 (body, 5-file list, net diff against main at the head), the 42 check-runs on the head, and the source at the head and at origin/main read from git objects. Nothing built, run or re-run.

① Derived judgments

  1. zod tolerant face, a metric-card directly in widgets[] carrying label: ACCEPT → REFUSE. RIGHT. The private slot arm DashboardWidgetSlotComponentSchema = BaseSchema.extend({ … }) gains label: aliasKeyRefusal('label', 'title', 'this metric-card node', METRIC_CARD_LABEL_IS_TITLE); the helper (tombstone.zod.ts:185, pre-existing at main, in use in app/base/data-display/form/layout zod files) is z.never({ error }).optional().describe(guidance), so the member refuses at its own path with invalid_type and one string feeds the message and .describe(). The slot union's other arm (DashboardWidgetSchema, .strict()) does not admit the node, so the widget reports invalid_union with the arm's invalid_type at widgets.0.label. This is option A of triage 6008987293 under the objectui#8284 ruling, in objectui#9256's shape (the body / children members one line below).
  2. zod strict authoring face: the same node REFUSED, directly in widgets[] and inside the component envelope. RIGHT. The derived strict face inherits the member; refusal is invalid_type at label, never unrecognized_keys, because the key stays a declared member.
  3. zod tolerant face, legacy component envelope: accept set UNCHANGED. RIGHT to record rather than fix. DashboardWidgetConfigSchema.component is z.union([slot arm, BaseSchema]) (complex.zod.ts:1420 at main), so a card the arm refuses still parses through BaseSchema and objectui validate exits 0 there. The 7952 file's objectui#11467 block already records this limit for a trend outside its enum; it covers body / children / label alike, and closing it is the envelope-fallback owner's change, outside this claim's surface. The dev filed it as a class-c finding for the seat to merge; that is the right carrier.
  4. TypeScript face: DashboardWidgetSlotComponentSchema.label?: never (exported interface, complex.ts:2435), narrowing the inherited BaseSchema.label?: string | I18nLabel (base.ts:190). RIGHT. It reaches DashboardComponentSchema.widgets (:2603) and DashboardWidgetSchema.component (:2296), so a literal carrying label stops compiling on the interface, in widgets[] and in the envelope (TS2322). Same shape as body?: never / children?: never on the same interface. The three @ts-expect-error lines plus the Equal pin are read by tsc -p tsconfig.test.json, so a re-widening turns that program red (ablation leg T: exactly 4 errors, all on the block).
  5. Published names: none added, none removed. RIGHT. METRIC_CARD_LABEL_IS_TITLE is a module-private const beside METRIC_CARD_NEITHER_CHANNEL; the arm itself is not exported; aliasKeyRefusal existed at main. The PR's own d.ts account (union order normalised): the new member and docblock in complex.d.ts, label going from the I18nLabel union to ZodOptional of ZodNever (5x) and to undefined (4x, the inferred validateSchema / safeValidateSchema results), export lines of all 74 declaration files identical. Consistent with the source diff; not rebuilt here.
  6. Scope: the metric-card slot node only. RIGHT. The sibling metric widget keeps label (it IS that node's heading), and BaseSchema.label is untouched everywhere else. The ruling is per rendered thing.
  7. No generated artifact moves. RIGHT. No checked-in snapshot or baseline carries the arm's describe strings (the matches outside packages/types/src are CHANGELOGs, docs, README and source comments); the cli ratchet registered-types-validate-ratchet-10859 reads type-level refusals only. check:component-surface-parity and check:doc-types ran green per the PR; Doc Component Type Check and Doc Snippet Type Check are green on the head.
  8. Producer exposure: zero authored producers. RIGHT. My own census over tracked md / mdx / json / yaml / examples / console / README at the head finds no metric-card node authored with label; the two label: 'My Metric' hits (content/docs/plugins/plugin-dashboard.mdx:179, packages/plugin-dashboard/README.md:128) sit on metric widgets. The only label on a card is the render-door fixture in MetricWidget.sduiDomWhitelist-4425 case (c) and the sweep's schemaExtras row (widget-dom-leak-sweep.test.tsx:897), neither of which crosses a schema face. Matches the dev's 97-node census; no producer rewrite was owed, and the faces now refuse a new one.
  9. Card closure inputs hold at the head. LEAK_LEDGER has 0 rows with issue: 'objectui#4425' and all 4 rows are objectui#5574 (:1170–:1209), so the DOM half (objectui#11668, a600924) stands, and the plugin-widget contract text landed with it.

② Semver level

  • Changeset .changeset/4425-metric-card-label-refused.md: '@object-ui/types': minor, break spelled out. RIGHT. The body opens with BREAKING (\@object-ui/types`), gives a FROM → TO migration (label→title, value type kept) and states each face's new behaviour, the envelope limit included. That is the spelling AGENTS.md:272asks for (objectui's own breaking changes ship asminorwith the semantics in the body;major never) and the spelling two other pending changesets use (11266-subforms-columns-mirror.md:7, 11610-grid-keys-camelcase.md:8). Only @object-ui/typesmoves a published surface;@object-ui/plugin-dashboard gains a test file only, so one entry is correct. Changeset Bump Policy (check-changeset-no-major`), Changeset Declaration, Changeset Claim Re-read and Changeset Fixed Group Check: success on the head.
  • Note, not a defect: objectui runs no ADR-0087 registration gate, and objectstack's breakingDeclaration reads only **BREAKING, BREAKING CHANGE, major, a !: summary or a Clause-② arm INSIDE the changeset body. None of those spellings is in this body. No objectui gate reads it that way today; if that reader ever reaches objectui, the bold spelling would be the one to carry.
  • The Clause-② line: the diff supports no (narrowing). The diff adds no key, value, literal or export to any accept set or published surface; it narrows the zod accept set on both faces (directly in widgets[]) and the TypeScript surface. Under scripts/pm/clause2-line.mjs the value answers 「放宽接受集或扩大公开面吗」, and yes (narrowing) means a diff that widens one surface AND narrows another. Nothing here widens, so triage's line in 6008987293 (yes (narrowing)) is the wrong reading of this diff, and the claim 6011484417 and the PR body (no (narrowing), not a widening but breaking) carry the line the diff supports. The PR body's line sits at line start, where the gate reads it. No amendment is owed; the review triage called owed is this record.

③ Boundary flags

open_questions: [] in the dev report 6012783711. Dev flags (Acceptance notes, deviations, out-of-scope findings), each answered:

  1. Envelope tolerant fallback (class c, measured at the built CLI): answered in ① item 3 as a recorded limit shared with body / children / trend. Not this PR's to close. Escalated to the seat only as a merge into whichever card owns the envelope fallback, as the dev asked. Not blocking.
  2. Zone 3 substitution, aliasKeyRefusal for retirementTombstone: RIGHT. tombstone.zod.ts keeps the tombstone for a key the contract withdraws and the alias helper for a sibling spelling of a declared member, whose message must name the canonical key; label is title's second spelling here. The member / z.never / invalid_type / ?: never shape is 9256's.
  3. Silence clause names the parser tier (objectui#10981 closure walk red on 3ac08c8, green on the head): the zod message, the TS docblock and the changeset each say validateTree does not walk a dashboard's widgets, with the measurement in the dev report. Answered. Test shards confirm on landing.
  4. New pin file metricCardLabelRefusedTitleHeading-4425.test.tsx outside the listed test files: within the claim's "or a new pin file" wording, and it is triage's pin (both faces refuse the label card naming title; the title card draws its heading on DashboardRenderer, DashboardGridLayout and the bare registration; the label card draws its figure and no heading, so the refusal is the remedy and not a rendered alias). Answered.
  5. Two stale comments (the complex.ts docblock's BaseSchema index-signature sentence, gone since objectui#8347; case (c) of MetricWidget.sduiDomWhitelist-4425 still calling label an open question): doc debt with no contract effect. The first sits in a docblock this PR already edits, so the seat may fold it into the landing or leave it. Not blocking, not escalated.
  6. Triage yes (narrowing) versus claim no (narrowing): ruled in ②. The claim is right; no amendment.
  7. The wider variant (name, placeholder, style, data): measured identical markup on three doors with title / className as positive controls and determinism checked; not refused here, per triage. Carrier: triage's own follow-up per 6008987293. Escalated to the seat to hand triage the PR's table. Not this PR's.
  8. Consumer sweep: plugin-dashboard and plugin-designer type-checked locally against the rebuilt dist; app-shell DashboardPreview and core dashboard-filters are a read-only inference (neither reads label off a slot entry). The CI Type Check on the head covers the rest. Landing check.
  9. Fixes #4425: supportable. The card's closure condition (ledger holds no row naming it, 6006987402) is met at the head; the contract doc landed with 11668; triage's Done-when (refused by name on all three faces in 9256's shape, remedy names title, pin present) is met. The inert-members follow-up is triage's card, not this card's condition.
  10. Check-runs on the head at read time: 28 success, 3 skipped (dependabot, the two coverage lanes), 11 still in progress: Lint, Type Check, Spec Main Shape Gate, Test (shard 1/8) through Test (shard 8/8). Judged ①②③ regardless; their convergence is the seat's landing check, not this record's.

Implemented-by: claude/issue-4425-metric-card-label-refused
Reviewed-by: session_01FngvPpdrnhHMdHHq6vwwju

VERDICT: PASS


Generated by Claude Code

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

Projects

None yet

2 participants