From 6c8034d1422a2fc91ec09c9f97925470a27bff77 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 08:44:46 +0000 Subject: [PATCH 1/2] fix(types): declare the component-node arm of DashboardComponentSchema.widgets on the TypeScript face MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `widgets` was `DashboardWidgetSchema[]` while the zod twin has carried a two-arm union since the 2026-08-14 ruling (objectstack#8593): a component node placed directly in the slot (`type: 'metric-card'`, passthrough `BaseSchema` body) or a spec-family widget. Every `metric-card` block the plugin-dashboard README teaches parsed green and `tsc --strict` refused it (6 x TS2561 on `value` at fc32921). Ruled option (a), decision batch #68. - `DashboardWidgetSlotComponentSchema` — `BaseSchema` with `type` narrowed to the closed `DASHBOARD_COMPONENT_WIDGET_TYPES`; exported (declaration emit needs a name, TS4033) and added to the barrel. - `widgets: Array`, component arm first, as in the zod twin. `DashboardWidgetSchema` is NOT widened with `value` / `icon` / `trend` / `trendValue`. - Two-face pin `dashboard-widget-slot-component-arm-7952.test.ts`: README shape compiles and parses; forbidden widening still refused; discriminated refusals hold; the measured union corner (type-less envelope + stray key compiles, zod refuses by name) recorded, not contracted. - README "TypeScript Support" table and prose corrected to the two-arm slot. - Changeset: `@object-ui/types` minor (published TS accept set widens). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX --- .../7952-dashboard-widgets-component-arm.md | 13 ++ packages/plugin-dashboard/README.md | 23 +- ...ard-widget-slot-component-arm-7952.test.ts | 201 ++++++++++++++++++ packages/types/src/complex.ts | 65 +++++- packages/types/src/index.ts | 1 + 5 files changed, 298 insertions(+), 5 deletions(-) create mode 100644 .changeset/7952-dashboard-widgets-component-arm.md create mode 100644 packages/types/src/__tests__/dashboard-widget-slot-component-arm-7952.test.ts diff --git a/.changeset/7952-dashboard-widgets-component-arm.md b/.changeset/7952-dashboard-widgets-component-arm.md new file mode 100644 index 0000000000..42b36e9a91 --- /dev/null +++ b/.changeset/7952-dashboard-widgets-component-arm.md @@ -0,0 +1,13 @@ +--- +"@object-ui/types": minor +--- + +`DashboardComponentSchema.widgets` gains the component-node arm on the TypeScript face (objectui#7952) + +`widgets` was `DashboardWidgetSchema[]` in TypeScript while the zod schema it mirrors has been a two-arm union since the 2026-08-14 ruling (objectstack#8593): a component node placed directly in the widget slot (`type: 'metric-card'`, body validated as passthrough `BaseSchema`) or a spec-family widget. So the shape `@object-ui/plugin-dashboard`'s README teaches in every `metric-card` example — and the shape the shipped `DashboardRenderer` renders — parsed green under `safeParse` and was refused by `tsc --strict` (`TS2561: 'value' does not exist in type 'DashboardWidgetSchema'`, six occurrences across the README's dashboard blocks at `fc32921`). There was no annotation an author could write for a document the platform accepts. + +**Accept-set change (Clause ②, TypeScript face only).** `widgets` is now `Array`, and `DashboardWidgetSlotComponentSchema` — `BaseSchema` with `type` narrowed to the closed `DASHBOARD_COMPONENT_WIDGET_TYPES` — is a new export of `@object-ui/types`. The zod schema is unchanged; `DashboardWidgetSchema` is NOT widened with `value` / `icon` / `trend` / `trendValue` (those are `MetricCard`'s registry inputs, not widget keys — the compiler's `Did you mean to write 'values'?` points at the repair both declarations forbid). + +**What still refuses.** A widget that names a spec-family `type` and carries an undeclared key (`{ type: 'bar', bogus: 1 }`) is still a `tsc` error: the literal is discriminated by `type`, so the passthrough arm never applies to it. A `type` outside both vocabularies is refused as before. The one corner the TypeScript union cannot discriminate — a legacy `component` envelope with NO `type` plus an undeclared key — compiles on the TypeScript face and is refused by name at validation, as every `BaseSchema` slot already behaves. + +**Consumers.** The new arm is assignable to `DashboardWidgetSchema`, so code that annotates a widget callback `(w: DashboardWidgetSchema)` keeps compiling unchanged. Code that reads a property off an unannotated element of `schema.widgets` now sees the union, and through `BaseSchema`'s index signature that read is `any` rather than the widget's declared type — annotate the parameter to keep the narrower type. diff --git a/packages/plugin-dashboard/README.md b/packages/plugin-dashboard/README.md index 99358fbbd3..b3a30a83cf 100644 --- a/packages/plugin-dashboard/README.md +++ b/packages/plugin-dashboard/README.md @@ -426,12 +426,14 @@ The authored shape is typed by `@object-ui/types`: | --- | --- | | `DashboardComponentSchema` | the whole `type: 'dashboard'` node — `columns`, `gap`, `widgets`, `header`, `globalFilters`, `dateRange`, `refreshInterval`, … | | `DashboardWidgetSchema` | one entry of `widgets[]` — the spec's `DashboardWidget` keys, plus objectui's own (`component`, `layout`, `options`, …) | +| `DashboardWidgetSlotComponentSchema` | the other kind of `widgets[]` entry — a component node placed directly in the slot, `type` one of the closed component set (`metric-card`); every other key is that component's own prop | | `DashboardWidgetLayout` | a widget's `{ x, y, w, h }` grid box | ```typescript import type { DashboardComponentSchema, DashboardWidgetSchema, + DashboardWidgetSlotComponentSchema, } from '@object-ui/types'; // Dataset-bound KPI — the widget vocabulary (see "Dashboard-level filters"). @@ -466,11 +468,22 @@ const custom: DashboardWidgetSchema = { layout: { x: 0, y: 0, w: 3, h: 2 }, }; +// Component node directly in the slot — the shape every `metric-card` example +// above uses. `type` is one of the closed component set; the other keys are +// the component's own props, carried by `BaseSchema`'s index signature. +const kpi: DashboardWidgetSlotComponentSchema = { + type: 'metric-card', + title: 'Revenue', + value: '$123,456', + trend: 'up', + trendValue: '+12%', +}; + const dashboard: DashboardComponentSchema = { type: 'dashboard', columns: 3, gap: 4, - widgets: [revenue, users, custom], + widgets: [revenue, users, custom, kpi], }; ``` @@ -479,8 +492,12 @@ covers every `type` (`metric`, `bar`, `table`, …) and the family-specific settings live under `options`. `MetricCard`'s own props — `value`, `trend`, `trendValue` — are the component's, not the widget's: `DashboardWidgetSchema` declares none of them, and the component's props interface is not on this -package's export surface either. So a `metric-card` node is typed only where it -appears as a component (the `component` slot above), not as a widget family. +package's export surface either. A `metric-card` node is typed as a COMPONENT +node in both places it can appear: in a widget's `component` slot (`custom` +above) and directly in `widgets[]` (`kpi` above, `DashboardWidgetSlotComponentSchema` +— the second arm of `DashboardComponentSchema['widgets']`, matching the zod +schema's two-arm slot). Its keys are checked as `BaseSchema` keys either way, +never as widget keys. ## Customization diff --git a/packages/types/src/__tests__/dashboard-widget-slot-component-arm-7952.test.ts b/packages/types/src/__tests__/dashboard-widget-slot-component-arm-7952.test.ts new file mode 100644 index 0000000000..2639b19d22 --- /dev/null +++ b/packages/types/src/__tests__/dashboard-widget-slot-component-arm-7952.test.ts @@ -0,0 +1,201 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#7952 — `DashboardComponentSchema.widgets` carries the component-node + * arm on the TypeScript face, matching the Zod twin's two-arm slot. + * + * ## The gap this closes + * + * `zod/complex.zod.ts` has routed a `metric-card` node placed directly in the + * widget slot to passthrough `BaseSchema` since the 2026-08-14 ruling + * (objectstack#8593): `widgets: z.array(z.union([DashboardWidgetSlotComponentSchema, + * DashboardWidgetSchema]))`. The TypeScript declaration stayed one-armed, + * `DashboardWidgetSchema[]`. Measured at `fc32921` on the six dashboard blocks + * `plugin-dashboard/README.md` teaches, each annotated `DashboardComponentSchema`: + * `safeParse` ACCEPT with every authored key preserved; `tsc --strict` 6 × TS2561 + * (`'value' does not exist in type 'DashboardWidgetSchema'. Did you mean to + * write 'values'?`). No annotation existed for a document the platform accepts + * and the maintainer ruled legal. Ruled option (a), director seat, decision + * batch #68 (2026-09-07): the TypeScript face gains the arm; the Zod face is + * untouched and `DashboardWidgetSchema` is NOT widened. + * + * ## What is pinned, and on which face + * + * 1. the README's `metric-card` shape annotates and compiles (type level) AND + * parses green with its keys kept (runtime) — the two halves of the card's + * measurement, now agreeing; + * 2. the forbidden repair did not happen: `DashboardWidgetSchema` still refuses + * `value` — an `@ts-expect-error` that turns into TS2578 if anyone widens it; + * 3. the arm is CLOSED on `type` and the union still discriminates: a + * spec-family widget with an undeclared key is refused on both faces, and a + * `type` in neither vocabulary is refused; + * 4. the measured limit of a TypeScript union with a passthrough arm, recorded + * two-faced so it cannot be read as a hatch: a `type`-less legacy envelope + * with an undeclared key COMPILES (nothing to discriminate on, so the arm's + * index signature satisfies the excess-property check) while the Zod face + * refuses it by name; + * 5. shape identity: the slot's element type IS the two-arm union, the arm's + * `type` IS `DashboardComponentWidgetType`, and the arm is assignable to + * `DashboardWidgetSchema` — which is why every `(w: DashboardWidgetSchema)` + * callback in `plugin-dashboard` compiled unchanged. + * + * Type-level lines are erased at runtime and enforced because + * `packages/types/tsconfig.test.json` is chained from this package's + * `type-check` script (objectui#3009). Reverse-verified at the PR: with `widgets` + * restored to `DashboardWidgetSchema[]`, `tsc -p tsconfig.test.json` goes red on + * the lines marked REVERSE below and nowhere else in this file. + */ + +import { describe, it, expect } from 'vitest'; +import type { + DashboardComponentSchema, + DashboardComponentWidgetType, + DashboardWidgetSchema, + DashboardWidgetSlotComponentSchema, +} from '../complex.js'; +import { DASHBOARD_COMPONENT_WIDGET_TYPES } from '../complex.js'; +import { DashboardComponentSchema as DashboardComponentZod } from '../zod/complex.zod.js'; + +type Equal< A, B > = + (< T >() => T extends A ? 1 : 2) extends (< T >() => T extends B ? 1 : 2) ? true : false; + +/** + * `packages/plugin-dashboard/README.md`'s "Usage" block, byte-for-byte, with the + * annotation the card measured it under. The runtime half of the same document + * is read off the page by `plugin-dashboard`'s + * `readme-dashboard-examples-spec-valid.test.ts`; this copy exists because a + * type-level pin cannot read a file. + */ +// REVERSE — TS2561 on `value` with the one-arm declaration. +const usage: DashboardComponentSchema = { + type: 'dashboard', + widgets: [ + { + type: 'metric-card', + title: 'Total Sales', + value: '$123,456', + trend: 'up', + trendValue: '+12%' + } + ] +}; + +describe('the component-node arm is declared on the TypeScript face (objectui#7952)', () => { + it('the README shape annotates, compiles, and parses green with every key kept', () => { + const result = DashboardComponentZod.safeParse(usage); + expect(result.success).toBe(true); + if (!result.success) return; + const parsed = (result.data as { widgets: Record[] }).widgets[0]; + for (const key of Object.keys(usage.widgets[0])) expect(parsed).toHaveProperty(key); + }); + + it('the slot element IS the two-arm union, in the Zod twin\'s order', () => { + type Element = DashboardComponentSchema['widgets'][number]; + // REVERSE — `Element` collapses to `DashboardWidgetSchema` and this is `false`. + const twoArm: Equal< Element, DashboardWidgetSlotComponentSchema | DashboardWidgetSchema > = true; + // The arm's `type` is the closed component set, by reference — not a copy. + const closedByReference: Equal< DashboardWidgetSlotComponentSchema['type'], DashboardComponentWidgetType > = true; + // The arm is assignable to the widget type: consumers annotating a widget + // callback `(w: DashboardWidgetSchema)` keep compiling on the union. + const armAssignable: DashboardWidgetSlotComponentSchema extends DashboardWidgetSchema ? true : false = true; + expect(twoArm && closedByReference && armAssignable).toBe(true); + // The runtime side of "by reference": the set the arm keys on is the one + // export, and it is the set the Zod arm reads. + expect(DASHBOARD_COMPONENT_WIDGET_TYPES).toContain(usage.widgets[0].type); + }); +}); + +describe('the forbidden repair did not happen — DashboardWidgetSchema is not widened', () => { + it('`value` is still not a widget key on the TypeScript face', () => { + // `value` / `icon` / `trend` / `trendValue` are `MetricCard`'s registry + // inputs. The compiler's own suggestion for this line ("Did you mean to + // write 'values'?") is the repair both declarations forbid; if anyone + // makes it, this directive goes unused (TS2578) and `type-check` fails. + // @ts-expect-error — TS2561: 'value' does not exist in type 'DashboardWidgetSchema'. + const widened: DashboardWidgetSchema = { type: 'metric-card', value: '1' }; + expect(widened.type).toBe('metric-card'); + }); + + it('the arm\'s `type` is closed', () => { + // @ts-expect-error — TS2322: a spec family is not a component type. + const open: DashboardWidgetSlotComponentSchema = { type: 'bar' }; + expect(open.type).toBe('bar'); + }); +}); + +describe('NOT A HATCH — what the union still refuses, on both faces', () => { + it('a spec-family widget with an undeclared key is discriminated by `type` and refused', () => { + const doc: DashboardComponentSchema = { + type: 'dashboard', + widgets: [ + // `'bar'` excludes the component arm, so the excess-property check runs + // against `DashboardWidgetSchema` alone. + // @ts-expect-error — TS2353: 'bogus' does not exist in type 'DashboardWidgetSchema'. + { type: 'bar', title: 'x', bogus: 1 }, + ], + }; + const result = DashboardComponentZod.safeParse(doc); + expect(result.success).toBe(false); + if (result.success) return; + const flat = JSON.stringify(result.error.issues); + expect(flat).toContain('unrecognized_keys'); + expect(flat).toContain('bogus'); + }); + + it('a `type` in neither vocabulary is refused', () => { + const doc: DashboardComponentSchema = { + type: 'dashboard', + widgets: [ + // @ts-expect-error — TS2322: not a widget family, not a component type. + { type: 'not-a-component', value: '1' }, + ], + }; + expect(DashboardComponentZod.safeParse(doc).success).toBe(false); + }); + + it('a component node with an undeclared key is kept whole — that is the passthrough, by ruling', () => { + const doc: DashboardComponentSchema = { + type: 'dashboard', + widgets: [{ type: 'metric-card', title: 'x', someProp: 1 }], + }; + const result = DashboardComponentZod.safeParse(doc); + expect(result.success).toBe(true); + if (!result.success) return; + expect((result.data as { widgets: Record[] }).widgets[0]).toHaveProperty('someProp', 1); + }); +}); + +describe('MEASURED LIMIT of a TypeScript union with a passthrough arm — recorded, not a contract', () => { + it('a `type`-less legacy envelope with an undeclared key compiles, and the Zod face refuses it by name', () => { + // Nothing to discriminate on (the legacy `component` envelope has no + // `type`), so the union's excess-property check accepts any key one arm + // could hold, and the component arm's index signature holds every key. + // If tsc ever refuses this literal, the corner has closed: delete this + // constant and the note on `widgets` in `complex.ts` — ⛔ do not add an + // `@ts-expect-error` to keep the file green. + const envelopeStray: DashboardComponentSchema = { + type: 'dashboard', + widgets: [{ id: 'w', component: { type: 'metric-card', value: '1' }, bogus: 1 }], + }; + const result = DashboardComponentZod.safeParse(envelopeStray); + expect(result.success, 'the runtime is the strict face on this corner').toBe(false); + if (result.success) return; + const flat = JSON.stringify(result.error.issues); + expect(flat).toContain('unrecognized_keys'); + expect(flat).toContain('bogus'); + }); + + it('the same envelope without the stray key is legal on both faces', () => { + const envelope: DashboardComponentSchema = { + type: 'dashboard', + widgets: [{ id: 'w', component: { type: 'metric-card', value: '1' }, layout: { x: 0, y: 0, w: 1, h: 1 } }], + }; + expect(DashboardComponentZod.safeParse(envelope).success).toBe(true); + }); +}); diff --git a/packages/types/src/complex.ts b/packages/types/src/complex.ts index f10bd8e474..b7571577cc 100644 --- a/packages/types/src/complex.ts +++ b/packages/types/src/complex.ts @@ -1719,7 +1719,9 @@ export interface FloatingChatbotConfig { * `plugin-dashboard/src/index.tsx`). Those props are NOT widget keys and must * not be added to {@link DashboardWidgetSchema}; a member of this list is * validated as a component node against objectui's own passthrough - * `BaseSchema`, which is what keeps them. + * `BaseSchema`, which is what keeps them. On the TypeScript face that node is + * {@link DashboardWidgetSlotComponentSchema}, the second arm of + * `DashboardComponentSchema.widgets` (objectui#7952). * * ⛔ CLOSED on purpose. The ruling's triage block named an open * "extension allowed" hatch as the thing to avoid: an open hatch re-creates @@ -1869,6 +1871,42 @@ export interface DashboardWidgetSchema pagination?: boolean; } +/** + * A COMPONENT node sitting directly in a dashboard's widget slot — the + * `metric-card` extension the 2026-08-14 ruling (objectstack#8593) admits, on + * the TypeScript face. Twin of `zod/complex.zod.ts` + * `DashboardWidgetSlotComponentSchema`, spelled the same way that arm is: + * `BaseSchema` plus a `type` narrowed to the CLOSED component set + * ({@link DASHBOARD_COMPONENT_WIDGET_TYPES}). + * + * `BaseSchema`'s `[key: string]: any` is the passthrough. `value` / `icon` / + * `trend` / `trendValue` are `MetricCard`'s registry `inputs`, not widget keys: + * they reach the compiler through the index signature here and MUST NOT be + * declared on {@link DashboardWidgetSchema} — the compiler's own TS2561 + * suggestion ("Did you mean to write 'values'?") points at exactly that + * forbidden repair. + * + * Until objectui#7952 this arm existed on the Zod face only: + * `DashboardComponentSchema.widgets` was `DashboardWidgetSchema[]`, so the + * `metric-card` blocks `plugin-dashboard/README.md` teaches parsed green + * under `safeParse` and `tsc --strict` refused every one of them (6 × TS2561 + * on `value`, measured at `fc32921`). Ruled option (a) by the director seat + * (decision batch #68, 2026-09-07, maintainer 「同意」): the TypeScript face + * gains the second arm; the Zod face is untouched. + * + * Exported where the Zod twin is not: an exported member cannot name a + * private interface in the emitted `.d.ts` (TS4033), and a name is what lets + * an author annotate the node the platform accepts. Same interface shape as + * {@link DashboardComponentSchema} itself (`extends BaseSchema` + a literal + * `type`), which is why it is an interface rather than an intersection. + * + * Pinned by `__tests__/dashboard-widget-slot-component-arm-7952.test.ts`. + */ +export interface DashboardWidgetSlotComponentSchema extends BaseSchema { + /** An objectui component type legal in a widget slot — the CLOSED set. */ + type: DashboardComponentWidgetType; +} + /** * Dashboard Schema */ @@ -1894,7 +1932,30 @@ export interface DashboardComponentSchema extends BaseSchema { // `__tests__/dashboard-title-retired-declaration.test.ts`. columns?: number; gap?: number; - widgets: DashboardWidgetSchema[]; + /** + * The widget slot — TWO arms, matching the Zod twin (`zod/complex.zod.ts` + * `DashboardComponentSchema.widgets`, component arm first): a component + * node ({@link DashboardWidgetSlotComponentSchema}, `type` in the closed + * {@link DASHBOARD_COMPONENT_WIDGET_TYPES}) or a spec-family / legacy + * `component`-envelope widget ({@link DashboardWidgetSchema}). + * + * One-armed (`DashboardWidgetSchema[]`) until objectui#7952, which refused + * the shape the 2026-08-14 ruling (objectstack#8593) admits and the runtime + * renders — see the arm's own docblock for the measurement and the ruling. + * + * ⚠️ Measured limits of a TypeScript union with a passthrough arm, recorded + * so nobody reads them as a hatch (pinned two-faced next to the arm): + * - a literal that NAMES a `type` outside the component set is discriminated + * by it — `{ type: 'bar', bogus: 1 }` is still refused, because `'bar'` + * excludes the component arm and `DashboardWidgetSchema` has no `bogus`; + * - a literal with NO `type` (the legacy `component` envelope) cannot be + * discriminated, and the component arm's index signature then satisfies + * the union's excess-property check, so `{ component, bogus: 1 }` + * compiles here. The Zod face refuses it (`.strict()` widget schema) — + * the runtime is the strict face on that corner, as it already was for + * every `BaseSchema` slot. + */ + widgets: Array; /** Auto-refresh interval in seconds. When set, the dashboard will periodically trigger onRefresh. */ refreshInterval?: number; /** diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 67b5bfc65f..f48568d2a1 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -321,6 +321,7 @@ export type { CarouselSchema, DashboardWidgetLayout, DashboardWidgetSchema, + DashboardWidgetSlotComponentSchema, DashboardComponentSchema, DashboardComponentWidgetType, DashboardWidgetTypeExtension, From 197e248546c524fb94698a0eef908c56755a2552 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 10:52:48 +0000 Subject: [PATCH 2/2] docs(types): state the real reason the widget-slot component arm is exported MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contract review on PR #8296 measured the docblock's claim that TS4033 forces the export as false: in this package's own build (declaration + composite, TypeScript 6.0.3) a non-exported arm referenced from the exported DashboardComponentSchema emits into dist/complex.d.ts as a local interface, exit 0, and a barrel consumer still writes the node with no name. - arm docblock: the export is a deliberate authoring-surface decision — a name an author can annotate the node with — taken intentionally in the opposite direction to the zod twin's non-exported const. - "the second arm" → the component arm, first in the declaration as in zod (complex.ts closed-set docblock, plugin-dashboard README). Comment and prose only; no declaration, pin, changeset or barrel change. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX --- packages/plugin-dashboard/README.md | 5 +++-- packages/types/src/complex.ts | 23 +++++++++++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/plugin-dashboard/README.md b/packages/plugin-dashboard/README.md index b3a30a83cf..ee6c54a67a 100644 --- a/packages/plugin-dashboard/README.md +++ b/packages/plugin-dashboard/README.md @@ -495,8 +495,9 @@ declares none of them, and the component's props interface is not on this package's export surface either. A `metric-card` node is typed as a COMPONENT node in both places it can appear: in a widget's `component` slot (`custom` above) and directly in `widgets[]` (`kpi` above, `DashboardWidgetSlotComponentSchema` -— the second arm of `DashboardComponentSchema['widgets']`, matching the zod -schema's two-arm slot). Its keys are checked as `BaseSchema` keys either way, +— the component arm of `DashboardComponentSchema['widgets']`, first in the +declaration as in the zod schema's two-arm slot). Its keys are checked as +`BaseSchema` keys either way, never as widget keys. ## Customization diff --git a/packages/types/src/complex.ts b/packages/types/src/complex.ts index b7571577cc..60cb9fac9e 100644 --- a/packages/types/src/complex.ts +++ b/packages/types/src/complex.ts @@ -1720,8 +1720,8 @@ export interface FloatingChatbotConfig { * not be added to {@link DashboardWidgetSchema}; a member of this list is * validated as a component node against objectui's own passthrough * `BaseSchema`, which is what keeps them. On the TypeScript face that node is - * {@link DashboardWidgetSlotComponentSchema}, the second arm of - * `DashboardComponentSchema.widgets` (objectui#7952). + * {@link DashboardWidgetSlotComponentSchema}, the component arm — first, as + * in the Zod twin — of `DashboardComponentSchema.widgets` (objectui#7952). * * ⛔ CLOSED on purpose. The ruling's triage block named an open * "extension allowed" hatch as the thing to avoid: an open hatch re-creates @@ -1892,13 +1892,20 @@ export interface DashboardWidgetSchema * under `safeParse` and `tsc --strict` refused every one of them (6 × TS2561 * on `value`, measured at `fc32921`). Ruled option (a) by the director seat * (decision batch #68, 2026-09-07, maintainer 「同意」): the TypeScript face - * gains the second arm; the Zod face is untouched. + * gains the component arm; the Zod face is untouched. * - * Exported where the Zod twin is not: an exported member cannot name a - * private interface in the emitted `.d.ts` (TS4033), and a name is what lets - * an author annotate the node the platform accepts. Same interface shape as - * {@link DashboardComponentSchema} itself (`extends BaseSchema` + a literal - * `type`), which is why it is an interface rather than an intersection. + * Exported on purpose, where the Zod twin is not. The compiler does not force + * it — measured in this package's own build (`declaration` + `composite`, + * TypeScript 6.0.3): a non-exported arm referenced from the exported + * `DashboardComponentSchema` emits into `dist/complex.d.ts` as a local + * interface, exit 0, and a barrel consumer still writes the node with no + * name. The export is an authoring-surface decision — a name an author can + * annotate the node with, which `plugin-dashboard/README.md` teaches — taken + * deliberately in the opposite direction to the Zod arm, whose own docblock + * keeps that const private because its routing is an internal property of + * the slot. Same interface shape as {@link DashboardComponentSchema} itself + * (`extends BaseSchema` + a literal `type`), which is why it is an interface + * rather than an intersection. * * Pinned by `__tests__/dashboard-widget-slot-component-arm-7952.test.ts`. */