Repository navigation
fix(types): the legacy widget component envelope judges a metric-card by its arm alone (objectui#11709) - #11715
Merged
objectstack-fleet[bot] merged 4 commits intoOct 6, 2026
Conversation
… to its arm alone The widget's legacy `component` slot was a plain `z.union` of the slot's component arm and passthrough `BaseSchema`, so every `metric-card` the arm refuses (`label`, `children`, an out-of-enum `trend`, a missing `value`) parsed through the second arm on the tolerant face that `objectui validate` runs. The slot now routes a node whose `type` the arm declares to the arm alone; every other node goes to the same union as before. Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju Co-authored-by: Claude <noreply@anthropic.com>
…rm carries, through the legacy envelope Derives each probe from the arm's own members (a tombstone, an enum, a typed member, a required input) and asserts the tolerant face refuses it inside a widget's `component` envelope with the member's own message and no union issue at the slot. Replaces the two MEASURED LIMIT notes that recorded the `BaseSchema` fallback, and keeps the title and custom-widget passthrough controls. Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju Co-authored-by: Claude <noreply@anthropic.com>
…d narrowing (objectui#11709) Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju Co-authored-by: Claude <noreply@anthropic.com>
…ith the union's own constructor The census compares each closed union with an open twin rebuilt from the same def. It rebuilt every twin as a plain `z.ZodUnion`, which drops the widget `component` slot's routing (objectui#11709) and read the routing as an accept-set move of the terminal refusal. The twin now keeps the union's constructor, as it already keeps `def.checks`. The changeset names the one strict-face corner the routing narrows. Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ 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
|
This was referenced Oct 6, 2026
objectstack-fleet
Bot
deleted the
claude/issue-11709-envelope-metric-card-arm
branch
October 6, 2026 13:07
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #11709
Clause-②: no (narrowing)
What changes
The dashboard widget's legacy
componentslot (DashboardWidgetSchema.componentin@object-ui/types/zod) was a plainz.unionof the slot's component arm (DashboardWidgetSlotComponentSchema) and passthroughBaseSchema. A union takes the first arm that parses, andBaseSchemaadmits any node. So everymetric-cardthe arm refuses parsed throughBaseSchemaon the tolerant face (safeValidateSchema, whichobjectui validateruns).A module-private constructor,
WidgetComponentSlotUnion, now builds the slot. It holds the same two arms and adds one routing step before the union runs. A node whosetypethe arm declares goes to the arm alone. The routing reads the arm's ownpropValuesfortype, which isDASHBOARD_COMPONENT_WIDGET_TYPES. Every other node goes to the unchanged two-arm union. There is no new key, no copy of the arm's rules and no new export. The comment above the slot keeps its reasoning for the passthrough that every other node (acustomwidget's) gets.The shape, measured against the alternatives (same documents, from source)
labelthrough the envelopeobjectui validateprintsz.discriminatedUnion('type', [arm, BaseSchema])Invalid discriminated union option at index "1"), with and withoutunionFallback:BaseSchema's opentypedeclares no valueBaseSchemaarm that refusestype: 'metric-card'[arm 1/2]with the arm's message, and[arm 2/2]with a second, routing-only message from the fallbacktypebefore the union (shipped)widgets → 0 → component → label. That is the shape the same card already has directly inwidgets[]Three faces, before (
da35453) and afterThe envelope is
{ type: 'dashboard', widgets: [{ id: 'w', component: NODE }] }. The tolerant column is the exit code of the built CLI,node packages/cli/dist/cli.js validate. Thetsccolumn is a probe program compiled against the built@object-ui/typesdeclarations.metric-cardplus)objectui validatebeforetscbefore / afterlabelbodychildrentrend: 'sideways'value(titleonly)titlecontrolkpi_customenvelopecustomwidget'stextnode with an undeclared keyThe strict face narrows in one corner. It was measured in a base worktree against the head: a card with no
valuethat carries onlyBaseSchemakeys ({ type: 'metric-card' }, or that plusid/className) used to parse through the strictBaseSchemaarm. It is refused now for its missingvalue.tscalready refused both literals, and the changeset states the corner.Pins
packages/types/src/__tests__/dashboard-widget-slot-component-arm-7952.test.ts:One enumeration pin, derived from the arm. For each member of the arm except
type, the probe authors the first value from a fixed list of JSON values that the member itself refuses. Each required member also gets an omission probe. The pin then asserts, throughsafeValidateSchema:invalid_unionat the slot (nothing but the arm judged the card);At
32dfdf0this derives 26 probes,label,body,children,trendand an omittedvalueamong them. A lit control requires those five to be derived, so a derivation that reads nothing cannot pass. A refusal added to the arm later (a tombstone, an enum or a required input) is enumerated with no edit. The bound is stated in the pin: a refusal that only a value outside the list trips (a.refineon a member) is not derived.Controls. A well-formed card and the same card with
titleparse on both faces, with every key kept. Acustomwidget'scomponentstill keeps an undeclared key. The slot's first arm IS the armwidgets[]holds a card by.Both "MEASURED LIMIT" blocks are replaced. The valueless TypeScript-face case now also asserts the tolerant refusal.
Ablation (fix reverted, pins kept)
The ablation leg used objectstack's
scripts/ablation-replace.mjs. It swapped the routed constructor incomplex.zod.tsback toz.union([DashboardWidgetSlotComponentSchema, BaseSchema]):41e1da22941826e88ad8df9031b0d101e700a73e→1f39e187437f7bfaebbc8523abf7c93ff3ad73d8.41e1da2294, andgit diff HEADis empty.The test imports the schema by relative path, so it resolves to source and no build was in the loop. The leg ran at
0683a7f. The commits after it change only the changeset and the census test;complex.zod.tsand the pin file are byte-identical at32dfdf0.One test outside the claimed file surface: objectui#11073's census
terminal-unknown-key-refusal-11073.test.tsmeasures the terminal unknown-key refusal. It compares each plain union against an open twin rebuilt from the union's def. It built every twin with a plainz.ZodUnion, which drops the slot's routing. On the slot's strict twin,{ type: 'metric-card' }therefore readclosed=false open=true, and the census reported it as an accept move of the terminal refusal.Measured directly, the routed slot refuses that card, a plain
z.ZodUniontwin accepts it, and an own-constructor twin refuses it. The one-line change builds the twin with the union's own constructor (u._zod.constr), for the same reason the file already keepsdef.checkson the twin. This file is not on the claim's file surface, and the report asks the seat to amend it.Producers
I counted the tracked files at the base for a
metric-cardinside acomponentobject, with the README as the lit positive control. Only two places hold one:packages/plugin-dashboard/README.md's TypeScript Support block (kpi_custom, which is well-formed and still parses, pinned) and test fixtures insidepackages/types. No producer exists underexamples/**,content/docs/**orapps/**. The README is untouched.Clause-② evidence
I compared the built
@object-ui/typesdeclarations, base against head:.d.tsfiles, and 1182exportheads on each side, with an identical md5;WidgetComponentSlot, instead of printing the union inline. The alias is declared withoutexport, and the module endsexport {};zod/index.zod.d.tsandzod/layout.zod.d.ts.Nothing widens.
Local verification, head
32dfdf0pnpm exec vitest run packages/types/: 359 files, 9566 tests passed.pnpm --filter @object-ui/types type-check: exit 0, over three programs.tsconfig.test.jsonlists both edited test files.pnpm --filter @object-ui/types lint(eslint .): 0 errors over a population of 440 files, read from--format json. Warnings on the three touched files are unchanged, base against head (0 / 6 / 2). Type-aware linting is not enabled (parserOptionsis empty), so this diff cannot move a verdict on an untouched file.pnpm exec vitest run packages/plugin-dashboard/: 172 files, 1699 passed, 6 skipped.examples/schema-catalog/: 41 files, 2263 passed.packages/cli/: 25 files, 369 passed.pnpm --filter @object-ui/plugin-dashboard type-check: exit 0. It neededfields,permissions,providersandplugin-chartsbuilt first.@object-ui/cliand its closure at the base;@object-ui/typesand@object-ui/cliat1b08f95. From there to32dfdf0only the changeset and one test file move, so the built source is identical.package.jsonand workflows, all exit 0:check:new-line-citations(0 new),check:control-bytes,check-changeset-presence,check-changeset-fixed,check-changeset-no-major,check:changeset-claims,check:pending-changeset-literals,check-governed-queue-guard --testover the four paths (NOT GOVERNED),check:spec-symbols,check:test-path-roots,check:vi-mock-override-shape,check:esm-specifiers,check:unreferenced-sources,check:doc-types.check:doc-snippetsandcheck:doc-examples: exit 2, PRECONDITION NOT MET, because the packages they resolve against are unbuilt;check:readme-exports: exit 1, "the population COLLAPSED -- this run proves nothing", for the same reason;examples/**type-check: no example authors a legacy envelope.Acceptance notes
labelinside the envelope through theBaseSchemafallback. This change makes that false. This PR's changeset supersedes that line in the same release, the shape objectui#11608's note used for objectui#8347's. Editing the objectui#4425 changeset directly would need the claim's surface amended.bodywas already refused at the base. It was refused through the envelope by both arms, so the loss there was the message, not the verdict. Forlabel,children, an out-of-enumtrendand a valueless card, the verdict was lost.objectui validateprints the arm's refusal "as one arm of two". Measured, the CLI's arm selection prints the component arm's issue alone for a refused card directly inwidgets[]. I did not edit it, because it is outside the routing the claim covers. Carrier: none.main. The three commits that landed sinceda35453touch no path in the packages this diff touches.Generated by Claude Code