Repository navigation
feat(types): refuse label on the widget-slot metric-card, naming title (objectui#4425) - #11707
Conversation
…`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>
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: 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 ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #4425
Clause-②: no (narrowing)
Summary
A
metric-cardplaced directly in a dashboard'swidgets[]now refuseslabelby name, and the refusal namestitle. That is the keyMetricCarddraws as the card heading. The card inheritedlabelfromBaseSchemawithout reading it. So a card authored withlabel, which is the siblingmetricnode'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 answer6008987293: option A, settled by the standing objectui#8284 ruling (5572150897: one spelling per rendered thing, the other refused by name,?: neveron the TypeScript face). After this PR the card has nothing open. NoLEAK_LEDGERrow names it:issue: 'objectui#4425'has 0 hits inwidget-dom-leak-sweep.test.tsx, and the same pattern finds the 4objectui#5574rows, so the instrument is not blind. The plugin-widget contract is stated incontent/docs/guide/plugin-development.md("What Reaches the DOM"), and the label half is this PR. So the body usesFixes.What changed
packages/types/src/zod/complex.zod.ts. The private widget-slot component arm (DashboardWidgetSlotComponentSchema, the first arm of the slot's union) gains alabelmember built byaliasKeyRefusal('label', 'title', …), with the site detail in a new constant,METRIC_CARD_LABEL_IS_TITLE. The arm keepslabelas a member whosez.neverrefuses at the key's own path with codeinvalid_type. The message leads withUnrecognized 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, likeMETRIC_CARD_NEITHER_CHANNELbeside it. The derived strict face inherits the member.packages/types/src/complex.ts.DashboardWidgetSlotComponentSchemarestates the inherited member aslabel?: never, with a docblock namingtitle. This is the same shape the arm'sbody/childrenpair took for objectui#9256..changeset/4425-metric-card-label-refused.md:minorfor@object-ui/types, with the BREAKING banner. objectui never declaresmajor(AGENTS.md section 9).Measured at the public doors, before (
c3623eb) and after (05c8d31)Fixtures are one-widget dashboards.
objectui validatewas run as the built CLI (node packages/cli/dist/cli.js validate FILE). The strict face isStrictAnyComponentSchemaread from the builtdist. The TypeScript face is the builtdist/index.d.ts, compiled by a scratch program that is not committed. After the change, the types package was rebuilt before each reading.objectui validatevalue+labelSchema is valid!1.1 Unrecognized key(s) on this metric-card node: label. Did you mean label → title? …, atwidgets → 0 → label, codeinvalid_typeobjectui validatevalue+title+labelobjectui validatevalue+title(control)objectui validatevalue(instrument control)objectui validatelabelinside a widget's legacycomponentenvelopevalue+labelinvalid_typeatwidgets.0.label, same messagelabelinside thecomponentenvelopeinvalid_typeatwidgets.0.component.labelvalue+titletscon the built declarationslabelon the interface, inwidgets[], in the envelopeType 'string' is not assignable to type 'undefined')tscon the built declarationstitletscon the built declarationssomeProp(negative control)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.tsgains the member and its docblock, +20 lines.zod/complex.zod.d.tshas 5 ×labelgoing from the I18nLabel union toz.ZodOptional of z.ZodNever.zod/index.zod.d.tshas 4 ×label?:going from the I18nLabel union toundefined, in the inferredvalidateSchema/safeValidateSchemaresult types. Theexportlines 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
typeismetric-cardorplugin-dashboard:metric-card. It ran over all 7511 tracked json / ts / tsx / js / mjs / md / mdx / yaml files atc3623eb. It found 97 nodes in 27 files. 60 carrytitle, which is the positive control. 1 carrieslabel: case (c) ofMetricWidget.sduiDomWhitelist-4425.test.tsx, a render-door fixture on the bare registration node. It pins thatlabelis 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 nolabel. 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
BaseSchemamembers (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-cardwas drawn with and without the key on three doors:DashboardRenderer,DashboardGridLayout, and a bareSchemaRendererofplugin-dashboard:metric-card. Then the markup was compared. The run was deterministic: the control drawn twice gave identical markup. Positive controls:titleandclassNamechange the markup on all three doors.name'kpi_probe'placeholder'PLACEHOLDER-PROBE'style{ color: 'rgb(1, 2, 3)', marginTop: 7 }data{ value: 'DATA-PROBE', title: 'DATA-TITLE' }label, for reference'LABEL-PROBE'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 anobjectui#4425block. It covers:titlecontrol on both faces, with keys kept;widgets.0.label, namingtitle, on the tolerant face and the strict face, with and withouttitlealongside;.describe()IS the message;@ts-expect-errorlines read bytsc -p tsconfig.test.json, plusEqualof the member andundefined;tsc, accepted by the tolerant face, as a recorded limit.packages/plugin-dashboard/src/__tests__/metricCardLabelRefusedTitleHeading-4425.test.tsxholds triage's pin where the card renders. Both faces refuse the card withlabeland nametitle, and accept the same card withtitle. OnDashboardRenderer,DashboardGridLayoutand the bare registration, thetitlecard draws its heading, and thelabelcard draws its figure and no heading. No existing test asserted the slot card's heading: the surface pins inslotEntryWidgetArmReads-11598.test.tsxandwidget-type-default-and-unknown-11514.test.tsxread the figure only.Ablations, run with the fix committed (objectstack
scripts/ablation-replace.mjs, wrap mode; each leg printedok mutation landedandok restored: blob == HEAD and git diff HEAD is empty; no build was needed, because vitest aliases@object-ui/typesand@object-ui/types/zodtosrcand the TypeScript leg compiles the test program againstsrc):labelmember. 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: thetitlecontrols, the six render cases (they do not depend on the refusal), and the TypeScript case (?: neveris erased under vitest). That is the expected split.label?: neverfrom the TypeScript twin.tsc -p tsconfig.test.jsongave exactly 4 errors, all on this block's lines: TS2322 on theEqualline and TS2578 (unused@ts-expect-error) on the three authoring lines. Nothing else in the program went red.Gates, all at head
05c8d31Heavy 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 echoestsc --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, throughisSlotComponentEntry, and they resolve@object-ui/typesthrough its rebuiltdist. Their dependency closure was built once at3ac08c8. 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).plugin-dashboard-component-schemaandplugin-dashboard-gallery-rendertests, the app-shellwidget-dom-leak-sweep, plugin-designerDashboardEditor.slotComponentArm-11598and clicheck-validity-recogniser: exit 0,Tests 270 passed (270).pnpm --filter @object-ui/types lintandpnpm --filter @object-ui/plugin-dashboard lint: exit 0, 0 errors.eslint --format jsonover the 4 touched source files: 0 errors, and no warning on a changed line. The lint config has no type-aware linting (projectService/parserOptions0 hits ineslint.config.js), so this diff cannot move a verdict in an untouched file.check:new-line-citations(0 new);check:control-bytes;check:changeset-claimsandcheck:pending-changeset-literals;check:test-path-roots;check:component-surface-parityandcheck:handler-key-reads;check:unreferenced-sources,check:vi-mock-specifiersandcheck:vi-mock-inherit;check:self-importandcheck:phantom-deps;check:spec-symbols,check:metadata-write-doors,check:doc-typesandcheck:doc-example-readers;check-changeset-no-major,check-changeset-presenceandcheck-changeset-fixed;check-governed-queue-guard --teston the five paths: NOT GOVERNED.check:doc-snippets/check:doc-examples. Reason: the gate's--build-filterclosure is a build of about 35 packages, all cache misses after a@object-ui/typeschange. No doc is touched, and the census above counts 0 doc or READMEmetric-cardnodes that carrylabel. CI owns it.check:readme-exports. Reason: prerequisite not met, unbuiltdistof plugin-view / calendar / gantt / kanban.check:sdui-registration-pins. Reason: prerequisite not met, no console build.DashboardPreviewand coredashboard-filters, a read of the source shows neither readslabeloff a slot entry. That is a read-only inference, not a type-check.Acceptance notes
componentenvelope keeps a tolerant fallback (reported to the seat, not changed here). Inside{ id, component: { type: 'metric-card', … } }the tolerant face'scomponentslot isz.union([slot arm, BaseSchema]). A card the arm refuses therefore still parses throughBaseSchema, andobjectui validateexits 0 on the envelope fixture above. The strict face andtscrefuse it. This is the limit the arm's objectui#11467 block already records for atrendoutside its enum, and it applies tobody/childrentoo. The slot itself,widgets[]directly, has no such fallback, and every face refuses.DashboardWidgetSlotComponentSchema's docblock incomplex.tsstill says "Every other key still reaches this arm throughBaseSchema's index signature while it stands". The signature went with objectui#8347: the 7952 file'ssomePropcase pinstscrefusing an undeclared key. A stale sentence, left alone here, not filed.MetricWidget.sduiDomWhitelist-4425.test.tsxstill calls thelabelquestion "that card's open question". This PR answers it. The file is outside this claim's surface. A stale comment, not filed.aliasKeyRefusal, not aretirementTombstonestring.tombstone.zod.tskeeps 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.labelhere is the second spelling oftitle. The shape is the same as objectui#9256's: az.nevermember,invalid_typeat the key's own path, and?: neveron the twin. The detail string is still one module-private constant besideMETRIC_CARD_NEITHER_CHANNEL.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 waygen-manifestbuilds it,dashboarddeclares no slots, sovalidateTreenever reaches a widget-slot card. It draws nothing for{ value, label }or for{ value, title }. The control, an undeclared key on the dashboard itself, drawsunknown-prop.Generated by Claude Code