From 5109e3ae26e23235c24d970c60e8e477183372ed Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 07:51:50 +0000 Subject: [PATCH 01/14] refactor(plugin-kanban,plugin-timeline): type the two runtime keys their readers take off the node, where those readers live `ObjectKanban` reads the gate-written `sort` (the binding's `dataSource.sort`, objectui#10068) through a non-exported read type, `GateBoundKanbanSchema`, and `TimelineRenderSchema` declares the `onItemClick` slot `ObjectTimeline` composes. Neither key joins an authoring face: `sort` stays undeclared on `ObjectKanbanSchema` (objectui#8174) and `onItemClick` stays off `TimelineSchema` (the objectui#6170 docblock). Both reads compiled only as `any` through `BaseSchema`'s index signature; this compiles with or without it. Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude --- packages/plugin-kanban/src/ObjectKanban.tsx | 40 ++++++++++++++++--- packages/plugin-timeline/src/renderHandoff.ts | 27 +++++++++---- 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/packages/plugin-kanban/src/ObjectKanban.tsx b/packages/plugin-kanban/src/ObjectKanban.tsx index a4c9516e98..868bb43f08 100644 --- a/packages/plugin-kanban/src/ObjectKanban.tsx +++ b/packages/plugin-kanban/src/ObjectKanban.tsx @@ -7,7 +7,7 @@ */ import React, { useEffect, useState, useMemo } from 'react'; -import type { DataSource, ObjectKanbanSchema } from '@object-ui/types'; +import type { DataSource, ObjectKanbanSchema, SortConfig } from '@object-ui/types'; import { useDataScope, useNavigationOverlay, @@ -333,9 +333,12 @@ export interface ObjectKanbanComponentProps { * Nothing, for an `object-kanban` document. Such a node was never judged by * the `kanban` arm, so every key this component reads off `schema` that * `ObjectKanbanSchema` does not declare reached the renderer through - * {@link BaseSchema}'s `[key: string]: any` BEFORE the retirement and still - * does. ⇒ No `object-kanban` node changes meaning; what changed is that - * `kanban` nodes no longer exist. + * {@link BaseSchema}'s `[key: string]: any` BEFORE the retirement, and did + * after it. ⇒ No `object-kanban` node changed meaning; what changed is that + * `kanban` nodes no longer exist. Since objectui#8347 `BaseSchema` carries no + * index signature, so a read of an undeclared key here no longer compiles; + * the one key the gate writes that the node does not declare is typed at + * {@link GateBoundKanbanSchema}. * * ⛔ WHICH keys those are is deliberately not listed here, and neither is * how many there are. This paragraph listed both (objectui#8802, @@ -389,8 +392,32 @@ export interface ObjectKanbanComponentProps { onCardClick?: (record: any, event?: any) => void; } +/** + * The board node as this component READS it: {@link ObjectKanbanSchema} plus + * the one key `ElementDataSourceGate` writes onto it that the node does not + * declare (objectui#8347). + * + * `sort` is the gate's carrier for the per-element binding's `dataSource.sort` + * (`OBJECT_KANBAN_DATA_SOURCE` in `./index.tsx` maps it, objectui#10068), and + * the fetch lowers it onto `$orderby`. It is ⛔ not an authoring key: the + * spec's `object-kanban` props declare no top-level `sort` and refuse one, so + * neither `ObjectKanbanSchema` face declares it (objectui#8174, pinned by + * `_KanbanSortStaysUndeclared` in `@object-ui/types`). While `BaseSchema` + * carried `[key: string]: any` the read compiled as `any`; this type names it + * where its only reader lives instead, the way `plugin-timeline`'s + * `renderHandoff.ts` types the keys its composer writes (objectui#6356). + * + * ⛔ Deliberately NOT exported, and ⛔ never to be added to + * `ObjectKanbanComponentProps` or `@object-ui/types`: a public type naming + * `sort` would invite the spelling the authoring faces refuse. + */ +type GateBoundKanbanSchema = ObjectKanbanSchema & { + /** The binding's (or its view's) ordering, written by the gate. */ + sort?: SortConfig[]; +}; + export const ObjectKanban: React.FC = ({ - schema, + schema: boardSchema, dataSource, className, data: externalData, @@ -400,6 +427,9 @@ export const ObjectKanban: React.FC = ({ ..._props }) => { void _props; + // Widened, not cast: the gate-written `sort` is optional, so the declared + // node is assignable to the read type as it stands (see `GateBoundKanbanSchema`). + const schema: GateBoundKanbanSchema = boardSchema; const { translateOptions, fieldLabel } = useSafeFieldLabel(); const tt = useSafeTranslate(); // Separate from `tt` because the record-detail heading interpolates a label — diff --git a/packages/plugin-timeline/src/renderHandoff.ts b/packages/plugin-timeline/src/renderHandoff.ts index aa12fa7fa4..2d07f2f3d5 100644 --- a/packages/plugin-timeline/src/renderHandoff.ts +++ b/packages/plugin-timeline/src/renderHandoff.ts @@ -67,17 +67,30 @@ export interface TimelineRenderFeedItem extends Omit; + /** + * The click handler `ObjectTimeline` composes onto the schema it hands over + * (it resolves the item's record and fires the overlay navigation). A runtime + * slot, not authorable metadata: it stays off `TimelineSchema` on purpose, + * per the objectui#6170 docblock in `@object-ui/types` (`data-display.ts`). + * While `BaseSchema` carried `[key: string]: any` the renderer's read of it + * compiled as `any`; objectui#8347 removed the signature, so it is typed here, + * where its only producer and its only consumer live. The gantt branch also + * passes the authored row and both indices. + */ + onItemClick?: (item: unknown, authored?: unknown, rowIndex?: number, itemIndex?: number) => void; }; From 0a0ef9ab098f1016e8ecfc4af97741e6ba9f014b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 07:51:50 +0000 Subject: [PATCH 02/14] test(types,plugin-gantt,plugin-map): the mechanical rows that stop compiling without `BaseSchema`'s index signature Two schema-to-record reads convert through `unknown`, and the gantt and map `propertiesBag-10859` fixtures keep `operator` a literal and type the bag node as its block node. Each compiles with the signature present too. Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude --- .../src/ObjectGantt.propertiesBag-10859.test.tsx | 9 ++++++--- .../src/ObjectMap.propertiesBag-10859.test.tsx | 9 ++++++--- .../src/__tests__/action-callback-retired-7068.test.ts | 2 +- .../__tests__/calendar-date-alias-refusal-8355.test.ts | 2 +- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/plugin-gantt/src/ObjectGantt.propertiesBag-10859.test.tsx b/packages/plugin-gantt/src/ObjectGantt.propertiesBag-10859.test.tsx index 5cccc6b353..8867eaf353 100644 --- a/packages/plugin-gantt/src/ObjectGantt.propertiesBag-10859.test.tsx +++ b/packages/plugin-gantt/src/ObjectGantt.propertiesBag-10859.test.tsx @@ -34,7 +34,7 @@ import { describe, it, expect, vi } from 'vitest'; import { render, waitFor } from '@testing-library/react'; import React from 'react'; import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react'; -import type { DataSource, DeclaredNode } from '@object-ui/types'; +import type { DataSource, DeclaredNode, ObjectGanttBlockNode } from '@object-ui/types'; import { safeValidateSchema } from '@object-ui/types/zod'; vi.mock('sonner', () => ({ toast: { error: vi.fn() } })); @@ -73,7 +73,10 @@ vi.mock('@object-ui/plugin-detail', async (importOriginal) => ({ import './index'; const GANTT = { startDateField: 'start_date', endDateField: 'end_date', titleField: 'name', progressField: 'progress' }; -const FILTER = [{ field: 'status', operator: 'equals', value: 'open' }]; +// `as const` keeps `operator` the literal the bag's filter row declares; a +// widened `string` is refused there (objectui#8347 took away the index +// signature that used to admit the bag on the post-hoist node type). +const FILTER = [{ field: 'status', operator: 'equals' as const, value: 'open' }]; const SORT: Array<{ field: string; order: 'asc' | 'desc' }> = [{ field: 'name', order: 'desc' }]; const ROWS = [ { id: 't1', name: 'Design', start_date: '2026-03-02', end_date: '2026-03-06', progress: 100, status: 'open' }, @@ -81,7 +84,7 @@ const ROWS = [ ]; /** The authored spelling, spec-valid. */ -const BAG: DeclaredNode = { type: 'object-gantt', properties: { objectName: 'task', gantt: GANTT, filter: FILTER, sort: SORT } }; +const BAG: ObjectGanttBlockNode = { type: 'object-gantt', properties: { objectName: 'task', gantt: GANTT, filter: FILTER, sort: SORT } }; /** The same props written flat, with the `gantt` block — what a code composer can build. */ const FLAT: DeclaredNode = { type: 'object-gantt', objectName: 'task', gantt: GANTT, filter: FILTER, sort: SORT }; /** The flattened `GanttConfig` keys `ObjectView` writes onto the node it composes. */ diff --git a/packages/plugin-map/src/ObjectMap.propertiesBag-10859.test.tsx b/packages/plugin-map/src/ObjectMap.propertiesBag-10859.test.tsx index fd35aafb40..302dcf396a 100644 --- a/packages/plugin-map/src/ObjectMap.propertiesBag-10859.test.tsx +++ b/packages/plugin-map/src/ObjectMap.propertiesBag-10859.test.tsx @@ -33,7 +33,7 @@ import { describe, it, expect, vi } from 'vitest'; import { render, waitFor } from '@testing-library/react'; import React from 'react'; import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react'; -import type { DataSource, DeclaredNode } from '@object-ui/types'; +import type { DataSource, DeclaredNode, ObjectMapBlockNode } from '@object-ui/types'; import { safeValidateSchema } from '@object-ui/types/zod'; type StandInProps = { children?: React.ReactNode; latitude?: number; longitude?: number }; @@ -56,7 +56,10 @@ vi.mock('react-map-gl/maplibre', () => ({ import './index'; const MAP = { latitudeField: 'lat', longitudeField: 'lng', titleField: 'name' }; -const FILTER = [{ field: 'region', operator: 'equals', value: 'west' }]; +// `as const` keeps `operator` the literal the bag's filter row declares; a +// widened `string` is refused there (objectui#8347 took away the index +// signature that used to admit the bag on the post-hoist node type). +const FILTER = [{ field: 'region', operator: 'equals' as const, value: 'west' }]; const SORT: Array<{ field: string; order: 'asc' | 'desc' }> = [{ field: 'name', order: 'desc' }]; const ROWS = [ { id: 's1', name: 'North Store', lat: 37.8044, lng: -122.2711, region: 'west' }, @@ -64,7 +67,7 @@ const ROWS = [ ]; /** The authored spelling, spec-valid. */ -const BAG: DeclaredNode = { type: 'object-map', properties: { objectName: 'store', map: MAP, filter: FILTER, sort: SORT } }; +const BAG: ObjectMapBlockNode = { type: 'object-map', properties: { objectName: 'store', map: MAP, filter: FILTER, sort: SORT } }; /** The same props written flat — what a code composer builds. */ const FLAT: DeclaredNode = { type: 'object-map', objectName: 'store', map: MAP, filter: FILTER, sort: SORT }; /** The bag with its object supplied by the node's binding instead. */ diff --git a/packages/types/src/__tests__/action-callback-retired-7068.test.ts b/packages/types/src/__tests__/action-callback-retired-7068.test.ts index 0ae8f38df2..7adce86ee9 100644 --- a/packages/types/src/__tests__/action-callback-retired-7068.test.ts +++ b/packages/types/src/__tests__/action-callback-retired-7068.test.ts @@ -200,7 +200,7 @@ describe('the retirement narrows exactly `onSuccess` / `onFailure` (objectui#706 const result = ActionSchema.safeParse({ ...LEGACY_ACTION, notAKeyAtAll: 'anything' }); expect(result.success).toBe(true); if (!result.success) return; - expect((result.data as Record).notAKeyAtAll).toBe('anything'); + expect((result.data as unknown as Record).notAKeyAtAll).toBe('anything'); }); }); diff --git a/packages/types/src/__tests__/calendar-date-alias-refusal-8355.test.ts b/packages/types/src/__tests__/calendar-date-alias-refusal-8355.test.ts index 0964967fd8..b1fc28efcd 100644 --- a/packages/types/src/__tests__/calendar-date-alias-refusal-8355.test.ts +++ b/packages/types/src/__tests__/calendar-date-alias-refusal-8355.test.ts @@ -163,7 +163,7 @@ describe('objectui#8355 · the compile-time fixtures above are real program inpu expect(canonicalBlockCompiles.calendar?.startDateField).toBe('kickoff'); expect(canonicalNodeCompiles.endDateField).toBe('wrapup'); expect((viewBlockRefusesBothByTsc.calendar as Record).dateField).toBe('kickoff'); - expect((nodeRefusesBothByTsc as Record).endField).toBe('wrapup'); + expect((nodeRefusesBothByTsc as unknown as Record).endField).toBe('wrapup'); }); }); From e954befc09557dcdee2fbc303cbe2c4a3f82e5ef Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 08:02:51 +0000 Subject: [PATCH 03/14] feat(types): `BaseSchema.visibleWhen` is the spec's evaluated-slot input, by reference, on both faces The TypeScript face is `@objectstack/spec`'s `EvaluatedExpressionInput`; the zod twin is the same accept set (a predicate string, or the spec's own `EvaluatedExpressionSchema` envelope) with `EvaluatedExpressionInputSchema`'s verdict and without its string transform. Both read `string` before, so the `{ dialect, source }` envelope a spec parse writes into this key was refused (objectui#8347, ruled Q6 = B). `visible-when-spec-input-8347.test.ts` pins the type identity, the verdict parity on a probe set read against the spec in the same run, and that a string parses to itself. Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude --- content/docs/api/schema-reference.md | 2 +- .../__tests__/imported-defaults-8317.test.ts | 8 +- .../visible-when-spec-input-8347.test.ts | 138 ++++++++++++++++++ .../widget-schema-anchors-6576.test.ts | 4 +- .../src/__tests__/zod-mirror-parity.test.ts | 5 + packages/types/src/base.ts | 16 +- packages/types/src/zod/base.zod.ts | 32 +++- 7 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 packages/types/src/__tests__/visible-when-spec-input-8347.test.ts diff --git a/content/docs/api/schema-reference.md b/content/docs/api/schema-reference.md index 062962a7f3..83e46c7898 100644 --- a/content/docs/api/schema-reference.md +++ b/content/docs/api/schema-reference.md @@ -96,7 +96,7 @@ One row per declared member, in declaration order, so the list can be checked ag | `body` | *retired* | ⛔ Refused by name (objectui#6771). `body` was a second child-list spelling `BaseSchema` declared beside `children`; it is now `never` on the TypeScript face and an alias refusal on the Zod mirror, and the refusal names `children`. | | `children` | `SchemaNode \| SchemaNode[]` | Child components rendered inside this component — the child-list key, and since objectui#6771 the only one. Whether a given node type renders a child list at all is still per component; see the note below. | | `visible` | `boolean \| string \| { dialect?: string; source: string }` | Visibility control. Accepts a boolean, a predicate expression string, **or** the CEL envelope object (`{ dialect: 'cel', source }` — what `objectstack build` emits for every authored predicate) — the renderer evaluates this key rather than reading it as a boolean. The string-or-envelope half is `ExpressionWire`, the one wire type `visibleWhen` on form fields already carries. | -| `visibleWhen` | `string` | Canonical conditional-visibility predicate (ADR-0089); the element is shown when it evaluates truthy. Evaluated **before** `visible` and `visibleOn`, and outranks both. | +| `visibleWhen` | `string \| { dialect, source }` | Canonical conditional-visibility predicate (ADR-0089); the element is shown when it evaluates truthy. Typed as `@objectstack/spec`'s `EvaluatedExpressionInput`: a predicate string, or the envelope the spec's parse writes (`dialect` is `cel`, `cron` or `template`, and `source` is not blank). Evaluated **before** `visible` and `visibleOn`, and outranks both. | | `visibleOn` | `string` | Expression for conditional visibility. **Deprecated** (ADR-0089) — use `visibleWhen`. | | `hidden` | `boolean \| string \| { dialect?: string; source: string }` | Inverse of `visible` — the node is not rendered. Accepts a boolean, a predicate expression string **or** the CEL envelope object (`ExpressionWire`), which the renderer evaluates rather than reading as a boolean; `hiddenOn` remains the sibling spelling. | | `hiddenOn` | `string` | Expression for conditional hiding. | diff --git a/packages/types/src/__tests__/imported-defaults-8317.test.ts b/packages/types/src/__tests__/imported-defaults-8317.test.ts index fe02078f24..9478300384 100644 --- a/packages/types/src/__tests__/imported-defaults-8317.test.ts +++ b/packages/types/src/__tests__/imported-defaults-8317.test.ts @@ -138,7 +138,10 @@ import { checkDashboardWidgetMetricMeasureArity, } from '@objectstack/spec/ui'; import { FieldSchema as SpecFieldSchema, SelectOptionSchema as SpecSelectOptionSchema } from '@objectstack/spec/data'; -import { EvaluatedExpressionInputSchema as SpecEvaluatedExpressionInputSchema } from '@objectstack/spec/shared'; +import { + EvaluatedExpressionInputSchema as SpecEvaluatedExpressionInputSchema, + EvaluatedExpressionSchema as SpecEvaluatedExpressionSchema, +} from '@objectstack/spec/shared'; import { stripImportedDefaults } from '../zod/imported-defaults.js'; const HERE = dirname(fileURLToPath(import.meta.url)); @@ -248,6 +251,9 @@ const IMPORTED: Array = [ // TEXT to this schema and takes its verdict and sentence (ADR-0137 D1), so // the crossing is measured here like every other one. ['EvaluatedExpressionInputSchema', SpecEvaluatedExpressionInputSchema], + // objectui#8347: `BaseSchema.visibleWhen`'s envelope arm is this schema, by + // reference, and its verdict is `EvaluatedExpressionInputSchema`'s above. + ['EvaluatedExpressionSchema', SpecEvaluatedExpressionSchema], // objectui#7265, the @object-ui/types slice: `UserFiltersSchema.fields[]` // stopped being a hand copy of the spec's field shape and now derives from // it, so that crossing is measured here like every other one. diff --git a/packages/types/src/__tests__/visible-when-spec-input-8347.test.ts b/packages/types/src/__tests__/visible-when-spec-input-8347.test.ts new file mode 100644 index 0000000000..f5a5d10be2 --- /dev/null +++ b/packages/types/src/__tests__/visible-when-spec-input-8347.test.ts @@ -0,0 +1,138 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * `BaseSchema.visibleWhen` is the spec's evaluated-slot INPUT, by reference, on + * both faces (objectui#8347, ruled Q6 = B). + * + * `@objectstack/spec` types every evaluated `visibleWhen` it declares, + * `PageComponentSchema`'s included, as `EvaluatedExpressionInputSchema`: a + * predicate string, or a `{ dialect, source }` envelope whose dialect is + * `cel` / `cron` / `template` and whose `source` is not blank. Its parse + * rewrites a string into the envelope, so a spec-parsed component carries the + * envelope here, and `SchemaRenderer` evaluates it. Both faces of this key read + * `string` before, so the envelope a spec parse produces was refused on the zod + * face and was not assignable on the TypeScript face. + * + * ## What is pinned + * + * - TYPE level (judged by `tsc -p tsconfig.test.json`): the TypeScript face + * IS the spec's `EvaluatedExpressionInput`, with nothing restated, and both + * the string and the envelope are authorable on a node literal. + * - RUNTIME: the zod twin returns the spec's verdict on every probe value, + * AND it does not take the spec's transform — a string parses to the same + * string. The probe set's controls are the spec schema itself, read on the + * same values in the same run. + * + * ⚠️ The envelope here is NARROWER than `ExpressionWire`, the wire `visible` / + * `hidden` / `disabled` carry (objectui#7530): a dialect-less envelope and an + * unknown dialect are refused on this key, because the spec refuses them. + */ + +import { describe, it, expect } from 'vitest'; +import { + EvaluatedExpressionInputSchema as SpecEvaluatedExpressionInputSchema, + type EvaluatedExpressionInput, +} from '@objectstack/spec/shared'; +import { PageComponentSchema as SpecPageComponentSchema } from '@objectstack/spec/ui'; +import type { BaseSchema } from '../base'; +import { BaseSchema as Mirror } from '../zod/base.zod'; +import { AnyComponentSchema, StrictAnyComponentSchema } from '../zod/index.zod.js'; + +/* ── Type level ───────────────────────────────────────────────────────────── */ + +type Equal = + (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; +type Expect = T; + +// By reference: the member IS the spec's input type. A local restatement that +// drifted from the spec (a dialect added, `source` made optional) turns this red. +export type _VisibleWhenIsTheSpecInput = Expect, EvaluatedExpressionInput>>; + +export const stringPredicateIsAuthorable: BaseSchema = { type: 'test-component', visibleWhen: "record.status == 'open'" }; +export const envelopeIsAuthorable: BaseSchema = { + type: 'test-component', + visibleWhen: { dialect: 'cel', source: "record.status == 'open'" }, +}; +export const dialectlessEnvelopeIsRefused: BaseSchema = { + type: 'test-component', + // @ts-expect-error the spec's envelope requires `dialect`; `ExpressionWire`'s optional one does not reach this key + visibleWhen: { source: "record.status == 'open'" }, +}; + +/* ── Runtime ──────────────────────────────────────────────────────────────── */ + +const ENVELOPE = { dialect: 'cel', source: "record.status == 'open'" }; + +const PROBES: Array<{ label: string; value: unknown }> = [ + { label: 'a predicate string', value: "record.status == 'open'" }, + { label: 'a template string', value: "${data.role === 'admin'}" }, + { label: 'a cel envelope', value: ENVELOPE }, + { label: 'a template envelope', value: { dialect: 'template', source: '${data.ready}' } }, + { label: 'an envelope with meta', value: { dialect: 'cel', source: 'true', meta: { rationale: 'why' } } }, + { label: 'a blank string', value: ' ' }, + { label: 'an empty string', value: '' }, + { label: 'a dialect-less envelope', value: { source: "record.status == 'open'" } }, + { label: 'an unknown dialect', value: { dialect: 'sql', source: 'x' } }, + { label: 'an envelope with a blank source', value: { dialect: 'cel', source: ' ' } }, + { label: 'an ast-only envelope', value: { dialect: 'cel', ast: {} } }, + { label: 'a number', value: 42 }, + { label: 'a boolean', value: true }, +]; + +const mirrorVerdict = (value: unknown) => Mirror.safeParse({ type: 'test-component', visibleWhen: value }); + +describe('BaseSchema.visibleWhen is the spec\'s evaluated-slot input on the zod face (objectui#8347)', () => { + it('the probe set reads both verdicts on the spec schema itself (lit control)', () => { + const verdicts = PROBES.map(({ value }) => SpecEvaluatedExpressionInputSchema.safeParse(value).success); + expect(verdicts).toContain(true); + expect(verdicts).toContain(false); + }); + + it.each(PROBES)('$label: the mirror answers what the spec answers', ({ value }) => { + expect(mirrorVerdict(value).success).toBe(SpecEvaluatedExpressionInputSchema.safeParse(value).success); + }); + + it('…and what `PageComponentSchema.visibleWhen` answers, the key this one mirrors', () => { + for (const { value } of PROBES) { + const spec = SpecPageComponentSchema.safeParse({ type: 'element:text', visibleWhen: value }); + const specIssueAtKey = spec.success ? false : spec.error.issues.some((i) => i.path[0] === 'visibleWhen'); + expect(mirrorVerdict(value).success, JSON.stringify(value)).toBe(!specIssueAtKey); + } + }); + + it('a blank predicate string is refused with the spec\'s own sentence', () => { + const mine = mirrorVerdict(' '); + const spec = SpecEvaluatedExpressionInputSchema.safeParse(' '); + expect(mine.success).toBe(false); + expect(spec.success).toBe(false); + if (mine.success || spec.success) return; + expect(mine.error.issues.map((i) => i.message)).toEqual(spec.error.issues.map((i) => i.message)); + }); + + it('does NOT take the spec\'s transform: a string parses to the same string', () => { + const predicate = "record.status == 'open'"; + // The spec rewrites it — the control that the transform exists. + expect(SpecEvaluatedExpressionInputSchema.parse(predicate)).toEqual({ dialect: 'cel', source: predicate }); + const mine = mirrorVerdict(predicate); + expect(mine.success).toBe(true); + if (!mine.success) return; + expect(mine.data.visibleWhen).toBe(predicate); + }); + + it('an envelope parses to the same envelope', () => { + const mine = mirrorVerdict(ENVELOPE); + expect(mine.success).toBe(true); + if (!mine.success) return; + expect(mine.data.visibleWhen).toEqual(ENVELOPE); + }); + + it('the envelope a spec parse writes is accepted by both component faces', () => { + const parsed = SpecEvaluatedExpressionInputSchema.parse("record.status == 'open'"); + const node = { type: 'text', content: 'Open', visibleWhen: parsed }; + expect(AnyComponentSchema.safeParse(node).success).toBe(true); + expect(StrictAnyComponentSchema.safeParse(node).success).toBe(true); + // Control: the same node with a dialect-less envelope is refused at the key. + const refused = StrictAnyComponentSchema.safeParse({ ...node, visibleWhen: { source: 'x' } }); + expect(refused.success).toBe(false); + }); +}); diff --git a/packages/types/src/__tests__/widget-schema-anchors-6576.test.ts b/packages/types/src/__tests__/widget-schema-anchors-6576.test.ts index 2d48b72a5d..7ea8dce908 100644 --- a/packages/types/src/__tests__/widget-schema-anchors-6576.test.ts +++ b/packages/types/src/__tests__/widget-schema-anchors-6576.test.ts @@ -72,6 +72,7 @@ import { ObjectDataTableSchema as ObjectDataTableMirror, } from '../zod/objectql.zod.js'; import type { ExpressionWire } from '../expression'; +import type { EvaluatedExpressionInput } from '@objectstack/spec/shared'; const HERE = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = join(HERE, '..', '..', '..', '..'); @@ -110,7 +111,8 @@ export type assertionOnRowClickDeclared = Expect>; +// `string` until objectui#8347 typed it as the spec's evaluated-slot input, by reference. +export type assertionGalleryInheritsVisibleWhen = Expect>; export type assertionGalleryInheritsBind = Expect>; // `boolean | string` until objectui#7530 declared the CEL envelope on the base union. export type assertionDataTableInheritsVisible = Expect>; diff --git a/packages/types/src/__tests__/zod-mirror-parity.test.ts b/packages/types/src/__tests__/zod-mirror-parity.test.ts index 94ac86df28..d260538e00 100644 --- a/packages/types/src/__tests__/zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts @@ -5075,6 +5075,11 @@ const SPEC_DERIVED_PAIRS: readonly string[] = [ // to the split's LOCAL half (its `UnmirroredDeclared` docblock carries the // per-key reading). The split's counts now derive from THIS list's membership // and are pinned against the header by 'objectui#7279' below. + // objectui#8347: `BaseSchema` is back, for a real reference this time — + // `visibleWhen` is the spec's `EvaluatedExpressionSchema` (the envelope arm) + // with `EvaluatedExpressionInputSchema`'s verdict, so a spec bump that moves + // the evaluated-slot input moves ONE side of this pair. + 'base.zod.ts#BaseSchema', 'complex.zod.ts#DashboardComponentSchema', 'complex.zod.ts#DashboardWidgetSchema', // ⛔ `complex.zod.ts#KanbanSchema` removed with the retired `kanban` arm diff --git a/packages/types/src/base.ts b/packages/types/src/base.ts index c0d9ad447f..6694f73171 100644 --- a/packages/types/src/base.ts +++ b/packages/types/src/base.ts @@ -17,6 +17,7 @@ */ import type { I18nLabel } from '@objectstack/spec/ui'; +import type { EvaluatedExpressionInput } from '@objectstack/spec/shared'; import type { AuthoringNode } from './authoring-nodes.js'; import type { ExpressionWire } from './expression.js'; import type { AnySchema } from './index.js'; @@ -386,9 +387,22 @@ export interface BaseSchema { * Canonical conditional-visibility predicate (ADR-0089) — the element is shown * when this evaluates truthy. The spec folds the deprecated `visibleOn` / * `visibility` aliases into this key at parse. + * + * Typed BY REFERENCE as the spec's own input type for an evaluated slot, + * `EvaluatedExpressionInput` (objectui#8347, ruled Q6 = B): a predicate + * string, or the `{ dialect, source }` envelope the spec's parse of + * `PageComponentSchema.visibleWhen` produces and `SchemaRenderer` evaluates. + * It read `string` before, which refused the envelope the spec's own parse + * writes into this key. ⚠️ Narrower than + * `ExpressionWire` (`visible` / `hidden` / `disabled`, objectui#7530): the + * spec refuses a dialect-less envelope and an unknown dialect, and so does + * this key. Its zod twin is the same accept set without the spec's string + * transform (`../zod/base.zod.ts`). + * * @example "${data.role === 'admin'}" + * @example { dialect: 'cel', source: "record.status == 'open'" } */ - visibleWhen?: string; + visibleWhen?: EvaluatedExpressionInput; /** * Expression for conditional visibility. diff --git a/packages/types/src/zod/base.zod.ts b/packages/types/src/zod/base.zod.ts index 899648142d..5c5e3caf7d 100644 --- a/packages/types/src/zod/base.zod.ts +++ b/packages/types/src/zod/base.zod.ts @@ -20,6 +20,10 @@ import { z } from 'zod'; import { I18nLabelSchema } from '@objectstack/spec/ui'; import { aliasKeyRefusal, retirementTombstone } from './tombstone.zod.js'; import { ExpressionWireSchema } from './expression.zod.js'; +import { + EvaluatedExpressionInputSchema as SpecEvaluatedExpressionInputSchema, + EvaluatedExpressionSchema as SpecEvaluatedExpressionSchema, +} from '@objectstack/spec/shared'; import type { SchemaNode } from '../base.js'; import { stripImportedDefaults } from './imported-defaults.js'; @@ -328,6 +332,30 @@ const NodeSlotSchema: NodeSlotZodType = z .optional() .describe('Child components (React-style)'); +/** + * `visibleWhen`'s wire: the spec's `EvaluatedExpressionInputSchema`, by + * reference, WITHOUT its string transform (objectui#8347, ruled Q6 = B). + * + * The TypeScript twin is the spec's input type, `EvaluatedExpressionInput` + * (`../base.ts`). The accept set here is that type's: a predicate string, or + * the envelope arm, which is the spec's own `EvaluatedExpressionSchema` read by + * reference. Every verdict is the spec's: the value is handed to + * `EvaluatedExpressionInputSchema` and its issues are the refusal, so a blank + * predicate string is refused with the spec's sentence. What is NOT taken is + * the spec's transform, which rewrites a bare string into + * `{ dialect: 'cel', source }` at parse: this mirror authors nothing into a + * parsed document, so a string stays a string. + * `../__tests__/visible-when-spec-input-8347.test.ts` pins both halves. + */ +const VisibleWhenInputSchema = z + .union([z.string(), stripImportedDefaults(SpecEvaluatedExpressionSchema)]) + .superRefine((predicate, ctx) => { + const verdict = stripImportedDefaults(SpecEvaluatedExpressionInputSchema).safeParse(predicate); + if (!verdict.success) { + for (const issue of verdict.error.issues) ctx.addIssue({ code: 'custom', message: issue.message }); + } + }); + /** * Base Schema - Core validation schema that all components extend * @@ -448,8 +476,10 @@ const BaseSchemaCore = z.object({ /** * Canonical conditional-visibility predicate (ADR-0089) — shown when truthy. * The spec folds the deprecated `visibleOn` / `visibility` aliases into this. + * The spec's evaluated-slot input, by reference and untransformed — see + * `VisibleWhenInputSchema` above (objectui#8347). */ - visibleWhen: z.string().optional().describe('Canonical conditional-visibility predicate (ADR-0089)'), + visibleWhen: VisibleWhenInputSchema.optional().describe('Canonical conditional-visibility predicate (ADR-0089)'), /** * Conditional visibility expression From b66492b8a54feabec6ff540b5e5e0fb2c93cff01 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 09:15:43 +0000 Subject: [PATCH 04/14] feat(types)!: `BaseSchema` loses its index signature; the compile-fail pin, both directions, and the adjudicated flips `BaseSchema` declares no `[key: string]: any`, so a node literal annotated with its node type refuses a key no declaration names (objectui#8347, executing the objectui#7927 ruling). `ComponentRendererProps` keeps its own signature. - `base-schema-closed-face-8347.test.ts`: #7927's P5 turned red on a fresh literal, the correct spelling compiling, a nested node, the non-fresh bound, and `PartialSchema`'s working reading. - The adjudicated probe and ceiling rows flip: each "an unknown key still compiles" row becomes an `@ts-expect-error`, and each `IsAny` fall-through control becomes a membership reading with a lit `IsAny` control. - The `props` alias rows flip (option A, the maintainer's open choice). - The valueless `metric-card` in the legacy envelope is pinned refused on the TS face (Q3 = A); `envelopeStray` and its `complex.ts` note go, per the constant's own removal condition. - `object-view-slot-key-lists` tripwires flip; `partial-schema-collapse-pin` is deleted per its header and `PartialSchema`'s docblock describes the alias as it now works. - The mirror-parity ledger records five interface-to-passthrough entries the removal surfaces, measured as an artifact of the assignability test. - Prose in these files that described the signature as live is corrected. Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude --- .../alias-precedence-cross-channel.test.tsx | 8 +- .../src/__tests__/form-renderers.test.tsx | 1 + .../ObjectChart.schemaAnchor-7946.test.ts | 11 +- .../ObjectChartBlock.props-8885.test.tsx | 9 +- .../ObjectDataTable.schemaAnchor-6576.test.ts | 6 +- .../widgetDataProviderRetired-7353.test.tsx | 12 +- .../dataTableSchemaSlot-6459.test.ts | 48 ++- .../ObjectGallery.schemaAnchor-6576.test.ts | 6 +- .../src/ObjectMap.schemaAlignment.test.tsx | 18 +- .../src/ObjectTree.schemaTyped-8655.test.ts | 16 +- .../action-callback-retired-7068.test.ts | 7 +- .../ai-zero-read-members-retired-8178.test.ts | 14 +- ...nyschema-declared-node-types-11478.test.ts | 2 +- .../app-actions-retired-7469.test.ts | 8 +- .../base-schema-closed-face-8347.test.ts | 120 ++++++ .../calendar-flat-color-allday-8466.test.ts | 44 +- .../chatbot-dark-keys-retired-7703.test.ts | 33 +- .../chatbot-display-mode-retired.test.ts | 13 +- .../checkbox-wrapper-class-6938.test.ts | 22 +- .../content-channel-e3-residual-9256.test.ts | 8 +- ...ashboard-title-retired-declaration.test.ts | 13 +- ...ard-widget-slot-component-arm-7952.test.ts | 39 +- .../flat-arm-responsive-styles-10872.test.ts | 15 +- .../flex-bag-children-list-11564.test.ts | 20 +- .../input-wrapper-class-mirrored-8072.test.ts | 21 +- .../kanban-calendar-filter-sort-8174.test.ts | 41 +- .../mirror-only-published-keys-9729.test.ts | 20 +- .../__tests__/node-slot-union-11466.test.ts | 7 +- ...art-legacy-axis-keys-retired-10608.test.ts | 8 +- .../object-chart-undeclared-keys-8885.test.ts | 10 +- ...ta-table-dataprovider-retired-7353.test.ts | 13 +- .../object-gallery-filter-9309.test.ts | 15 +- ...ject-grid-operators-tombstone-9739.test.ts | 30 +- ...kanban-allow-collapse-retired-8801.test.ts | 9 +- .../object-kanban-group-by-limit-7322.test.ts | 11 +- ...-kanban-on-quick-add-retired-11234.test.ts | 7 +- ...ject-kanban-quick-add-retired-8285.test.ts | 7 +- .../__tests__/object-tree-filter-9549.test.ts | 14 +- .../object-view-slot-key-lists.test.ts | 27 +- .../object-view-unmirrored-keys-7779.test.ts | 9 +- .../partial-schema-collapse-pin.test.ts | 377 ------------------ .../twins-spec-by-reference-9736.test.ts | 7 +- .../widget-schema-anchors-6576.test.ts | 13 +- .../widget-schema-anchors-7946.test.ts | 7 +- .../wrapper-class-declared-7722.test.ts | 28 +- .../src/__tests__/zod-mirror-parity.test.ts | 41 +- packages/types/src/authoring-nodes.ts | 18 +- packages/types/src/base.ts | 30 +- packages/types/src/complex.ts | 25 +- packages/types/src/index.ts | 65 +-- packages/types/src/zod/base.zod.ts | 5 +- 51 files changed, 634 insertions(+), 724 deletions(-) create mode 100644 packages/types/src/__tests__/base-schema-closed-face-8347.test.ts delete mode 100644 packages/types/src/__tests__/partial-schema-collapse-pin.test.ts diff --git a/packages/components/src/__tests__/alias-precedence-cross-channel.test.tsx b/packages/components/src/__tests__/alias-precedence-cross-channel.test.tsx index 6a35c361e1..63a2b925b2 100644 --- a/packages/components/src/__tests__/alias-precedence-cross-channel.test.tsx +++ b/packages/components/src/__tests__/alias-precedence-cross-channel.test.tsx @@ -81,6 +81,7 @@ describe('alias precedence across the two renderer families (objectui#5123)', () schema={{ type: 'badge', label: 'b', + // @ts-expect-error — `props` is not on the TS authoring face (objectui#8347, the `props` alias, option A); the runtime alias read is what this test measures props: { title: 'FROM_PROPS' }, properties: { title: 'FROM_PROPERTIES' }, }} @@ -138,7 +139,12 @@ describe('alias precedence across the two renderer families (objectui#5123)', () const badge = render( ); expect( diff --git a/packages/components/src/__tests__/form-renderers.test.tsx b/packages/components/src/__tests__/form-renderers.test.tsx index 5371a8e0c1..696d7b2d72 100644 --- a/packages/components/src/__tests__/form-renderers.test.tsx +++ b/packages/components/src/__tests__/form-renderers.test.tsx @@ -397,6 +397,7 @@ describe('Form Renderers - Display Issue Detection', () => { // `@object-ui/types` retire it, so a node carrying only `content` renders no text. const { container } = renderComponent({ type: 'label', + // @ts-expect-error — retired on `label` (objectui#6152), and `BaseSchema` declares no `content` and no index signature (objectui#8347) content: 'Retired Spelling', }); diff --git a/packages/plugin-charts/src/__tests__/ObjectChart.schemaAnchor-7946.test.ts b/packages/plugin-charts/src/__tests__/ObjectChart.schemaAnchor-7946.test.ts index 66c1eedf33..8fd3b4e60f 100644 --- a/packages/plugin-charts/src/__tests__/ObjectChart.schemaAnchor-7946.test.ts +++ b/packages/plugin-charts/src/__tests__/ObjectChart.schemaAnchor-7946.test.ts @@ -210,11 +210,12 @@ describe('ObjectChartProps.schema — anchored to ObjectChartSchema (objectui#79 expect(node.visibleWhen).toBe('${data.ready}'); }); - it('the ceiling, stated: an UNKNOWN key still compiles (inherited index signature, objectui#5155)', () => { - // Counter-probe against reading the anchor as more than it is. The ruling's - // acceptance is a wrong VALUE TYPE being refused loudly; a misspelled KEY - // is not reachable from here, exactly as on `ObjectGallerySchema`. + it('an UNKNOWN key is refused — the ceiling this row stated is gone (objectui#8347)', () => { + // This row was the counter-probe against reading the anchor as more than it + // was: the inherited index signature let a misspelled KEY compile. objectui#8347 + // removed it, so the misspelling is refused like a wrong value type. + // @ts-expect-error — `xAxisKy` is no member; the key is `xAxisKey` const node: ObjectChartProps['schema'] = { type: 'object-chart', chartType: 'bar', xAxisKy: 'stage' }; - expect(node.xAxisKy).toBe('stage'); + expect(node.type).toBe('object-chart'); }); }); diff --git a/packages/plugin-charts/src/__tests__/ObjectChartBlock.props-8885.test.tsx b/packages/plugin-charts/src/__tests__/ObjectChartBlock.props-8885.test.tsx index bc461b1fd3..a06ad6069f 100644 --- a/packages/plugin-charts/src/__tests__/ObjectChartBlock.props-8885.test.tsx +++ b/packages/plugin-charts/src/__tests__/ObjectChartBlock.props-8885.test.tsx @@ -112,8 +112,13 @@ const probes = () => ( onSegmentClick={() => undefined} /> - {/* CEILING, not a refusal: a misspelled key INSIDE the node still - compiles, through `BaseSchema`'s passthrough index signature. */} + {/* This row was the CEILING: a misspelled key INSIDE the node compiled + through `BaseSchema`'s index signature. objectui#8347 removed it, so a + fresh literal at the shell's pre-gate `BaseSchema` prop is excess-checked + against `BaseSchema` itself: the misspelling is refused, and so would a + correctly spelled `objectName` be. Hand the shell a typed node, as the + controls above do. */} + {/* @ts-expect-error — `objectNme` is no `BaseSchema` member, and the pre-gate prop is `BaseSchema` */} ); diff --git a/packages/plugin-dashboard/src/__tests__/ObjectDataTable.schemaAnchor-6576.test.ts b/packages/plugin-dashboard/src/__tests__/ObjectDataTable.schemaAnchor-6576.test.ts index 18bbebc1c3..43ac96e1aa 100644 --- a/packages/plugin-dashboard/src/__tests__/ObjectDataTable.schemaAnchor-6576.test.ts +++ b/packages/plugin-dashboard/src/__tests__/ObjectDataTable.schemaAnchor-6576.test.ts @@ -107,8 +107,10 @@ describe('ObjectDataTableProps.schema — anchored to ObjectDataTableSchema (obj expect(node.data).toBe('not-an-array'); }); - it('the ceiling, stated: an UNKNOWN key still compiles (inherited index signature, objectui#5155)', () => { + it('an UNKNOWN key is refused — the ceiling this row stated is gone (objectui#8347)', () => { + // The inherited index signature let it compile until objectui#8347 removed it. + // @ts-expect-error — `ObjectDataTableSchema` declares no `bogusKey` const node: Schema = { type: 'object-data-table', bogusKey: 1 }; - expect(node.bogusKey).toBe(1); + expect(node.type).toBe('object-data-table'); }); }); diff --git a/packages/plugin-dashboard/src/__tests__/widgetDataProviderRetired-7353.test.tsx b/packages/plugin-dashboard/src/__tests__/widgetDataProviderRetired-7353.test.tsx index 95efacbf01..2364860a1a 100644 --- a/packages/plugin-dashboard/src/__tests__/widgetDataProviderRetired-7353.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/widgetDataProviderRetired-7353.test.tsx @@ -237,10 +237,13 @@ describe('objectui#7353 — both widget prop types refuse an authored dataProvid // @ts-expect-error `dataProvider` is a retirement tombstone (objectui#7353) — write `objectName` dataProvider: { provider: 'object', object: 'account' }, }; - // Control: an undeclared key carries no directive — the index signature - // absorbs it, which is what a deletion would have left `dataProvider` as. + // Control: an undeclared key carried no directive while the index signature + // absorbed it — what a deletion would have left `dataProvider` as. Since + // objectui#8347 it is refused too, as undeclared, with no remedy named; the + // tombstone above is what names `objectName`. + // @ts-expect-error — the pivot node declares no `zzUndeclared7353` (objectui#8347) const undeclared: PivotSchema = { type: 'pivot', rowField: 'r', columnField: 'c', valueField: 'v', data: [], zzUndeclared7353: 1 }; - expect([node.objectName, undeclared.zzUndeclared7353]).toEqual(['account', 1]); + expect([node.objectName, undeclared.type]).toEqual(['account', 'pivot']); }); it('ObjectDataTableProps (anchored to ObjectDataTableSchema): the same, through the built types', () => { @@ -250,7 +253,8 @@ describe('objectui#7353 — both widget prop types refuse an authored dataProvid // @ts-expect-error `dataProvider` is a retirement tombstone (objectui#7353) — write `objectName` dataProvider: { provider: 'object', object: 'account' }, }; + // @ts-expect-error — `ObjectDataTableSchema` declares no `zzUndeclared7353` (objectui#8347) const undeclared: DataTableSchema = { type: 'object-data-table', zzUndeclared7353: 1 }; - expect([node.objectName, undeclared.zzUndeclared7353]).toEqual(['account', 1]); + expect([node.objectName, undeclared.type]).toEqual(['account', 'object-data-table']); }); }); diff --git a/packages/plugin-grid/src/__tests__/dataTableSchemaSlot-6459.test.ts b/packages/plugin-grid/src/__tests__/dataTableSchemaSlot-6459.test.ts index 7b90730e8e..f8a76927c5 100644 --- a/packages/plugin-grid/src/__tests__/dataTableSchemaSlot-6459.test.ts +++ b/packages/plugin-grid/src/__tests__/dataTableSchemaSlot-6459.test.ts @@ -84,34 +84,37 @@ const columns: TableColumn[] = [{ header: 'Name', accessorKey: 'name' }]; describe('objectui#6459 — the schema slot annotation is an instrument, not a decoration', () => { /** - * ⭐ THE ROOT CAUSE, PINNED: a bare `DataTableSchema` annotation refuses - * NOTHING. This assignment MUST COMPILE — the bogus key is written out - * longhand in a FRESH literal, the strongest position excess-property - * checking ever has, and the inherited `[key: string]: any` still admits it. - * Measured on the real seam before the fix was shaped (`tsc --noEmit` - * exit 0, zero diagnostics, with `bogusKeyForProbe6459: true` injected into - * the flat literal under a bare annotation). If this ever goes red, - * TypeScript or `BaseSchema` changed underneath and the docblock above - * `RemoveIndexSignature` in `ObjectGrid.tsx` needs re-measuring. + * ⭐ THE ROOT CAUSE, as it was: a bare `DataTableSchema` annotation refused + * NOTHING. The assignment below COMPILED — the bogus key written out longhand + * in a FRESH literal, the strongest position excess-property checking ever + * has, and the inherited `[key: string]: any` still admitted it. Measured on + * the real seam before the fix was shaped (`tsc --noEmit` exit 0, zero + * diagnostics). This docblock said that if it ever went red, `BaseSchema` + * had changed underneath and `RemoveIndexSignature` in `ObjectGrid.tsx` + * needed re-measuring: objectui#8347 removed the signature, the row went + * red, and it is flipped here. The local strip is redundant from that day + * (it strips nothing now); retiring it is left out of the removal PR. */ - it('a bare `DataTableSchema` annotation accepts a bogus key even at a fresh literal (the blind instrument)', () => { + it('a bare `DataTableSchema` annotation refuses a bogus key at a fresh literal since objectui#8347', () => { const blind: DataTableSchema = { type: 'data-table', columns, data: [], - bogusKeyRefusedNowhere: 'admitted by the index signature', + // @ts-expect-error — `DataTableSchema` declares no such key, and `BaseSchema` carries no index signature (objectui#8347) + bogusKeyRefusedNowhere: 'admitted by the index signature until objectui#8347', }; expect(blind.type).toBe('data-table'); }); /** - * …and the index signature is exactly what separates the two types: present - * on `DataTableSchema`, stripped from the seam type. If `@object-ui/types` - * ever removes the signature from `BaseSchema`, the first line goes red — - * a useful red: the local stripping machinery becomes redundant that day. + * …and the index signature was exactly what separated the two types: present + * on `DataTableSchema`, stripped from the seam type. This docblock predicted + * that the first line would go red if `@object-ui/types` removed the signature + * from `BaseSchema` — objectui#8347 did, and it is flipped: neither type is + * indexed by `string` now. */ - it('the strip is real: `string` indexes `DataTableSchema` but not the seam type', () => { - type _UpstreamHasIndex = Expect; + it('neither `DataTableSchema` nor the seam type is indexed by `string` (objectui#8347)', () => { + type _UpstreamHasNone = Expect; type _SeamHasNone = Expect; expect(true).toBe(true); }); @@ -284,11 +287,14 @@ describe('objectui#6459 — the schema slot annotation is an instrument, not a d * The first line pins that blindness; the second pins that the strip * `ObjectGrid.tsx` already derives is what restores a usable `false`. * - * If the first line ever goes red, `BaseSchema` dropped its index signature — - * a useful red, and the same one "the strip is real" above reports. + * The first line went red when objectui#8347 dropped `BaseSchema`'s index + * signature, as predicted here, and is flipped: the raw probe can refuse now, + * so both types answer alike. */ - it('the gate must read the STRIPPED type — the raw `DataTableSchema` probe is blind', () => { - type _RawIsAlwaysTrue = Expect>; + it('the raw `DataTableSchema` probe can refuse since objectui#8347, as the stripped one always could', () => { + type _RawCanRefuse = Expect< + Has<'zzNotADataTableSchemaKeyZZ', DataTableSchema> extends false ? true : false + >; type _StrippedCanRefuse = Expect< Has<'zzNotADataTableSchemaKeyZZ', DeclaredDataTableSchema> extends false ? true : false >; diff --git a/packages/plugin-list/src/__tests__/ObjectGallery.schemaAnchor-6576.test.ts b/packages/plugin-list/src/__tests__/ObjectGallery.schemaAnchor-6576.test.ts index d2eedb1aaa..0f06c2f3d9 100644 --- a/packages/plugin-list/src/__tests__/ObjectGallery.schemaAnchor-6576.test.ts +++ b/packages/plugin-list/src/__tests__/ObjectGallery.schemaAnchor-6576.test.ts @@ -85,8 +85,10 @@ describe('ObjectGalleryProps.schema — anchored to ObjectGallerySchema (objectu expect(node.data).toBe('not-an-array'); }); - it('the ceiling, stated: an UNKNOWN key still compiles (inherited index signature, objectui#5155)', () => { + it('an UNKNOWN key is refused — the ceiling this row stated is gone (objectui#8347)', () => { + // The inherited index signature let it compile until objectui#8347 removed it. + // @ts-expect-error — `visibleWhn` is no member; the key is `visibleWhen` const node: ObjectGalleryProps['schema'] = { type: 'object-gallery', visibleWhn: 'typo' }; - expect(node.visibleWhn).toBe('typo'); + expect(node.type).toBe('object-gallery'); }); }); diff --git a/packages/plugin-map/src/ObjectMap.schemaAlignment.test.tsx b/packages/plugin-map/src/ObjectMap.schemaAlignment.test.tsx index d97590273e..ea66b3bf73 100644 --- a/packages/plugin-map/src/ObjectMap.schemaAlignment.test.tsx +++ b/packages/plugin-map/src/ObjectMap.schemaAlignment.test.tsx @@ -131,12 +131,12 @@ describe('ObjectMapSchema declares what the renderer reads (type pins)', () => { * The bound on (a), measured rather than assumed — the first draft of this * file pinned the opposite and `tsc` said otherwise (TS2578 on both). * - * `BaseSchema` carries `[key: string]: any` ("additional properties specific + * `BaseSchema` carried `[key: string]: any` ("additional properties specific * to the component type"), and `ObjectMapSchema extends BaseSchema`. So a - * misspelled key at the TOP level type-checks — for every component schema in - * the repo, not just this one. That index signature is a platform-wide - * decision well outside objectui#5018, and removing it here would only move - * the hole. + * misspelled key at the TOP level type-checked — for every component schema + * in the repo, not just this one. That index signature was a platform-wide + * decision well outside objectui#5018; objectui#8347 removed it, and the row + * below flipped with it: the top level is closed now too. * * What #5018 buys is the half that matters for the card's symptom: `ObjectMapConfig` * is a plain interface with no index signature, so the `map` BLOCK is closed @@ -144,12 +144,14 @@ describe('ObjectMapSchema declares what the renderer reads (type pins)', () => { * empty map. Pinning the real boundary keeps the next reader from believing * the whole surface is closed. */ - it('does NOT reject a misspelled TOP-LEVEL key — BaseSchema is open', () => { + it('rejects a misspelled TOP-LEVEL key since objectui#8347 closed `BaseSchema`', () => { const schema: ObjectMapSchema = { type: 'object-map', objectName: 'stores', - // No `@ts-expect-error`: `BaseSchema`'s index signature admits this, and - // saying otherwise is what the first draft got wrong. + // This row carried no directive while `BaseSchema`'s index signature + // admitted the key (the first draft of this file pinned the opposite too + // early); objectui#8347 removed the signature. + // @ts-expect-error — `enableClusterng` is no member; the key is `enableClustering` enableClusterng: true, }; diff --git a/packages/plugin-tree/src/ObjectTree.schemaTyped-8655.test.ts b/packages/plugin-tree/src/ObjectTree.schemaTyped-8655.test.ts index 551a208c15..f50fdc1aa3 100644 --- a/packages/plugin-tree/src/ObjectTree.schemaTyped-8655.test.ts +++ b/packages/plugin-tree/src/ObjectTree.schemaTyped-8655.test.ts @@ -293,15 +293,17 @@ export type _TreeIsDeclared = Expect>; export type _StaticDataIsDeclared = Expect>; /** - * ⚠️ THE CEILING. An undeclared key still compiles and still types `any`, so - * declaring buys VALUE typing and never buys refusal of a misspelling. Both - * halves are pinned: the control key resolves to `any`, and so does a typo — - * while a declared key (`navigation`, since objectui#11168 slice 3) no longer - * does, which is the value typing declaring bought. + * THE CEILING, as it was: an undeclared key compiled and typed `any`, so + * declaring bought VALUE typing and never refusal of a misspelling. objectui#8347 + * removed `BaseSchema`'s index signature, so the control key and the typo are + * no members now, and a declared key (`navigation`, since objectui#11168 slice + * 3) still resolves to its own type, not `any`. */ -export type _UndeclaredStillResolvesToAny = Expect>; +export type _UndeclaredIsNoMember = Expect>; export type _DeclaredNoLongerResolvesToAny = Expect, false>>; -export type _MisspellingStillAdmitted = Expect>; +export type _MisspellingIsNoMember = Expect>; +/** Lit control for the detector: `IsAny` does answer `true`, so the line above is a reading. */ +export type _IsAnyCanAnswerTrue = Expect>; /** The node still type-checks the way a host writes it, with no cast. */ const node: ObjectTreeProps['schema'] = { diff --git a/packages/types/src/__tests__/action-callback-retired-7068.test.ts b/packages/types/src/__tests__/action-callback-retired-7068.test.ts index 7adce86ee9..89c81d1fa0 100644 --- a/packages/types/src/__tests__/action-callback-retired-7068.test.ts +++ b/packages/types/src/__tests__/action-callback-retired-7068.test.ts @@ -338,8 +338,11 @@ describe('legacy ActionSchema.onSuccess / onFailure are RETIRED — the TS half redirect: '/done', }; - // BASE CONTROL on the TS face: the same literal IS a legal `BaseSchema` — the - // acceptance a deleted member would have fallen through to. + // BASE CONTROL on the TS face: the same literal WAS a legal `BaseSchema` — the + // acceptance a deleted member would have fallen through to — until + // objectui#8347 removed `BaseSchema`'s index signature. The base refuses it + // now too, so a deleted member is no longer silently acceptable here. + // @ts-expect-error — `BaseSchema` declares no `onSuccess` and no index signature (objectui#8347). const base: BaseSchemaTS = { type: 'action', onSuccess: { type: 'toast', message: 'ok' } }; expect([retired, migrated, base]).toHaveLength(3); diff --git a/packages/types/src/__tests__/ai-zero-read-members-retired-8178.test.ts b/packages/types/src/__tests__/ai-zero-read-members-retired-8178.test.ts index 48a2277096..805177603c 100644 --- a/packages/types/src/__tests__/ai-zero-read-members-retired-8178.test.ts +++ b/packages/types/src/__tests__/ai-zero-read-members-retired-8178.test.ts @@ -235,11 +235,15 @@ describe('the controls — without these, a broken import would satisfy every pi expect([assist.showConfidence, recs.layout, query.showHistory]).toEqual([true, 'grid', true]); }); - it('an UNDECLARED key rides both shapes on this carrier — the DELETED row, live', () => { - // No directive on purpose: this is where each of the seven would sit had it - // been deleted instead of tombstoned. `BaseSchema`'s `[key: string]: any` - // absorbs it at any value in every shape, so a deletion produces no - // diagnostic at all — it is not a quieter refusal, it is none. + it('an UNDECLARED key is refused on a fresh literal, and still rides a widened value — the DELETED row, live', () => { + // This is where each of the seven would sit had it been deleted instead of + // tombstoned. Until objectui#8347 `BaseSchema`'s `[key: string]: any` + // absorbed it at any value in every shape, so a deletion produced no + // diagnostic at all. The signature is gone: a fresh literal is refused, + // with no name for the remedy, while a value that reached its annotation + // through a wider variable is still not re-checked (no excess-property + // check on a non-fresh object). The tombstone is what refuses both. + // @ts-expect-error — `AIFormAssistSchema` declares no `bogusUndeclared` (objectui#8347) const fresh: AIFormAssistSchema = { type: 'ai-form-assist', suggestions, bogusUndeclared: 1 }; const raw = { type: 'ai-recommendations' as const, recommendations, bogusUndeclared: 1 }; const widened: AIRecommendationsSchema = raw; diff --git a/packages/types/src/__tests__/anyschema-declared-node-types-11478.test.ts b/packages/types/src/__tests__/anyschema-declared-node-types-11478.test.ts index 1b2dd17af0..cfd3e8147d 100644 --- a/packages/types/src/__tests__/anyschema-declared-node-types-11478.test.ts +++ b/packages/types/src/__tests__/anyschema-declared-node-types-11478.test.ts @@ -19,7 +19,7 @@ * * The population is computed, never listed. The compiler API reads this * package's own sources with its own `tsconfig.json`, so nothing depends on a - * built `dist/`, the same reason `partial-schema-collapse-pin.test.ts` gives. + * built `dist/`, the same reason `object-view-slot-key-lists.test.ts` gives. * It takes every type exported from the root barrel or from a published * subpath entry (read from `package.json` `exports`) that: * diff --git a/packages/types/src/__tests__/app-actions-retired-7469.test.ts b/packages/types/src/__tests__/app-actions-retired-7469.test.ts index a8ed84b8cf..91c772eefb 100644 --- a/packages/types/src/__tests__/app-actions-retired-7469.test.ts +++ b/packages/types/src/__tests__/app-actions-retired-7469.test.ts @@ -167,10 +167,12 @@ type Equal = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; type Expect = T; -/** `Equal`, not `extends`: an UNDECLARED member reads `any` through the index signature. */ +/** `Equal`, not `extends`: an UNDECLARED member read `any` through the index signature until objectui#8347. */ export type assertionActionsRetired = Expect>; -/** The helper can FAIL — synthetic control (an undeclared key reads `any`). */ -export type assertionEqualCanFail = Expect, false>>; +/** The helper can FAIL — synthetic control (a declared member is not `undefined`). */ +export type assertionEqualCanFail = Expect, false>>; +/** An undeclared key is not a member at all since objectui#8347 — it read `any` before. */ +export type assertionUndeclaredKeyIsNoMember = Expect>; /** Keeps the retired-import directive above honest: the name is USED, so a * re-exported `AppAction` cannot leave the directive satisfied by an * unused-import diagnostic instead of the missing-export one. */ diff --git a/packages/types/src/__tests__/base-schema-closed-face-8347.test.ts b/packages/types/src/__tests__/base-schema-closed-face-8347.test.ts new file mode 100644 index 0000000000..346a1e194e --- /dev/null +++ b/packages/types/src/__tests__/base-schema-closed-face-8347.test.ts @@ -0,0 +1,120 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * `BaseSchema` carries no index signature, so a misspelled key stops + * type-checking — the compile-fail pin objectui#7927's ruling made + * non-negotiable, executed by objectui#8347. + * + * ## What the ruling asked for + * + * objectui#7927 measured the defect with seven planted probes on + * `plugin-calendar`'s README. Its P5 renamed `titleField` to `titleFieldd` + * inside an annotated block and came back GREEN, while the type, optionality + * and payload-member probes went red: `BaseSchema` ended in + * `[key: string]: any`, so no annotation on any node type could catch a + * misspelled key. The ruling: remove the signature, and pin it with a fixture + * whose misspelled key must stop type-checking, in BOTH directions — the + * correct spelling must still compile. + * + * ## Why the rows below are FRESH literals + * + * TypeScript runs its excess-property check only on a fresh object literal + * written straight against its target. P5 as #7927 ran it does not qualify on + * today's README: that block is a type alias plus two assignments of declared + * variables, which tsc never excess-checks, so P5 stays green with or without + * the signature (measured on objectui#8347's census, with a lit control that + * went red). Every refusal row here is therefore a fresh literal, and the + * bound is pinned too (section 3), so nobody reads this file as claiming + * more than it does. + * + * ## Who reads it + * + * The rows are TYPE-level: `tsc -p tsconfig.test.json`, chained from this + * package's `type-check` script, is their reader. An `@ts-expect-error` that + * stops matching an error is itself an error (TS2578), so a signature put back + * on `BaseSchema` turns every refusal row below red. The vitest leg only keeps + * the bindings alive. + */ + +import { describe, it, expect } from 'vitest'; +import type { BaseSchema, ComponentRendererProps } from '../base'; +import type { CalendarViewSchema } from '../complex'; +import type { ButtonSchema } from '../form'; +import type { PartialSchema } from '../index'; + +type Equal = + (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; +type Expect = T; + +/* ── 1. The declarations: no string index on the authoring face ──────────── */ + +/** `string extends keyof T` is true exactly when `T` carries a string index signature. */ +type HasStringIndex = string extends keyof T ? true : false; + +export type _BaseSchemaHasNoIndexSignature = Expect, false>>; +export type _NodeTypesInheritNone = Expect, false>>; +// ⛔ Renderer props are not the authoring face: `ComponentRendererProps` keeps +// its own signature on purpose (objectui#8347's card body), and this row is +// what notices it being removed by momentum. +export type _RendererPropsKeepTheirs = Expect, true>>; + +/* ── 2. The pin, both directions, on fresh literals ───────────────────────── */ + +// #7927's P5, turned red: the misspelled key is refused… +export const p5Misspelled: CalendarViewSchema = { + type: 'calendar-view', + // @ts-expect-error — `titleFieldd` is no member of `CalendarViewSchema`; the key is `titleField` + titleFieldd: 'subject', +}; +// …and the correct spelling still compiles. +export const p5Correct: CalendarViewSchema = { type: 'calendar-view', titleField: 'subject' }; + +// The same on the base itself, with a member every node inherits. +export const baseMisspelled: BaseSchema = { + type: 'text', + // @ts-expect-error — `classNmae` is no member of `BaseSchema`; the key is `className` + classNmae: 'font-bold', +}; +export const baseCorrect: BaseSchema = { type: 'text', className: 'font-bold' }; + +// A node nested in a slot is judged as its own node type (objectui#11466), +// and now every node type refuses a key it does not declare. +export const nestedMisspelled: BaseSchema = { + type: 'card', + // @ts-expect-error — `labell` is no member of the `button` node; the key is `label` (tsc reports it at the slot) + children: [{ type: 'button', labell: 'Save' }], +}; +export const nestedCorrect: BaseSchema = { type: 'card', children: [{ type: 'button', label: 'Save' }] }; + +/* ── 3. The bound: a NON-fresh value is not re-checked ───────────────────── */ + +// No directive on purpose. A value that reached its annotation through a +// variable of a wider type carries its extra key past the check: TypeScript's +// excess-property check is a fresh-literal check, not a structural one. This is +// why `plugin-calendar`'s README assignments cannot carry P5, and why the +// strict zod face (`StrictAnyComponentSchema`, objectui#8345), not this face, +// is the refusal for metadata that arrives as data. +const arrived = { type: 'calendar-view' as const, titleField: 'subject', titleFieldd: 'subject' }; +export const widenedIsNotRechecked: CalendarViewSchema = arrived; + +/* ── 4. `PartialSchema` works as written now (objectui#6397) ─────────────── */ + +// While the signature stood, `keyof T` absorbed every member name and this alias +// declared `type` alone. It keeps `T`'s named members now… +export type _PartialKeepsTheMembers = Expect, keyof ButtonSchema>>; +// …with `type` required and the rest optional… +export const partialButton: PartialSchema = { type: 'button' }; +// …and it refuses a misspelling like every node type. +export const partialMisspelled: PartialSchema = { + type: 'button', + // @ts-expect-error — `labell` is no member of `ButtonSchema`; the key is `label` + labell: 'Save', +}; + +describe('`BaseSchema` declares no index signature (objectui#8347, the objectui#7927 pin)', () => { + it('keeps the type-level rows alive — `tsc -p tsconfig.test.json` is their reader', () => { + expect([p5Correct.titleField, baseCorrect.className, partialButton.type]).toEqual(['subject', 'font-bold', 'button']); + expect([p5Misspelled, baseMisspelled, nestedMisspelled, nestedCorrect, widenedIsNotRechecked, partialMisspelled]) + .toHaveLength(6); + }); +}); diff --git a/packages/types/src/__tests__/calendar-flat-color-allday-8466.test.ts b/packages/types/src/__tests__/calendar-flat-color-allday-8466.test.ts index 01722bae1a..465528a6cf 100644 --- a/packages/types/src/__tests__/calendar-flat-color-allday-8466.test.ts +++ b/packages/types/src/__tests__/calendar-flat-color-allday-8466.test.ts @@ -65,10 +65,12 @@ * * ## What declaring buys — and what it does NOT, measured not assumed * - * objectui#7927 measured the ceiling: `BaseSchema` ends in `[key: string]: any`, - * so no annotation here can catch a MISSPELLED key. `colourField` stays admitted - * on both faces, and the control assertions below PIN that, so nobody reads this - * file as claiming more than it does. What the ceiling does not cap is the VALUE + * objectui#7927 measured the ceiling: `BaseSchema` ended in `[key: string]: any`, + * so no annotation here could catch a MISSPELLED key. objectui#8347 removed the + * signature, so the TS face refuses `colourField` on a fresh literal now (pinned + * below); the tolerant zod face still admits it, and the control assertions + * below PIN that, so nobody reads this file as claiming more than it does. What + * the ceiling never capped is the VALUE * dimension, and that is the half this file pins — on the TS face through * `@ts-expect-error` directives that go UNUSED (TS2578, a hard type-check * failure) the moment their member is deleted, and on the mirror through @@ -138,16 +140,16 @@ const NEWLY_DECLARED = ['colorField', 'allDayField'] as const; /** * A key the renderer never reads and neither face declares. Non-vacuity control - * for every "declared" assertion: it must stay `any` on the TS face and out of - * the mirror shape, while still being ADMITTED — the objectui#7927 ceiling, - * pinned rather than claimed away. + * for every "declared" assertion: it must stay no member of the TS face (it + * read `any` there until objectui#8347) and out of the mirror shape, while + * still being ADMITTED by the tolerant mirror's `.passthrough()`. */ const CONTROL_KEY = 'swatchField'; /** A declared-and-read control for the off-disk read census, read BARE. */ const READ_CONTROL_KEY = 'objectName'; /** The same, read through a CAST — the census must see both forms, not one. */ const CAST_READ_CONTROL_KEY = 'defaultView'; -/** The misspelling the ceiling still admits. Pinned, not fixed here. */ +/** The misspelling the tolerant mirror still admits (the TS face refuses it since objectui#8347). */ const MISSPELLING = 'colourField'; const CALENDAR_NODE = { type: 'object-calendar', objectName: 'event' } as const; @@ -172,11 +174,16 @@ export type _AllDayFieldIsString = Expect, false>>; export type _AllDayFieldIsOptional = Expect>; // The control key is undeclared, exactly as the two above were before this card. -// Same instrument, opposite verdict — which is what makes the six lines above -// readings rather than a type that says `true` for everything. -export type _ControlKeyFallsThrough = Expect>; -// objectui#7927's ceiling, pinned rather than claimed away. -export type _MisspellingStillAdmitted = Expect>; +// Until objectui#8347 it fell through `BaseSchema`'s index signature and read +// `any`; that signature is gone, so it is no member at all. `IsAny` is the +// lit control that keeps the `IsNotAny` lines above readings rather than a +// type that says `false` for everything. +export type _IsAnyCanAnswerTrue = Expect>; +export type _ControlKeyIsNoMember = Expect>; +// objectui#7927's ceiling ("a misspelling is still admitted") was pinned here +// rather than claimed away; objectui#8347 removed it, so the misspelling is no +// member either, and a fresh literal carrying it is refused (below). +export type _MisspellingIsNoMember = Expect>; // The SIBLING interface in the same plugin — served by its OWN renderer, which // reads the same five flat keys — already declares all five. A member that fell @@ -190,9 +197,9 @@ const siblingPins: [ Expect>, Expect>, ] = [true, true, true, true, true]; -// Control: the same instrument returns the OPPOSITE verdict for a key the -// sibling does not declare either, so the five above are readings. -export type _SiblingControlFallsThrough = Expect>; +// Control: a key the sibling does not declare either is no member of it — it +// read `any` through the index signature until objectui#8347 removed it. +export type _SiblingControlIsNoMember = Expect>; // The TS face ACCEPTS the flat handoff shape… const calendarLiteral: TsObjectCalendarSchema = { @@ -207,6 +214,9 @@ const calendarLiteral: TsObjectCalendarSchema = { const calendarBadColorField: TsObjectCalendarSchema = { ...CALENDAR_NODE, colorField: 0xff0000 }; // @ts-expect-error — `allDayField` names a FIELD; a boolean is the VALUE, the confusion this declaration catches const calendarBadAllDayField: TsObjectCalendarSchema = { ...CALENDAR_NODE, allDayField: true }; +// …and, since objectui#8347 removed `BaseSchema`'s index signature, the misspelling itself. +// @ts-expect-error — `colourField` is no member of `ObjectCalendarSchema`; the key is `colorField` +const calendarMisspelledKey: TsObjectCalendarSchema = { ...CALENDAR_NODE, colourField: 'status_colour' }; /* ── Off-disk derivations ─────────────────────────────────────────────────── */ @@ -662,6 +672,6 @@ describe('objectui#8466 — the TS face accepts the flat handoff node', () => { expect(calendarLiteral.allDayField).toBe('is_all_day'); // The refused literals exist only so their `@ts-expect-error` directives do; // referencing them keeps `noUnusedLocals` off this file's back. - expect([calendarBadColorField, calendarBadAllDayField]).toHaveLength(2); + expect([calendarBadColorField, calendarBadAllDayField, calendarMisspelledKey]).toHaveLength(3); }); }); diff --git a/packages/types/src/__tests__/chatbot-dark-keys-retired-7703.test.ts b/packages/types/src/__tests__/chatbot-dark-keys-retired-7703.test.ts index 672f1d0c68..8a0ff685c4 100644 --- a/packages/types/src/__tests__/chatbot-dark-keys-retired-7703.test.ts +++ b/packages/types/src/__tests__/chatbot-dark-keys-retired-7703.test.ts @@ -413,21 +413,22 @@ describe('the six keys are RETIRED on the TypeScript face too (objectui#7703)', // boundary on the `tsc` face. `ChatbotEnhancedSchema` and // `ChatbotFloatingSchema` reach `ChatbotSchema` only through // `Pick<..., ChatbotSharedKey | ...>`, and none of the six is a picked key, - // so no tombstone can travel to them: the keys stay UNDECLARED there and - // ride `BaseSchema`'s index signature, exactly as before this change. Both - // annotated assignments below are directive-FREE on purpose — if a - // tombstone ever reached either face, they would stop compiling and this - // leg would be the one that says so. - const enhanced: ChatbotEnhancedSchemaTS = { - type: 'chatbot-enhanced', - messages: [], - showAvatars: true, - }; - const floating: ChatbotFloatingSchemaTS = { - type: 'chatbot-floating', - messages: [], - height: 400, - }; - expect([enhanced.type, floating.type]).toEqual(['chatbot-enhanced', 'chatbot-floating']); + // so no tombstone can travel to them: the keys stay UNDECLARED there — no + // member at all, where a tombstone would be a `?: never` member. Until + // objectui#8347 they rode `BaseSchema`'s index signature, and a + // directive-free annotated literal was this leg's instrument. With the + // signature gone an undeclared key is refused on a fresh literal as well, + // so the leg reads membership instead, which separates the two. + type IsMember = K extends keyof T ? true : false; + const undeclared: [IsMember, IsMember] = [ + false, + false, + ]; + // Control: a picked key IS a member of each sibling, so `false` is a reading. + const picked: [IsMember, IsMember] = [ + true, + true, + ]; + expect([...undeclared, ...picked]).toEqual([false, false, true, true]); }); }); diff --git a/packages/types/src/__tests__/chatbot-display-mode-retired.test.ts b/packages/types/src/__tests__/chatbot-display-mode-retired.test.ts index a237f25973..ff0bfffee4 100644 --- a/packages/types/src/__tests__/chatbot-display-mode-retired.test.ts +++ b/packages/types/src/__tests__/chatbot-display-mode-retired.test.ts @@ -148,11 +148,14 @@ describe('the `displayMode` tombstone makes authoring a `tsc` error on both face expect(onChatbot.processVisibility).toBe('debug'); }); - it('an UNDECLARED key rides a fresh literal AND a widened value on this carrier — the DELETED row, live', () => { - // No directive on purpose: this is where `displayMode` would sit had it - // been deleted instead of tombstoned. `BaseSchema`'s `[key: string]: any` - // absorbs the key at any value in every shape, so a deletion here is not - // "quieter" than a tombstone — it produces no diagnostic at all. + it('an UNDECLARED key is refused on a fresh literal and still rides a widened value — the DELETED row, live', () => { + // This is where `displayMode` would sit had it been deleted instead of + // tombstoned. Until objectui#8347 `BaseSchema`'s `[key: string]: any` + // absorbed the key at any value in every shape, so a deletion produced no + // diagnostic at all. The signature is gone: a fresh literal is refused, + // with no name for the remedy, while a widened value is still not + // re-checked (no excess-property check on a non-fresh object). + // @ts-expect-error — `ChatbotSchema` declares no `bogusUndeclared` (objectui#8347) const fresh: TsChatbotSchema = { type: 'chatbot', messages, bogusUndeclared: 1 }; const raw = { type: 'chatbot' as const, messages, bogusUndeclared: 1 }; const widened: TsChatbotSchema = raw; diff --git a/packages/types/src/__tests__/checkbox-wrapper-class-6938.test.ts b/packages/types/src/__tests__/checkbox-wrapper-class-6938.test.ts index dba76dee11..c45f239cbd 100644 --- a/packages/types/src/__tests__/checkbox-wrapper-class-6938.test.ts +++ b/packages/types/src/__tests__/checkbox-wrapper-class-6938.test.ts @@ -70,18 +70,20 @@ type Expect = T; /** The canonical `any` detector: only `any` absorbs `1 &` down to something `0` extends. */ type IsAny = 0 extends (1 & T) ? true : false; -// Declared as `string`. Were the member removed, the read would fall back to -// the index signature and resolve to `any`, and `Equal` is false. +// Declared as `string`. Were the member removed, the read would not compile +// (objectui#8347 took away the index signature it used to fall back to as `any`). export type _WrapperClassIsString = Expect, string>>; export type _WrapperClassIsNotAny = Expect, false>>; -// The control key is NOT declared: it resolves to `any` through the index -// signature, exactly as `wrapperClass` did before this card. Declaring it turns -// this red — which is the point. -export type _ControlKeyFallsThroughToIndexSignature = Expect>; - -// The TS face accepts the key on a literal. ⚠️ This is the WEAK half: the index -// signature would accept it undeclared too. The invariant pins above are the -// guard; this line only shows the declared spelling in use. +// Lit control for the detector: `IsAny` does answer `true`, so the line above is a reading. +export type _IsAnyCanAnswerTrue = Expect>; +// The control key is NOT declared: it resolved to `any` through the index +// signature, exactly as `wrapperClass` did before this card, until objectui#8347 +// removed the signature. It is no member now. Declaring it turns this red. +export type _ControlKeyIsNoMember = Expect>; + +// The TS face accepts the key on a literal. Since objectui#8347 this is no +// longer the weak half: without the index signature an undeclared key is +// refused on a fresh literal, so the acceptance reads the declaration. const literal: TsCheckboxSchema = { type: 'checkbox', label: 'Accept', wrapperClass: 'gap-4' }; /** Every `schema.KEY` read in the renderer, off disk. */ diff --git a/packages/types/src/__tests__/content-channel-e3-residual-9256.test.ts b/packages/types/src/__tests__/content-channel-e3-residual-9256.test.ts index 1d23f211e1..b946ed2538 100644 --- a/packages/types/src/__tests__/content-channel-e3-residual-9256.test.ts +++ b/packages/types/src/__tests__/content-channel-e3-residual-9256.test.ts @@ -350,9 +350,11 @@ describe('objectui#9256 E3 residual — the TypeScript face refuses both channel it('CONTROL — the same nodes WITHOUT a content channel compile (no `@ts-expect-error` here, and `tsc` is the reader)', () => { const ok = [grid, form, kanban, map, tree, view, gantt, calendar, chart, detail, email, password, uiCalendar]; - // The repair keeps the index signature, so it narrows nothing an author - // could write before beyond the members' own declared types — an - // undeclared key still compiles, exactly as it did. + // The repair kept the index signature, so it narrowed nothing an author + // could write before beyond the members' own declared types. objectui#8347 + // then removed `BaseSchema`'s signature, so an undeclared key no longer + // compiles on these nodes either. + // @ts-expect-error — `InputShorthandSchema` declares no `notAMember` (objectui#8347) const undeclared = { type: 'email', label: 'Email', notAMember: 1 } satisfies InputShorthandSchema; const calendarRange = { type: 'ui:calendar', mode: 'range' } satisfies UiCalendarSchema; expect(ok).toHaveLength(13); diff --git a/packages/types/src/__tests__/dashboard-title-retired-declaration.test.ts b/packages/types/src/__tests__/dashboard-title-retired-declaration.test.ts index 185791ed16..5962783094 100644 --- a/packages/types/src/__tests__/dashboard-title-retired-declaration.test.ts +++ b/packages/types/src/__tests__/dashboard-title-retired-declaration.test.ts @@ -36,11 +36,14 @@ * * ## The trap: why this is a key-set probe and NOT `@ts-expect-error` * - * ⚠️ `BaseSchema` carries `[key: string]: any`, so an authored `title:` on a - * dashboard literal STILL COMPILES after the member is gone — it falls to the index - * signature. A `@ts-expect-error` pin on an authored literal therefore cannot stick: - * it would pass for the wrong reason before the removal and fail after it. The - * pinnable effect is that `title` stops being a DECLARED member. The probe below + * ⚠️ When this was written `BaseSchema` carried `[key: string]: any`, so an + * authored `title:` on a dashboard literal STILL COMPILED after the member was gone + * — it fell to the index signature, and a `@ts-expect-error` pin on an authored + * literal could not stick. objectui#8347 removed that signature, so the literal is + * refused now too; the key-set probe stays the pin because it reads the + * DECLARATION, which an excess-key refusal on a literal cannot tell apart from a + * `?: never` tombstone. The pinnable effect is that `title` stops being a DECLARED + * member. The probe below * extracts the interface's literal key set — the index signature is filtered out by * `string extends K` — and asserts `title` is out while its neighbours stay in. * Real enforcement because `packages/types/tsconfig.test.json` is chained from this 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 index 96821ae4dd..45dff6e4e8 100644 --- 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 @@ -49,7 +49,7 @@ * type, so a `metric-card` with no `value` is refused on this face too; * 6. objectui#11467: the arm DECLARES `MetricCard`'s registered inputs, so the * README's `metric-card` literals compile against members rather than - * against `BaseSchema`'s index signature (which objectui#8347 removes); + * against `BaseSchema`'s index signature (which objectui#8347 removed); * the Zod twin declares the same members; and the widget's `component` * slot takes the arm first, on both faces. The members' parity with the * live registration and `MetricCardProps` is pinned in @@ -180,6 +180,9 @@ describe('NOT A HATCH — what the union still refuses, on both faces', () => { const doc: DashboardComponentSchema = { type: 'dashboard', // `value` is the card's required input since objectui#11467; `someProp` is the undeclared key. + // The TS face refuses it since objectui#8347 removed `BaseSchema`'s index + // signature; the passthrough this test reads is the zod face's. + // @ts-expect-error — `DashboardWidgetSlotComponentSchema` declares no `someProp` (objectui#8347) widgets: [{ type: 'metric-card', title: 'x', value: '1', someProp: 1 }], }; const result = DashboardComponentZod.safeParse(doc); @@ -189,27 +192,25 @@ describe('NOT A HATCH — what the union still refuses, on both faces', () => { }); }); -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 = { +// A `MEASURED LIMIT` block stood here: a `type`-less legacy envelope carrying an +// undeclared key compiled, because the component arm's index signature +// satisfied the union's excess-property check. objectui#8347 removed +// `BaseSchema`'s signature, tsc refused the literal, and the block's own removal +// condition deleted it together with the note on `widgets` in `complex.ts`. +describe('the legacy `component` envelope on the TypeScript face (objectui#7952, objectui#8347)', () => { + it('a `metric-card` with no `value` inside the envelope is refused (objectui#8347, Q3 = A)', () => { + const valueless: DashboardComponentSchema = { type: 'dashboard', - widgets: [{ id: 'w', component: { type: 'metric-card', value: '1' }, bogus: 1 }], + // @ts-expect-error — `value` is the card's one required input, inside the envelope too (the diagnostic names `value`). + widgets: [{ id: 'w', component: { type: 'metric-card', title: 'Revenue' } }], }; - 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'); + // The tolerant zod face still accepts this document, through the widget + // `component` slot's `BaseSchema` arm (objectui#8344); that is the zod + // face's question, which this card leaves alone (objectui#8345). + expect(valueless.type).toBe('dashboard'); }); - it('the same envelope without the stray key is legal on both faces', () => { + it('the same envelope with `value` is legal on both faces (the control)', () => { const envelope: DashboardComponentSchema = { type: 'dashboard', widgets: [{ id: 'w', component: { type: 'metric-card', value: '1' }, layout: { x: 0, y: 0, w: 1, h: 1 } }], @@ -271,7 +272,7 @@ describe('objectui#11467 — the component arm declares `MetricCard`\'s register expect(twins).toBe(true); }); - it('the TypeScript face judges each value by its member, while the index signature still stands', () => { + it('the TypeScript face judges each value by its member', () => { // @ts-expect-error — TS2322: `trend` is the registration's enum. const sideways: DashboardWidgetSlotComponentSchema = { type: 'metric-card', value: '1', trend: 'sideways' }; // @ts-expect-error — TS2741: `value` is the card's one required input. diff --git a/packages/types/src/__tests__/flat-arm-responsive-styles-10872.test.ts b/packages/types/src/__tests__/flat-arm-responsive-styles-10872.test.ts index 58a5b9a069..63f7da6cd3 100644 --- a/packages/types/src/__tests__/flat-arm-responsive-styles-10872.test.ts +++ b/packages/types/src/__tests__/flat-arm-responsive-styles-10872.test.ts @@ -105,12 +105,17 @@ export type assertionTsTwinIsTheSpecType = [ Expect>, ]; /** - * The helper can FAIL, on the exact shape a missing declaration takes: `stack` - * declares no `responsiveStyles`, so the member resolves through the index - * signature, and `Equal` refuses it. + * The helper can FAIL: `Equal` refuses a member whose type differs from the + * spec's (`stack`'s declared `className` is no `SpecResponsiveStyles`), so the + * lines above are readings. `stack` declares no `responsiveStyles` at all: the + * member used to resolve through the index signature, and since objectui#8347 + * removed it the key is no member of `stack`. */ -export type assertionUndeclaredTwinIsRefused = Expect< - Equal, false> +export type assertionEqualCanFail = Expect< + Equal, false> +>; +export type assertionUndeclaredTwinIsNoMember = Expect< + Equal<'responsiveStyles' extends keyof TsStackSchema ? true : false, false> >; /* ── The population ───────────────────────────────────────────────────────── */ diff --git a/packages/types/src/__tests__/flex-bag-children-list-11564.test.ts b/packages/types/src/__tests__/flex-bag-children-list-11564.test.ts index 2fa115df66..4b40a8257b 100644 --- a/packages/types/src/__tests__/flex-bag-children-list-11564.test.ts +++ b/packages/types/src/__tests__/flex-bag-children-list-11564.test.ts @@ -21,8 +21,9 @@ * TypeScript face the same member read `unknown[]`, so the list, the form a * `flex` node is authored in, took any entry while a single child was judged * as a node. With `BaseSchema`'s index signature present nothing showed, since - * the signature absorbs a misspelled key either way; once objectui#8347 - * removes it, the list would have been the one unchecked node slot. + * the signature absorbed a misspelled key either way; once objectui#8347 + * removed it, the list would have been the one unchecked node slot, which is + * why this card landed first. * * ## What is pinned here * @@ -31,10 +32,11 @@ * * - the bag's `children` is one node or a list of nodes, the flat mirror's * own member, and it is the only member that differs from the arm; - * - a closed node's misspelling inside the list is refused today. A - * spec-derived node carries no index signature, so it is the shape every - * node has after objectui#8347; the control is the same document typed - * the way the arm reads it, which compiles; + * - a closed node's misspelling inside the list is refused. A spec-derived + * node carries no index signature, which is the shape every node has had + * since objectui#8347, so a node type that extends `BaseSchema` is refused + * there too (the `text` row); the control is the same document typed the + * way the arm reads it, which compiles; * - an undeclared `type` and a nested array inside the list are refused, * and a primitive entry compiles, as in every node slot. * @@ -110,7 +112,11 @@ describe('objectui#11564: the flex bag\'s child list is typed as nodes', () => { // Control: the same misspelling as a single child was refused before this card too. // @ts-expect-error `contnt` is not a member of `element:text`'s bag const single: FlexBlockNode = { type: 'flex', properties: { children: { type: 'element:text', properties: { contnt: 'x' } } } }; - expect([inList, armReading, single]).toHaveLength(3); + // Since objectui#8347 a node type that extends `BaseSchema` refuses a misspelling in the list + // too: `text` declares `content` (the well-formed list below writes it and compiles). + // @ts-expect-error `contnt` is no member of the `text` node + const extendsBase: FlexBlockNode = { type: 'flex', properties: { children: [{ type: 'text', contnt: 'x' }] } }; + expect([inList, armReading, single, extendsBase]).toHaveLength(4); }); it('a well-formed list compiles: declared nodes, primitives, a nested flex', () => { diff --git a/packages/types/src/__tests__/input-wrapper-class-mirrored-8072.test.ts b/packages/types/src/__tests__/input-wrapper-class-mirrored-8072.test.ts index df95880347..fc16077a20 100644 --- a/packages/types/src/__tests__/input-wrapper-class-mirrored-8072.test.ts +++ b/packages/types/src/__tests__/input-wrapper-class-mirrored-8072.test.ts @@ -90,17 +90,20 @@ type Expect = T; type IsAny = 0 extends (1 & T) ? true : false; // The TS face already declared this key; these guard it against being removed -// on the way past, in which case the read falls back to `BaseSchema`'s index -// signature and resolves to `any`, and `Equal` is false. +// on the way past, in which case the read stops compiling (it fell back to +// `BaseSchema`'s index signature as `any` until objectui#8347 removed it). export type _WrapperClassIsString = Expect, string>>; export type _WrapperClassIsNotAny = Expect, false>>; -// The control key is NOT declared: it resolves to `any` through the index -// signature, exactly as `wrapperClass` did on the MIRROR before this card. -export type _ControlKeyFallsThroughToIndexSignature = Expect>; - -// The TS face accepts the key on a literal. ⚠️ This is the WEAK half: the index -// signature would accept it undeclared too. The invariant pins above are the -// guard; this line only shows the declared spelling in use. +// Lit control for the detector: `IsAny` does answer `true`, so the line above is a reading. +export type _IsAnyCanAnswerTrue = Expect>; +// The control key is NOT declared: it resolved to `any` through the index +// signature, exactly as `wrapperClass` did on the MIRROR before this card, +// until objectui#8347 removed the signature. It is no member now. +export type _ControlKeyIsNoMember = Expect>; + +// The TS face accepts the key on a literal. Since objectui#8347 this is no +// longer the weak half: without the index signature an undeclared key is +// refused on a fresh literal, so the acceptance reads the declaration. const literal: TsInputSchema = { type: 'input', label: 'Name', wrapperClass: 'gap-4' }; interface Mirror { diff --git a/packages/types/src/__tests__/kanban-calendar-filter-sort-8174.test.ts b/packages/types/src/__tests__/kanban-calendar-filter-sort-8174.test.ts index e0bc0b8675..1880ff2dfe 100644 --- a/packages/types/src/__tests__/kanban-calendar-filter-sort-8174.test.ts +++ b/packages/types/src/__tests__/kanban-calendar-filter-sort-8174.test.ts @@ -27,11 +27,12 @@ * * ## What declaring buys — and what it does NOT, measured not assumed * - * objectui#7927 measured the ceiling: `BaseSchema` ends in `[key: string]: any`, - * so no annotation on any node schema can catch a MISSPELLED key. That ceiling - * is real and this card does not lift it — `filtr` stays admitted on both - * faces, and the control assertions below PIN that, so nobody reads this file as - * claiming more than it does. + * objectui#7927 measured the ceiling: `BaseSchema` ended in `[key: string]: any`, + * so no annotation on any node schema could catch a MISSPELLED key. This card + * did not lift it; objectui#8347 did, by removing the signature — `filtr` is + * refused on a fresh literal on the TS face now (pinned below) and stays + * admitted by the tolerant mirror's `.passthrough()`, which the control + * assertions below PIN, so nobody reads this file as claiming more than it does. * * What the ceiling does not cap is the VALUE dimension, and that is the half * this file pins: @@ -133,16 +134,21 @@ export type _CalendarSortIsOptional = Expect>; +// It fell through to the index signature as `any` until objectui#8347 removed +// that; it is no member now, and `ObjectKanban` types the gate-written key on +// its own read type (`GateBoundKanbanSchema`). Declaring it turns this red. +export type _KanbanSortStaysUndeclared = Expect>; +// Lit control for the detector: `IsAny` does answer `true`, so the `IsNotAny` +// lines above are readings. +export type _IsAnyCanAnswerTrue = Expect>; // The control key is undeclared on both, exactly as `filter` was before this -// card. Same instrument, opposite verdict. -export type _KanbanControlKeyFallsThrough = Expect>; -export type _CalendarControlKeyFallsThrough = Expect>; -// objectui#7927's ceiling, pinned rather than claimed away: a MISSPELLED key -// still resolves to `any` and still type-checks. This card does not fix that, -// and this line is what stops anyone reading it as if it had. -export type _MisspellingStillAdmitted = Expect>; +// card: no member of either (it read `any` before objectui#8347). +export type _KanbanControlKeyIsNoMember = Expect>; +export type _CalendarControlKeyIsNoMember = Expect>; +// objectui#7927's ceiling — a MISSPELLED key still resolved to `any` and still +// type-checked — was pinned here rather than claimed away. objectui#8347 +// removed it: the misspelling is no member, and a fresh literal is refused. +export type _MisspellingIsNoMember = Expect>; // The TS face ACCEPTS the documented shapes on a literal… const kanbanLiteral: TsObjectKanbanSchema = { ...KANBAN_NODE, filter: FILTER_VALUE }; @@ -158,6 +164,11 @@ const calendarBadFilter: TsObjectCalendarSchema = { ...CALENDAR_NODE, filter: 's const calendarRetiredSort: TsObjectCalendarSchema = { ...CALENDAR_NODE, sort: 'start_date asc' }; // @ts-expect-error — `order` is `'asc' | 'desc'`; the member is `SortConfig[]`, not `any[]` const calendarBadSortOrder: TsObjectCalendarSchema = { ...CALENDAR_NODE, sort: [{ field: 'start_date', order: 'sideways' }] }; +// …and, since objectui#8347, the misspelling itself and the gate-only key on the board. +// @ts-expect-error — `filtr` is no member of `ObjectCalendarSchema`; the key is `filter` +const calendarMisspelledFilter: TsObjectCalendarSchema = { ...CALENDAR_NODE, filtr: FILTER_VALUE }; +// @ts-expect-error — `sort` is not an `object-kanban` authoring key; the binding's `dataSource.sort` is +const kanbanAuthoredSort: TsObjectKanbanSchema = { ...KANBAN_NODE, sort: SORT_VALUE }; /* ── Off-disk derivations ─────────────────────────────────────────────────── */ @@ -361,6 +372,6 @@ describe('objectui#8174 — the TS face accepts the documented nodes', () => { expect(calendarLiteral.sort).toEqual(SORT_VALUE); // The refused literals exist only so their `@ts-expect-error` directives do; // referencing them keeps `noUnusedLocals` off this file's back. - expect([kanbanBadFilter, calendarBadFilter, calendarRetiredSort, calendarBadSortOrder]).toHaveLength(4); + expect([kanbanBadFilter, calendarBadFilter, calendarRetiredSort, calendarBadSortOrder, calendarMisspelledFilter, kanbanAuthoredSort]).toHaveLength(6); }); }); diff --git a/packages/types/src/__tests__/mirror-only-published-keys-9729.test.ts b/packages/types/src/__tests__/mirror-only-published-keys-9729.test.ts index c84397a8d5..95341f5a00 100644 --- a/packages/types/src/__tests__/mirror-only-published-keys-9729.test.ts +++ b/packages/types/src/__tests__/mirror-only-published-keys-9729.test.ts @@ -68,6 +68,9 @@ * refuses the key by name and prints the upstream spelling, while the twin * is deliberately left alone — so the two faces still do not contradict, * but the silent one is no longer the only one an author hears from. + * ⚠️ Dated note (objectui#8347): `BaseSchema` no longer carries that index + * signature, so the twin refuses an authored `operators` too, as an + * undeclared key. Still not declared, still not tombstoned on the twin. * * ## ⚠️ The third key the ledger groups with these two is NOT this defect * @@ -246,21 +249,24 @@ export type assertionDueLikeIsDeclaredOnTheTwin = Expect< Equal< DetailViewField['dueLike'], boolean | undefined > >; /** - * The TWIN still does NOT refuse `operators` — and the absence of a - * `@ts-expect-error` here is the measurement, not an omission: adding one - * reddens the same type-check leg as unused, which is how this claim fails if - * the twin ever loses `BaseSchema`'s index signature. + * The TWIN refuses `operators` too since objectui#8347 removed `BaseSchema`'s + * index signature. Before it, the absence of a `@ts-expect-error` here was the + * measurement — this binding compiled through the signature — and this + * docblock named the removal as the way that claim would fail. It failed as + * named: the key is still NOT declared on the twin (below), so it is refused + * as an undeclared key, with no name for the remedy; the mirror's tombstone + * is what names `operations`. * * ⭐ Unchanged by objectui#9739 ON PURPOSE. Letter C rules the key "not declared * on the TypeScript twin" — declaring it was letter A, refused, on the ground * that it writes a misspelling into the published interface beside the correct - * spelling. So this binding compiling is still the true reading of the twin; - * what changed is that the MIRROR beside it no longer agrees, and that asymmetry - * is the ruled outcome rather than an unrepaired gap. + * spelling. That ruling stands: the twin declares nothing for `operators`, + * and both faces now refuse it, the mirror by name and the twin as undeclared. */ export const authoredOperators: ObjectGridSchemaType = { type: 'object-grid', objectName: 'probe', + // @ts-expect-error — `operators` is not declared on the twin; since objectui#8347 an undeclared key is refused operators: { name: ['equals'] }, }; diff --git a/packages/types/src/__tests__/node-slot-union-11466.test.ts b/packages/types/src/__tests__/node-slot-union-11466.test.ts index adafee6843..6138029fbc 100644 --- a/packages/types/src/__tests__/node-slot-union-11466.test.ts +++ b/packages/types/src/__tests__/node-slot-union-11466.test.ts @@ -18,9 +18,10 @@ * * ## The compile-fail pin, and why it does not wait for objectui#8347 * - * Until objectui#8347 removes `BaseSchema`'s index signature, a misspelled key on - * a node type that extends `BaseSchema` still compiles: the signature absorbs it - * on the type's own arm. What this card changes is the SLOT: it discriminates on + * Until objectui#8347 removed `BaseSchema`'s index signature, a misspelled key on + * a node type that extends `BaseSchema` still compiled: the signature absorbed it + * on the type's own arm. (Since the removal it is refused there too, pinned in + * `base-schema-closed-face-8347.test.ts`.) What this card changed is the SLOT: it discriminates on * `type`, so an inline child is judged against its own type's arm wherever it is * nested. `ClosedProbeNode` below is a node type declared WITHOUT an index * signature, the shape every arm has once the removal lands, registered through diff --git a/packages/types/src/__tests__/object-chart-legacy-axis-keys-retired-10608.test.ts b/packages/types/src/__tests__/object-chart-legacy-axis-keys-retired-10608.test.ts index b485159fce..1a0fb38af3 100644 --- a/packages/types/src/__tests__/object-chart-legacy-axis-keys-retired-10608.test.ts +++ b/packages/types/src/__tests__/object-chart-legacy-axis-keys-retired-10608.test.ts @@ -212,12 +212,14 @@ type Equal = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; type Expect = T; -/** `Equal`, not `extends`: an UNDECLARED member reads `any` through the index signature. */ +/** `Equal`, not `extends`: an UNDECLARED member read `any` through the index signature until objectui#8347. */ export type assertionXAxisFieldRetired = Expect>; export type assertionYAxisFieldsRetired = Expect>; export type assertionAggregationRetired = Expect>; -/** The helper can FAIL — synthetic control (an undeclared key reads `any`). */ -export type assertionEqualCanFail = Expect, false>>; +/** The helper can FAIL — synthetic control (a declared member is not `undefined`). */ +export type assertionEqualCanFail = Expect, false>>; +/** An undeclared key is no member at all since objectui#8347 — it read `any` before. */ +export type assertionUndeclaredKeyIsNoMember = Expect>; describe('objectui#10608 (f) — the TS twin refuses what the mirror refuses', () => { it('each retired key is a compile error — checked by `tsc -p tsconfig.test.json`', () => { diff --git a/packages/types/src/__tests__/object-chart-undeclared-keys-8885.test.ts b/packages/types/src/__tests__/object-chart-undeclared-keys-8885.test.ts index 2a52e8e9b8..d41519b8a6 100644 --- a/packages/types/src/__tests__/object-chart-undeclared-keys-8885.test.ts +++ b/packages/types/src/__tests__/object-chart-undeclared-keys-8885.test.ts @@ -160,11 +160,13 @@ describe('ObjectChartSchema — compile-time pins for the three keys (objectui#8 expect(withMode.drillDown).toBeTruthy(); }); - it('the ceiling: a MISSPELLING still compiles, because `BaseSchema` carries an index signature', () => { - // Not a defect being papered over — the honest bound of what declaring a - // key buys. Revisit deliberately when objectui#5155 lands. + it('the ceiling is gone: a MISSPELLING stops compiling, since objectui#8347 removed `BaseSchema`\'s index signature', () => { + // This row pinned the honest bound of what declaring a key bought while the + // signature stood ("revisit deliberately when objectui#5155 lands"). The + // removal landed as objectui#8347, and the row flipped with it. + // @ts-expect-error — `drillDwn` is no member of `ObjectChartSchema`; the key is `drillDown` const typo: ObjectChartSchema = { type: 'object-chart', chartType: 'bar', drillDwn: { enabled: true } }; - expect(typo.drillDwn).toEqual({ enabled: true }); + expect(typo.type).toBe('object-chart'); }); }); diff --git a/packages/types/src/__tests__/object-data-table-dataprovider-retired-7353.test.ts b/packages/types/src/__tests__/object-data-table-dataprovider-retired-7353.test.ts index 56c4ad4015..f213e4888b 100644 --- a/packages/types/src/__tests__/object-data-table-dataprovider-retired-7353.test.ts +++ b/packages/types/src/__tests__/object-data-table-dataprovider-retired-7353.test.ts @@ -88,14 +88,17 @@ describe('objectui#7353 — the TS face refuses an authored dataProvider', () => // @ts-expect-error `dataProvider` is a retirement tombstone (objectui#7353) — write `objectName` dataProvider: MALFORMED, }; - // The contrast, pinned LIVE rather than in prose: an undeclared key carries - // no directive, because `BaseSchema`'s index signature absorbs it. That is - // what a deletion would have left `dataProvider` as. + // The contrast, pinned LIVE rather than in prose: an undeclared key carried + // no directive while `BaseSchema`'s index signature absorbed it — what a + // deletion would have left `dataProvider` as. objectui#8347 removed the + // signature, so it is refused too now, but as an undeclared key with no + // remedy named; the tombstone above is what names `objectName`. + // @ts-expect-error — `ObjectDataTableSchema` declares no `zzUndeclared7353` (objectui#8347) const undeclared: ObjectDataTableSchema = { type: 'object-data-table', zzUndeclared7353: 1 }; - expect([wellFormed.type, malformed.type, undeclared.zzUndeclared7353]).toEqual([ + expect([wellFormed.type, malformed.type, undeclared.type]).toEqual([ + 'object-data-table', 'object-data-table', 'object-data-table', - 1, ]); }); }); diff --git a/packages/types/src/__tests__/object-gallery-filter-9309.test.ts b/packages/types/src/__tests__/object-gallery-filter-9309.test.ts index b6cdf7e96e..79bc967fef 100644 --- a/packages/types/src/__tests__/object-gallery-filter-9309.test.ts +++ b/packages/types/src/__tests__/object-gallery-filter-9309.test.ts @@ -110,12 +110,16 @@ const galleryStringFilter: TsObjectGallerySchema = { type: 'object-gallery', fil const galleryNumberFilter: TsObjectGallerySchema = { type: 'object-gallery', filter: 42 }; /** - * The objectui#7927 ceiling, pinned rather than claimed away: `BaseSchema` ends - * in `[key: string]: any`, so a MISSPELLED key still resolves to `any` and - * still compiles. This card does not lift that, and this line is what stops - * anyone reading it as if it had. + * The objectui#7927 ceiling was pinned here rather than claimed away: while + * `BaseSchema` ended in `[key: string]: any`, a MISSPELLED key resolved to `any` + * and compiled. objectui#8347 removed the signature, so the misspelling is no + * member, and a fresh literal carrying it is refused. */ -export type _MisspellingStillAdmitted = Expect>; +export type _MisspellingIsNoMember = Expect>; +// Lit control for the detector: `IsAny` does answer `true`, so `_GalleryFilterIsNotAny` is a reading. +export type _IsAnyCanAnswerTrue = Expect>; +// @ts-expect-error — `filtr` is no member of `ObjectGallerySchema`; the key is `filter` +const galleryMisspelledFilter: TsObjectGallerySchema = { type: 'object-gallery', filtr: 'stage=won' }; /* ── The mirror, at runtime ───────────────────────────────────────────────── */ @@ -239,5 +243,6 @@ describe('objectui#9309 — the type-face literals above are real', () => { expect(galleryRecordArm.type).toBe('object-gallery'); expect(galleryStringFilter.type).toBe('object-gallery'); expect(galleryNumberFilter.type).toBe('object-gallery'); + expect(galleryMisspelledFilter.type).toBe('object-gallery'); }); }); diff --git a/packages/types/src/__tests__/object-grid-operators-tombstone-9739.test.ts b/packages/types/src/__tests__/object-grid-operators-tombstone-9739.test.ts index 257243d2a4..75ef79a763 100644 --- a/packages/types/src/__tests__/object-grid-operators-tombstone-9739.test.ts +++ b/packages/types/src/__tests__/object-grid-operators-tombstone-9739.test.ts @@ -162,24 +162,28 @@ describe('the UPSTREAM half — the remedy is the protocol’s, re-derived from * ⛔ `operators` is NOT declared on `ObjectGridSchema` — letter C says so in as * many words, and letter A (declaring it) was the option refused. * - * The absence of a `@ts-expect-error` on the literal below is the measurement, - * not an omission: `ObjectGridSchema` extends `BaseSchema`, whose index - * signature absorbs the key as `any`, so the compiler stays silent. Adding a - * directive here would redden this package's `tsc -p tsconfig.test.json` leg as - * unused — which is exactly how this claim fails if anyone ever declares the - * key on the twin, or if the twin loses that index signature. + * While `ObjectGridSchema` extended a `BaseSchema` carrying an index + * signature, the literal below compiled — the signature absorbed the key as + * `any` — and the absence of a directive on it was the measurement. This + * docblock named the signature's removal as one way that claim would fail. + * objectui#8347 removed it, so the compiler refuses the key too now, as an + * UNDECLARED key with no remedy named; `_OperatorsIsNoMember` keeps the + * "not declared" half measurable, since a refusal alone cannot tell an + * undeclared key from a `?: never` tombstone. * - * ⚠️ This is the asymmetry the tombstone deliberately creates and does not - * hide: the VALIDATOR now refuses the document the COMPILER still absorbs. The - * ruling accepted it — narrowing the mirror is the protocol-aligned direction, - * and widening the interface to match would have written the misspelling into - * the published TypeScript face. + * ⚠️ The asymmetry the tombstone created — the VALIDATOR refused the document + * the COMPILER absorbed — is closed by the removal, not by widening the + * interface: declaring the key would have written the misspelling into the + * published TypeScript face, which the ruling refused. */ -export const authoredOperatorsStillCompiles: ObjectGridSchemaType = { +export const authoredOperatorsIsRefused: ObjectGridSchemaType = { type: 'object-grid', objectName: 'probe', + // @ts-expect-error — `operators` is not declared on the twin (letter C); since objectui#8347 it is refused as undeclared operators: AUTHORED_MISSPELLING, }; +export type _OperatorsIsNoMember = 'operators' extends keyof ObjectGridSchemaType ? never : true; +const operatorsIsNoMember: _OperatorsIsNoMember = true; /** LIT CONTROL: the spelling the tombstone names is a DECLARED member of the twin. */ export const authoredOperationsIsDeclared: ObjectGridSchemaType = { @@ -190,6 +194,6 @@ export const authoredOperationsIsDeclared: ObjectGridSchemaType = { describe('the TypeScript twin', () => { it('is compiled by this package’s type-check leg, which is where the two bindings above are read', () => { - expect([authoredOperatorsStillCompiles, authoredOperationsIsDeclared]).toHaveLength(2); + expect([authoredOperatorsIsRefused, authoredOperationsIsDeclared, operatorsIsNoMember]).toHaveLength(3); }); }); diff --git a/packages/types/src/__tests__/object-kanban-allow-collapse-retired-8801.test.ts b/packages/types/src/__tests__/object-kanban-allow-collapse-retired-8801.test.ts index 2a298a0871..0dc583d149 100644 --- a/packages/types/src/__tests__/object-kanban-allow-collapse-retired-8801.test.ts +++ b/packages/types/src/__tests__/object-kanban-allow-collapse-retired-8801.test.ts @@ -82,9 +82,12 @@ type _RetiredIsNotAny = Assert>; -// …and the never-declared near miss DOES fall through to `any`, which is what -// the retired key would look like had it been deleted rather than tombstoned. -type _NearMissFallsThroughToIndexSignature = Assert>; +// …and the never-declared near miss is no member at all, which is what the +// retired key would look like had it been deleted rather than tombstoned (it +// fell through to `any` until objectui#8347 removed `BaseSchema`'s signature). +type _NearMissIsNoMember = Assert>; +// Lit control for the detector: `IsAny` does answer `true`, so `_RetiredIsNotAny` is a reading. +type _IsAnyCanAnswerTrue = Assert>; // The TS face still accepts a live board… // (`quickAdd` stood in this literal until it was retired on this arm too — diff --git a/packages/types/src/__tests__/object-kanban-group-by-limit-7322.test.ts b/packages/types/src/__tests__/object-kanban-group-by-limit-7322.test.ts index c1c9214bd3..00b8a6b90f 100644 --- a/packages/types/src/__tests__/object-kanban-group-by-limit-7322.test.ts +++ b/packages/types/src/__tests__/object-kanban-group-by-limit-7322.test.ts @@ -190,10 +190,13 @@ export type _LimitIsOptional = Expect> // pin red, which is the point: the tombstone is load-bearing. export type _GroupFieldIsTombstone = Expect>; export type _GroupFieldIsNotAny = Expect, false>>; -// The control key is NOT declared: it resolves to `any` through the index -// signature, exactly as `groupBy` and `limit` did before this card. Declaring -// it turns this red — which is the point. -export type _ControlKeyFallsThroughToIndexSignature = Expect>; +// The control key is NOT declared: it resolved to `any` through the index +// signature, exactly as `groupBy` and `limit` did before this card, until +// objectui#8347 removed the signature; it is no member now. Declaring it turns +// this red — which is the point. +export type _ControlKeyIsNoMember = Expect>; +// Lit control for the detector: `IsAny` does answer `true`, so the `IsNotAny` lines are readings. +export type _IsAnyCanAnswerTrue = Expect>; // The TS face accepts the documented shape on a literal — the exact node the // doc page's row-cap block now annotates. diff --git a/packages/types/src/__tests__/object-kanban-on-quick-add-retired-11234.test.ts b/packages/types/src/__tests__/object-kanban-on-quick-add-retired-11234.test.ts index 717e87576d..f4302ee066 100644 --- a/packages/types/src/__tests__/object-kanban-on-quick-add-retired-11234.test.ts +++ b/packages/types/src/__tests__/object-kanban-on-quick-add-retired-11234.test.ts @@ -76,8 +76,11 @@ export type _RetiredIsNotAny = Assert, false>>; // Non-vacuity on the same instruments: the sibling slot is still callable… export type _LiveSlotStillCallable = Assert>; -// …and the never-declared near miss does fall through to `any`. -export type _NearMissFallsThroughToIndexSignature = Assert>; +// …and the never-declared near miss is no member at all (it fell through to +// `any` until objectui#8347 removed `BaseSchema`'s index signature). +export type _NearMissIsNoMember = Assert>; +// Lit control for the detector: `IsAny` does answer `true`, so `_RetiredIsNotAny` is a reading. +export type _IsAnyCanAnswerTrue = Assert>; const liveLiteral: TsObjectKanbanSchema = { ...NODE, onCardClick: () => undefined }; // This directive goes unused — and the type-check goes red with TS2578 — the diff --git a/packages/types/src/__tests__/object-kanban-quick-add-retired-8285.test.ts b/packages/types/src/__tests__/object-kanban-quick-add-retired-8285.test.ts index e23750ec41..73699b1420 100644 --- a/packages/types/src/__tests__/object-kanban-quick-add-retired-8285.test.ts +++ b/packages/types/src/__tests__/object-kanban-quick-add-retired-8285.test.ts @@ -61,8 +61,11 @@ type _RetiredIsTombstone = Assert, false>>; // Non-vacuity on the same instrument: a live member resolves to its own type… type _LiveMemberStillDeclared = Assert>; -// …and the never-declared near miss does fall through to `any`. -type _NearMissFallsThroughToIndexSignature = Assert>; +// …and the never-declared near miss is no member at all (it fell through to +// `any` until objectui#8347 removed `BaseSchema`'s index signature). +type _NearMissIsNoMember = Assert>; +// Lit control for the detector: `IsAny` does answer `true`, so `_RetiredIsNotAny` is a reading. +type _IsAnyCanAnswerTrue = Assert>; const liveLiteral: TsObjectKanbanSchema = { ...NODE, coverImageField: 'cover' }; // This directive goes unused — and the type-check goes red with TS2578 — the diff --git a/packages/types/src/__tests__/object-tree-filter-9549.test.ts b/packages/types/src/__tests__/object-tree-filter-9549.test.ts index a4b21dab0d..f5a1f0dd58 100644 --- a/packages/types/src/__tests__/object-tree-filter-9549.test.ts +++ b/packages/types/src/__tests__/object-tree-filter-9549.test.ts @@ -62,8 +62,16 @@ const treeStringFilter: TsObjectTreeSchema = { type: 'object-tree', objectName: // @ts-expect-error — `filter` is `QueryParams['$filter']`; a number is neither arm const treeNumberFilter: TsObjectTreeSchema = { type: 'object-tree', objectName: 'account', filter: 42 }; -/** The objectui#7927 ceiling is unchanged: a misspelled key still resolves to `any`. */ -export type _MisspellingStillAdmitted = Expect>; +/** + * The objectui#7927 ceiling — a misspelled key resolved to `any` — was pinned + * here; objectui#8347 removed `BaseSchema`'s index signature, so the misspelling + * is no member and a fresh literal carrying it is refused. + */ +export type _MisspellingIsNoMember = Expect>; +// @ts-expect-error — `filtr` is no member of `ObjectTreeSchema`; the key is `filter` +const treeMisspelledFilter: TsObjectTreeSchema = { type: 'object-tree', objectName: 'account', filtr: 'stage=won' }; +// Lit control for the detector: `IsAny` does answer `true`, so the `IsNotAny` lines are readings. +export type _IsAnyCanAnswerTrue = Expect>; /* ── The mirror, at runtime ───────────────────────────────────────────────── */ @@ -107,7 +115,7 @@ describe('objectui#9549 — the tree mirror refuses what the declaration refuses /* Keep the type-face literals referenced so `noUnusedLocals` cannot drop them. */ describe('objectui#9549 — the type-face literals above are real', () => { it('each literal builds a node', () => { - for (const n of [treeArrayArm, treeRecordArm, treeStringFilter, treeNumberFilter]) { + for (const n of [treeArrayArm, treeRecordArm, treeStringFilter, treeNumberFilter, treeMisspelledFilter]) { expect(n.type).toBe('object-tree'); } }); diff --git a/packages/types/src/__tests__/object-view-slot-key-lists.test.ts b/packages/types/src/__tests__/object-view-slot-key-lists.test.ts index 142d71274a..7f342a1e1b 100644 --- a/packages/types/src/__tests__/object-view-slot-key-lists.test.ts +++ b/packages/types/src/__tests__/object-view-slot-key-lists.test.ts @@ -93,17 +93,20 @@ * * ## 🗑️ Removal condition (recorded at triage's request) * - * These `Pick` lists exist because `BaseSchema` carries a root string index - * signature. When an objectui#5155 phase removes it, `keyof ObjectGridSchema` - * becomes the literal member union again and `Omit` stops collapsing. + * These `Pick` lists exist because `BaseSchema` carried a root string index + * signature. objectui#8347 removed it (the objectui#5155 phase this condition + * waited for), so `keyof ObjectGridSchema` is the literal member union again + * and `Omit` no longer collapses. * `ObjectFormSlotKey` and the `form` half of this file then become removable in * favour of the original `Omit` form. The `table` half does NOT: since * objectui#10976 its list is a deliberate subset, so an `Omit` would have to * name the withheld set, and what this file guards for `table` — that every * grid member is either handed to the grid or withheld, and that both faces * withhold the same keys — stays true work. `declaresStringIndex` below is the - * tripwire that will notice the index signature going: when it reports `false` - * for the source schemas, the collapse half of this file is gone. + * tripwire that noticed the index signature going: it reports `false` for the + * source schemas since objectui#8347, so the collapse half of this file is gone + * and the `form` half may move back to `Omit` (not done here: the removal PR + * flipped the tripwires and left the lists, which still hold). */ import { describe, it, expect, afterAll } from 'vitest'; @@ -262,20 +265,22 @@ function slotType(slot: 'table' | 'form'): ts.Type { */ describe('the source schemas still declare their full member sets', () => { // 64 since objectui#10872 batch 9 declared `responsiveStyles` (withheld from the slot, above). - it('ObjectGridSchema declares 64 members and carries the #5155 index signature', () => { + it('ObjectGridSchema declares 64 members and no longer carries the #5155 index signature', () => { const grid = exportedType('ObjectGridSchema'); expect(memberNames(grid)).toHaveLength(64); expect(memberNames(grid)).toEqual(expect.arrayContaining(['columns', 'pageSize', 'rowActions'])); - // When this flips to `false`, objectui#5155 has removed the root index - // signature and the `Pick` lists this file pins become removable. - expect(declaresStringIndex(grid)).toBe(true); + // Flipped to `false` by objectui#8347, which removed the root index + // signature (the objectui#5155 phase this tripwire waited for): the `form` + // `Pick` list this file pins is removable now (see the removal condition + // in the header), and the `table` one stays deliberate. + expect(declaresStringIndex(grid)).toBe(false); }); - it('ObjectFormSchema declares 69 members and carries the #5155 index signature', () => { + it('ObjectFormSchema declares 69 members and no longer carries the #5155 index signature', () => { const form = exportedType('ObjectFormSchema'); expect(memberNames(form)).toHaveLength(69); expect(memberNames(form)).toEqual(expect.arrayContaining(['fields', 'sections', 'submitText'])); - expect(declaresStringIndex(form)).toBe(true); + expect(declaresStringIndex(form)).toBe(false); }); }); diff --git a/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts b/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts index 612fc16368..3d75d89031 100644 --- a/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts +++ b/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts @@ -573,9 +573,12 @@ export type _ViewTabBarIsNotAny = Expect | undefined>, false>>; export type _ListViewsIsTheSpecRecord = Expect> | undefined>>; -// The control key is NOT declared: it resolves to `any` through the index -// signature, exactly as the ten did on the zod side before this card. -export type _ControlKeyFallsThroughToIndexSignature = Expect>; +// The control key is NOT declared: it resolved to `any` through the index +// signature, exactly as the ten did on the zod side before this card, until +// objectui#8347 removed the signature; it is no member now. +export type _ControlKeyIsNoMember = Expect>; +// Lit control for the detector: `IsAny` does answer `true`, so the `IsNotAny` lines are readings. +export type _IsAnyCanAnswerTrue = Expect>; // The TS face accepts the documented shape on a literal. export const literal: TsObjectViewSchema = { ...NODE, ...(ACCEPTED as Record) }; diff --git a/packages/types/src/__tests__/partial-schema-collapse-pin.test.ts b/packages/types/src/__tests__/partial-schema-collapse-pin.test.ts deleted file mode 100644 index d432e34502..0000000000 --- a/packages/types/src/__tests__/partial-schema-collapse-pin.test.ts +++ /dev/null @@ -1,377 +0,0 @@ -// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. - -/** - * `PartialSchema` declares ONE property for every instantiation — pinned, not - * repaired (objectui#6397). - * - * ## What this file asserts, and why it is a PIN rather than a fix - * - * `packages/types/src/index.ts` exports: - * - * export type PartialSchema = { - * type: T['type']; - * } & Partial>; - * - * Its doc comment promises "all properties optional except the type". It does - * not deliver that. Every instantiation declares exactly ONE property — `type` - * — and carries a live `[key: string]: any`, so it accepts anything. Measured - * through the checker against the emitted `index.d.ts`, the same instrument - * that produced objectui#6269's 61 -> 0 reading: - * - * PartialSchema -> 1 declared property: type (source: 61) - * PartialSchema -> 1 declared property: type (source: 67) - * PartialSchema -> 1 declared property: type (source: 42) - * PartialSchema -> 1 declared property: type (source: 27) - * - * ## The mechanism — objectui#6151 / #6269, third position - * - * `Omit` is `Pick>`, and `keyof T` on a type - * carrying a string index signature is `string | number` — the literal member - * names are ABSORBED. Every `T extends BaseSchema` inherits `BaseSchema`'s - * `[key: string]: any` (objectui#5155), so `Partial>` rebuilds a - * type holding the index signature and none of the named members. The explicit - * `{ type: T['type'] }` half of the intersection is the only reason the count is - * 1 and not 0. - * - * objectui#6151 fixed this collapse in a HERITAGE clause and #6269 fixed it in - * PROPERTY position. Neither of their guards can see this one: #6151's walks the - * `LayoutSchema` union, #6269's reads `ObjectViewSchema`'s two slot properties, - * and this is a GENERIC MAPPED-TYPE ALIAS with no union membership and no - * declaring property. Hence a third pin. - * - * ## Why a pin and not a repair - * - * ⛔ Do not "fix" the declaration here, and do not tag it `@deprecated`. Triage - * ruled (objectui#6397, 2026-08-25 re-judgement): - * - * - Retiring the alias is a REMOVAL OF A PUBLISHED EXPORT of - * `@object-ui/types` — a breaking removal of published capability, which - * sits on the human floor. Not a dev's call, and not this file's business. - * - Repairing it in place was read as not available: `T` is generic, so there - * is no literal key list to `Pick` (the escape #6269 used for its two - * concrete schemas), and the triage held that every generic re-spelling — - * `{ [K in keyof T as K extends 'type' ? never : K]?: T[K] }` included — - * collapses for the same `keyof T` reason. - * ⚠️ That last reading is FALSE. A - * key-remapping mapped type iterates the named members and the index - * signature separately, so it keeps the named members; `OmitDeclared` in - * `../form.ts` is that spelling, and objectui#9256's E3 slice repaired two - * published faces with it (pinned in - * `content-channel-e3-residual-9256.test.ts`). `PartialSchema`'s own - * docblock in `../index.ts` records why the alias is still left as - * written; this file's assertions are about the alias as written and do - * not move. - * - Once objectui#5155 removes the root index signature, the alias starts - * working AS WRITTEN with no edit at all, and the removal question - * dissolves. - * - * So the one state that must not survive is "declared, published, collapsed, - * and UNPINNED" — a published type that promises - * `{ type } & everything-else-optional` and delivers `{ type } & any`, ready for - * the next consumer to adopt as protection it does not provide. This file is - * what removes the "unpinned". - * - * ## 🗑️ Removal condition — the objectui#5155 sequencing note - * - * ⭐ THIS FILE IS EXPECTED TO GO RED WHEN objectui#5155 LANDS. That is the - * point, not a regression. When a #5155 phase drops `[key: string]: any` from - * `BaseSchema`, `keyof T` becomes the literal member union again, `Omit` stops - * absorbing names, and `PartialSchema` jumps from 1 declared - * property to 61. Section 3's tripwire (`BaseSchema` still declares a string - * index) is the flag that says which side of that transition you are on. - * - * The correct response to that red is NOT to loosen these assertions. It is to - * delete this file and, if `PartialSchema` is still unconsumed at that point, - * file a fresh enforce-or-remove card for the maintainer — removal of a - * published export stays on the human floor either way. - * - * ## Why it emits its own declarations instead of reading `dist/` - * - * This repo's per-PR `test` job runs `pnpm test` with NO build ahead of it - * (turbo's `test` task only `dependsOn: ["^build"]` — the DEPENDENCY closure, - * never the package's own build), and `packages/types` has no workspace - * dependencies, so nothing builds it. A guard reading `dist/index.d.ts` would be - * absent-or-stale on a cold CI cache — vacuous exactly where it is needed. So - * this file runs the package's OWN tsconfig through the compiler API and - * measures the emitted declaration: the artifact a consumer resolves, derived - * deterministically and with no dependence on CI job ordering. Same reasoning as - * `stack-schema-emitted-members.test.ts`, `object-view-slot-key-lists.test.ts` - * and `package-exports-manifest.test.ts`. - * - * ## Census (objectui#6397, re-run on this branch) - * - * `PartialSchema` has ZERO in-repo instantiations. A grep across `packages/`, - * `apps/`, `examples/`, `content/`, `docs/` and `scripts/` (excluding - * `node_modules` and `dist`) finds exactly ONE occurrence of the identifier: the - * declaration itself at `packages/types/src/index.ts`. There is no call site to - * pin against, so the four types measured below are chosen deliberately — they - * are the four the card itself measured, they are real published schemas from - * four different modules of this package (`objectql`, `objectql`, `objectql`, - * `base`-derived form/action families), and they are exactly the population an - * "editor partial schema" alias would be pointed at. The synthetic type appears - * only in section 1, where it is the CONTROL, never the subject. - */ - -import { describe, it, expect, afterAll } from 'vitest'; -import { mkdtempSync, rmSync, existsSync, writeFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import ts from 'typescript'; - -const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); - -/** - * Emit declarations with the package's OWN build settings, into a scratch dir - * under `node_modules/` — which is gitignored, and from which Node's module - * resolution still walks up to `packages/types/node_modules`, so the emitted - * `import type … from '@objectstack/spec/ui'` still resolves. - */ -function emitDeclarations(): { dir: string; index: string } { - const configPath = join(packageRoot, 'tsconfig.json'); - const readConfig = ts.readConfigFile(configPath, ts.sys.readFile); - if (readConfig.error) { - throw new Error(ts.flattenDiagnosticMessageText(readConfig.error.messageText, '\n')); - } - const parsed = ts.parseJsonConfigFileContent(readConfig.config, ts.sys, packageRoot); - - const dir = mkdtempSync(join(packageRoot, 'node_modules', '.partial-schema-pin-')); - const program = ts.createProgram([join(packageRoot, 'src', 'index.ts')], { - ...parsed.options, - outDir: dir, - declaration: true, - emitDeclarationOnly: true, - declarationMap: false, - noEmit: false, - // The real build is `composite`/incremental; neither is meaningful for a - // one-shot emit into a scratch dir, and both would write build info next to - // the package's real artifacts. - composite: false, - incremental: false, - tsBuildInfoFile: undefined, - }); - const emitted = program.emit(); - const index = join(dir, 'index.d.ts'); - if (!existsSync(index)) { - const diagnostics = [...emitted.diagnostics, ...program.getSemanticDiagnostics()] - .map((d) => ts.flattenDiagnosticMessageText(d.messageText, '\n')) - .slice(0, 10); - throw new Error(`declaration emit produced no index.d.ts:\n${diagnostics.join('\n')}`); - } - return { dir, index }; -} - -const { dir: scratchDir, index: emittedIndex } = emitDeclarations(); -afterAll(() => rmSync(scratchDir, { recursive: true, force: true })); - -/** - * `PartialSchema` is a GENERIC alias: there is nothing to measure until it is - * instantiated, and this repo instantiates it nowhere (see the census above). - * So the probe below is where the instantiations are made — a source file - * written NEXT TO the emitted `index.d.ts`, resolving `./index` to it, so every - * type read here is read out of the artifact a consumer would resolve. - * - * `IndexFreeSchema` is the CONTROL and only the control: a `T` whose `keyof` - * resolves to its literal member names because it declares no index signature. - * It is the shape `PartialSchema`'s author was writing for. - */ -const PROBE_SOURCE = ` -import type { - PartialSchema, - ObjectGridSchema, - ObjectFormSchema, - ObjectViewSchema, - ButtonSchema, - BaseSchema, -} from './index'; - -export type SourceGrid = ObjectGridSchema; -export type SourceForm = ObjectFormSchema; -export type SourceView = ObjectViewSchema; -export type SourceButton = ButtonSchema; -export type SourceBase = BaseSchema; - -export type PartialGrid = PartialSchema; -export type PartialForm = PartialSchema; -export type PartialView = PartialSchema; -export type PartialButton = PartialSchema; - -/** The control: no index signature, so \`keyof\` resolves to literal names. */ -export type IndexFreeSchema = { - type: 'index_free_control'; - alpha?: number; - beta?: string; - gamma?: boolean; -}; -export type PartialIndexFree = PartialSchema; -`; - -const probeFile = join(scratchDir, '__partial-schema-probe.ts'); -writeFileSync(probeFile, PROBE_SOURCE, 'utf8'); - -const program = ts.createProgram([probeFile], { - noEmit: true, - skipLibCheck: true, - strict: true, - target: ts.ScriptTarget.ESNext, - module: ts.ModuleKind.ESNext, - moduleResolution: ts.ModuleResolutionKind.Bundler, -}); -const checker = program.getTypeChecker(); - -function probeExports(): Map { - const sourceFile = program.getSourceFile(probeFile); - const moduleSymbol = sourceFile && checker.getSymbolAtLocation(sourceFile); - if (!moduleSymbol) throw new Error(`no module symbol for ${probeFile}`); - return new Map(checker.getExportsOfModule(moduleSymbol).map((s) => [s.getName(), s])); -} - -function probeType(name: string): ts.Type { - const symbol = probeExports().get(name); - if (!symbol) throw new Error(`${name} is not exported from the probe`); - return checker.getDeclaredTypeOfSymbol(symbol); -} - -/** The property names a type DECLARES — not the keys its index signature answers. */ -const declaredMembers = (name: string): string[] => - checker.getPropertiesOfType(probeType(name)).map((p) => p.getName()).sort(); - -const declaresStringIndex = (name: string): boolean => - checker.getIndexInfoOfType(probeType(name), ts.IndexKind.String) !== undefined; - -/* ── 0. Non-vacuity — the probe really resolved the emitted declaration ───── */ - -/** - * ⚠️ Load-bearing. Section 2 asserts a ONE-element member set; if the probe had - * failed to resolve `./index`, or the emit had collapsed, every type here would - * read as an empty or `any` shape and section 2 would pass over nothing. These - * are what makes that impossible. - */ -describe('the probe resolved the emitted index.d.ts (guards everything below)', () => { - it('reports no semantic errors — every import in the probe resolved', () => { - const errors = program - .getSemanticDiagnostics(program.getSourceFile(probeFile)) - .map((d) => ts.flattenDiagnosticMessageText(d.messageText, '\n')); - expect(errors).toEqual([]); - }); - - it('read the FRESHLY EMITTED index.d.ts, not `dist/` or a node_modules copy', () => { - // `./index` from the probe must resolve to the scratch emit above. If it - // ever resolved elsewhere, every number below would describe some other - // artifact — possibly a stale one — while still looking like a measurement. - expect(program.getSourceFiles().map((f) => f.fileName)).toContain(emittedIndex); - }); - - it.each([ - { name: 'SourceGrid', min: 20, sample: ['columns', 'pageSize', 'rowActions'] }, - { name: 'SourceForm', min: 20, sample: ['fields', 'sections', 'submitText'] }, - { name: 'SourceView', min: 20, sample: ['objectName', 'table', 'form'] }, - { name: 'SourceButton', min: 20, sample: ['buttonType', 'className', 'type'] }, - ])('$name declares its full member set before any Omit touches it', ({ name, min, sample }) => { - // Counts at the time of the pin: 61 / 67 / 42 / 27. Asserted as a floor plus - // named members rather than an exact number, so adding a member to a schema - // does not have to be re-declared here — the collapse in section 2 is what - // this file owns, not the census of these schemas (that is #6269's job for - // grid and form). - expect(declaredMembers(name).length).toBeGreaterThan(min); - expect(declaredMembers(name)).toEqual(expect.arrayContaining(sample)); - }); -}); - -/* ── 1. The control — the alias body is fine; the COLLAPSE is about `keyof T` ─ */ - -/** - * ⭐ This is the ghost-assertion guard. "Declares exactly `['type']`" would be a - * decoration if the instrument answered `['type']` for everything, or if - * `PartialSchema` were incapable of declaring more than one property in any - * circumstance. It is not: the SAME alias, measured by the SAME checker, - * declares all four members of a `T` whose `keyof` resolves to literal names. - * - * Read the two together and the mechanism is unambiguous: - * - * PartialSchema -> 4 declared, no index signature (intended) - * PartialSchema -> 1 declared, index signature live (actual) - * - * That is also exactly what `PartialSchema` becomes once - * objectui#5155 removes `BaseSchema`'s index signature. - */ -describe('the intended reading — `PartialSchema` over an index-signature-free T', () => { - it('declares `type` PLUS every other member, each optional', () => { - expect(declaredMembers('PartialIndexFree')).toEqual(['alpha', 'beta', 'gamma', 'type']); - }); - - it('declares no string index signature — excess-property checks still fire', () => { - expect(declaresStringIndex('PartialIndexFree')).toBe(false); - }); - - it('keeps everything but `type` optional (the `Partial` wrapper did its job)', () => { - const optional = checker - .getPropertiesOfType(probeType('PartialIndexFree')) - .filter((p) => (p.getFlags() & ts.SymbolFlags.Optional) !== 0) - .map((p) => p.getName()) - .sort(); - expect(optional).toEqual(['alpha', 'beta', 'gamma']); - }); - - it('matches the control type it was derived from, member for member', () => { - // Same population — so section 2's `['type']` cannot be blamed on the probe. - expect(declaredMembers('PartialIndexFree')).toEqual(declaredMembers('IndexFreeSchema')); - }); -}); - -/* ── 2. The measurement — every real instantiation collapses to `type` ────── */ - -describe.each([ - { alias: 'PartialGrid', source: 'SourceGrid' }, - { alias: 'PartialForm', source: 'SourceForm' }, - { alias: 'PartialView', source: 'SourceView' }, - { alias: 'PartialButton', source: 'SourceButton' }, -])('$alias collapses to one declared property (objectui#6397)', ({ alias, source }) => { - it('declares EXACTLY `["type"]`, against a source that declares 20+ members', () => { - // ⛔ If this goes red with a LARGER set, objectui#5155 has landed and the - // alias now works as written — see this file's removal condition. Do not - // widen the assertion; delete the file. - expect(declaredMembers(alias)).toEqual(['type']); - expect(declaredMembers(source).length).toBeGreaterThan(20); - }); - - it('drops every named member the source declared apart from `type`', () => { - const lost = declaredMembers(source).filter((m) => m !== 'type'); - expect(lost.length).toBeGreaterThan(20); - for (const member of lost) expect(declaredMembers(alias)).not.toContain(member); - }); - - it('carries a live string index signature — it accepts ANY key, at `any`', () => { - // This is the second half of the harm and the reason the alias is not - // protection: nothing errors, because every absent key is answered `any`. - expect(declaresStringIndex(alias)).toBe(true); - }); - - it('still discriminates on `type` — the intersection’s explicit half survives', () => { - const typeProp = checker.getPropertyOfType(probeType(alias), 'type'); - expect(typeProp).toBeDefined(); - // A literal type here (not `string`) is why the count is 1 rather than 0. - expect(checker.typeToString(checker.getTypeOfSymbol(typeProp!))).not.toBe('any'); - }); -}); - -/* ── 3. The objectui#5155 sequencing tripwire ────────────────────────────── */ - -describe('the collapse is caused by BaseSchema’s root index signature (objectui#5155)', () => { - it('BaseSchema still declares `[key: string]: any`', () => { - // When this flips to `false`, objectui#5155 has removed the root index - // signature: `keyof T` resolves to literal member names again, `Omit` stops - // absorbing them, `PartialSchema` starts working as written with no edit, - // and section 2 above inverts. That is the sequencing this card pinned — - // the response is to DELETE this file, not to relax it, and the retirement - // question of objectui#6397 becomes a fresh enforce-or-remove card for the - // maintainer (removal of a published export sits on the human floor). - expect(declaresStringIndex('SourceBase')).toBe(true); - }); - - it.each(['SourceGrid', 'SourceForm', 'SourceView', 'SourceButton'])( - '%s inherits that index signature, which is what absorbs its member names', - (name) => { - expect(declaresStringIndex(name)).toBe(true); - }, - ); -}); diff --git a/packages/types/src/__tests__/twins-spec-by-reference-9736.test.ts b/packages/types/src/__tests__/twins-spec-by-reference-9736.test.ts index 5f31bf2698..a7d76d99b6 100644 --- a/packages/types/src/__tests__/twins-spec-by-reference-9736.test.ts +++ b/packages/types/src/__tests__/twins-spec-by-reference-9736.test.ts @@ -84,9 +84,10 @@ describe('admitted spec keys land on the twin with the SPEC\'S type, not `any`', const pageSource: Equal = true; const pageRequires: Equal = true; const pageInterfaceConfig: Equal = true; - // Control: an undeclared key reads as `any` through the index signature, and - // `Equal` tells `any` apart from a real member type. - const anyControl: Equal = false; + // Control: `Equal` tells `any` apart from a real member type. (An undeclared + // key read `any` through the index signature until objectui#8347 removed it; + // the control reads `any` directly now.) + const anyControl: Equal = false; expect([lock, appProtection, defaultAgent, dashProtection, dashPackageId, pageSource, pageRequires, pageInterfaceConfig, anyControl]) .toEqual([true, true, true, true, true, true, true, true, false]); }); diff --git a/packages/types/src/__tests__/widget-schema-anchors-6576.test.ts b/packages/types/src/__tests__/widget-schema-anchors-6576.test.ts index 7ea8dce908..2d95989d1f 100644 --- a/packages/types/src/__tests__/widget-schema-anchors-6576.test.ts +++ b/packages/types/src/__tests__/widget-schema-anchors-6576.test.ts @@ -156,13 +156,16 @@ describe('ObjectGallerySchema / ObjectDataTableSchema — compile-time pins (obj expect(typeof table.onRowClick).toBe('function'); }); - it('still accepts a MISSPELLING — the ceiling, pinned honestly (objectui#5155)', () => { - // Counter-probe against reading the anchor as more than it is. `BaseSchema`'s - // `[key: string]: any` is inherited, so `visibleWhn` compiles on both. The - // ruling accepted this cost; closing it is objectui#5155, not this card. + it('refuses a MISSPELLING — the ceiling this row pinned is gone (objectui#8347)', () => { + // This row was the counter-probe against reading the anchor as more than it + // was: `BaseSchema`'s inherited `[key: string]: any` let `visibleWhn` compile + // on both, and closing that was left to the removal of the signature, which + // landed as objectui#8347. + // @ts-expect-error — `visibleWhn` is no member; the key is `visibleWhen` const gallery: ObjectGallerySchema = { type: 'object-gallery', visibleWhn: 'typo' }; + // @ts-expect-error — `visibleWhn` is no member; the key is `visibleWhen` const table: ObjectDataTableSchema = { type: 'object-data-table', visibleWhn: 'typo' }; - expect([gallery.visibleWhn, table.visibleWhn]).toEqual(['typo', 'typo']); + expect([gallery.type, table.type]).toEqual(['object-gallery', 'object-data-table']); }); }); diff --git a/packages/types/src/__tests__/widget-schema-anchors-7946.test.ts b/packages/types/src/__tests__/widget-schema-anchors-7946.test.ts index 36632c22a0..f20c55f8d0 100644 --- a/packages/types/src/__tests__/widget-schema-anchors-7946.test.ts +++ b/packages/types/src/__tests__/widget-schema-anchors-7946.test.ts @@ -178,9 +178,12 @@ describe('ObjectChartSchema — compile-time pins (objectui#7946)', () => { expect([chart.aggregate?.groupBy, chart.xAxisKey, first && 'dataKey' in first ? first.dataKey : undefined]).toEqual(['stage', 'stage', 'amount']); }); - it('still accepts a MISSPELLING — the ceiling, pinned honestly (objectui#5155)', () => { + it('refuses a MISSPELLING — the ceiling this row pinned is gone (objectui#8347)', () => { + // `BaseSchema`'s inherited `[key: string]: any` let `xAxisKy` compile until + // objectui#8347 removed it. + // @ts-expect-error — `xAxisKy` is no member; the key is `xAxisKey` const chart: ObjectChartSchema = { type: 'object-chart', chartType: 'bar', xAxisKy: 'stage' }; - expect(chart.xAxisKy).toBe('stage'); + expect(chart.type).toBe('object-chart'); }); }); diff --git a/packages/types/src/__tests__/wrapper-class-declared-7722.test.ts b/packages/types/src/__tests__/wrapper-class-declared-7722.test.ts index 7df0286784..4eed15c6a2 100644 --- a/packages/types/src/__tests__/wrapper-class-declared-7722.test.ts +++ b/packages/types/src/__tests__/wrapper-class-declared-7722.test.ts @@ -114,18 +114,22 @@ export type _SelectWrapperClassIsNotAny = Expect, string>>; export type _ListWrapperClassIsNotAny = Expect, false>>; -// The control key is NOT declared on any of the five: it resolves to `any` -// through the index signature, exactly as `wrapperClass` did before this card. -// Declaring it anywhere turns the matching line red — which is the point. -export type _SwitchControlFallsThrough = Expect>; -export type _TextareaControlFallsThrough = Expect>; -export type _DatePickerControlFallsThrough = Expect>; -export type _SelectControlFallsThrough = Expect>; -export type _ListControlFallsThrough = Expect>; - -// The TS face accepts the key on a literal. ⚠️ This is the WEAK half: the index -// signature would accept it undeclared too. The invariant pins above are the -// guard; these lines only show the declared spelling in use, once per type. +// The control key is NOT declared on any of the five: it resolved to `any` +// through the index signature, exactly as `wrapperClass` did before this card, +// until objectui#8347 removed the signature; it is no member now. Declaring it +// anywhere turns the matching line red — which is the point. +type NoMember = Equal; +export type _SwitchControlIsNoMember = Expect>; +export type _TextareaControlIsNoMember = Expect>; +export type _DatePickerControlIsNoMember = Expect>; +export type _SelectControlIsNoMember = Expect>; +export type _ListControlIsNoMember = Expect>; +// Lit control for the detector: `IsAny` does answer `true`, so the `IsNotAny` lines are readings. +export type _IsAnyCanAnswerTrue = Expect>; + +// The TS face accepts the key on a literal. Since objectui#8347 this is no +// longer the weak half: without the index signature an undeclared key is +// refused on a fresh literal, so each acceptance reads the declaration. const literals = { switch: { type: 'switch', label: 'Enabled', wrapperClass: 'gap-4' } satisfies TsSwitchSchema, textarea: { type: 'textarea', label: 'Notes', wrapperClass: 'gap-4' } satisfies TsTextareaSchema, diff --git a/packages/types/src/__tests__/zod-mirror-parity.test.ts b/packages/types/src/__tests__/zod-mirror-parity.test.ts index d260538e00..b7a52d6e94 100644 --- a/packages/types/src/__tests__/zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts @@ -154,7 +154,13 @@ * a delta to this number; count the registry. Nothing asserts it against a written * one, so this line is prose and can rot; the pin that cannot is the one * comparing the two halves to each other. - * - **50 entries** in `KnownDrift`, **87 keys** across them — 50 / 88 until + * - **55 entries** in `KnownDrift`, **92 keys** across them — 50 / 87 until + * objectui#8347 removed `BaseSchema`'s index signature, which ledgered FIVE + * new entries of one key each, all the INTERFACE-TO-PASSTHROUGH artifact + * (`app.zod.ts#AppSchemaRendererNodeSchema` and four `reports.zod.ts` + * pairs: a member typed with an `interface` against a `.passthrough()` + * mirror input, which an interface is never implicitly assignable to). It + * was 50 / 88 until * objectui#11514 dropped the component type from the TS * `DashboardWidgetSchema`'s `type`, once `plugin-dashboard` read slot entries * by the slot's element type: both faces name the same widget vocabulary, so @@ -876,7 +882,7 @@ * * ## KNOWN_DRIFT is a ratchet, not a waiver * - * 50 of the registered pairs carry TYPE drift TODAY (measured, not assumed). Each is + * 55 of the registered pairs carry TYPE drift TODAY (measured, not assumed). Each is * pinned to its EXACT drifted key set, so the entry fails when new drift appears on * that mirror AND when the recorded drift is fixed — a stale entry cannot rot * quietly. Correcting them is not one change: the pairs below split into DISJOINT @@ -2209,6 +2215,21 @@ interface KnownDrift { * `AIRecommendationsSchema` above. */ 'ai.zod.ts#NLQuerySchema': 'onSubmit'; + /** + * INTERFACE-TO-PASSTHROUGH (objectui#8347), an artifact of this ratchet's + * assignability test and ⛔ not a narrowing a document can hit. The member is a + * node type declared as an `interface`; its mirror is a `.passthrough()` object, + * whose `z.input` carries `[k: string]: unknown`. TypeScript grants an IMPLICIT + * index signature to anonymous and mapped object types and never to an + * interface, so an interface is not assignable to it. While `BaseSchema` + * carried `[key: string]: any` the declared member had an explicit signature + * and fit; objectui#8347 removed it. Measured at removal: a one-level anonymous + * copy of the declared type (`{ [P in keyof D]: D[P] }`) IS assignable to the + * same mirror input, so the residue is exactly the interface rule. The four + * `reports.zod.ts` entries below are the same cause. They leave if the + * operator learns the rule, or if the member stops being an interface. + */ + 'app.zod.ts#AppSchemaRendererNodeSchema': 'schema'; /** * RUNTIME SLOT (objectui#6124): `calendar-view`'s `pickHostCallbacks` reads * `onViewChange` off the spread props (function values only) and hands it to @@ -2676,6 +2697,22 @@ interface KnownDrift { 'overlay.zod.ts#PopoverSchema': 'onOpenChange'; /** RUNTIME SLOT (objectui#6124): the `sheet` renderer spreads leftover props onto the Radix `Sheet` (Dialog) root. */ 'overlay.zod.ts#SheetSchema': 'onOpenChange'; + /** + * INTERFACE-TO-PASSTHROUGH (objectui#8347): the same artifact as + * `app.zod.ts#AppSchemaRendererNodeSchema` above — each member is, or holds, + * a declared `interface` (`report` is `ReportComponentSchema`, `chart` is + * `ChartSchema`, and `sections` holds `ReportSection`s, whose `chart` is) + * against a `.passthrough()` mirror whose input demands a string index + * signature an interface never implicitly has. Measured at removal for + * `chart`: the declared type is refused, a one-level anonymous copy fits. + */ + 'reports.zod.ts#ReportBuilderSchema': 'report'; + /** INTERFACE-TO-PASSTHROUGH (objectui#8347): see `ReportBuilderSchema` above; `sections` carries the element. */ + 'reports.zod.ts#ReportComponentSchema': 'sections'; + /** INTERFACE-TO-PASSTHROUGH (objectui#8347): see `ReportBuilderSchema` above. */ + 'reports.zod.ts#ReportSectionSchema': 'chart'; + /** INTERFACE-TO-PASSTHROUGH (objectui#8347): see `ReportBuilderSchema` above. */ + 'reports.zod.ts#ReportViewerSchema': 'report'; /** * RUNTIME SLOT (objectui#7344): `detail-view` spreads the node's keys onto * `DetailView`, whose `handleBack` calls `onBack()`. The TS twin declared the diff --git a/packages/types/src/authoring-nodes.ts b/packages/types/src/authoring-nodes.ts index f9113fffd3..5326c2902d 100644 --- a/packages/types/src/authoring-nodes.ts +++ b/packages/types/src/authoring-nodes.ts @@ -14,8 +14,8 @@ * * ## Why these exist * - * `BaseSchema` carries `[key: string]: any`, so today every key on every node - * compiles. Once objectui#8347 removes it, a node may author only the keys some + * `BaseSchema` carried `[key: string]: any`, so every key on every node + * compiled. objectui#8347 removed it, so a node may author only the keys some * TypeScript declaration names. The nodes below had none: an `element:text` * with its `properties` bag, a `page:tabs` with `properties.items`, an * `action:button` with its executor keys in the bag (objectui#11183), and a @@ -85,13 +85,13 @@ * discriminates on the literal `type`: a node of one of these types has its own * keys judged wherever it is written, nested in a slot or at the prop, and its * required keys are required. A misspelled key inside an `element:text` bag, or - * a bag on a node type that declares none, is refused once objectui#8347 - * removes `BaseSchema`'s index signature (`authoring-nodes-11364.test.ts` and + * a bag on a node type that declares none, is refused now that objectui#8347 + * has removed `BaseSchema`'s index signature (`authoring-nodes-11364.test.ts` and * `node-slot-union-11466.test.ts` pin it). * - * Until that removal, the component-schema members that extend `BaseSchema` - * still carry its index signature, so a misspelled key on one of THEM still - * compiles. These members carry none. + * Before that removal, the component-schema members that extend `BaseSchema` + * carried its index signature, so a misspelled key on one of THEM compiled. + * These members never carried one, and since the removal neither do they. */ import type { z } from 'zod'; @@ -271,8 +271,8 @@ type FlexBlockArmShape = ClosedArmShape<(typeof FlexBlockSchema)['shape']>; * `__tests__/flex-bag-children-list-11564.test.ts`. Both faces judge an object * entry as a node of its own `type`: an undeclared `type` is refused by both, * and a key the node does not declare is refused by the strict zod face and, - * once objectui#8347 removes the index signature, by this type (until then the - * signature absorbs it on the node types that extend `BaseSchema`, as it does + * since objectui#8347 removed the index signature, by this type (before it the + * signature absorbed it on the node types that extend `BaseSchema`, as it did * for a single child). A primitive entry is admitted by both. They differ on * one entry kind: a nested array, which the walk passes through unvisited and * this type refuses, as the mirror does and as `renderChildren` diff --git a/packages/types/src/base.ts b/packages/types/src/base.ts index 6694f73171..8d060494bb 100644 --- a/packages/types/src/base.ts +++ b/packages/types/src/base.ts @@ -61,7 +61,21 @@ export type KeyedI18nLabel = { /** * Base schema interface that all component schemas extend. * This is the fundamental building block of the Object UI protocol. - * + * + * ⭐ It declares NO index signature (objectui#8347, executing the + * objectui#7927 ruling: the TypeScript face is a contract). A node literal + * written against a node type is excess-property checked, so a key no + * declaration names, a misspelling included, stops type-checking at the + * authoring site. When a key you need is refused, declare it on the node + * type that reads it, by reference to the spec row, or fix the producer + * (AGENTS.md #0.1); ⛔ do not cast past it or put the signature back. + * The bound: TypeScript checks excess keys only on a FRESH object literal, so + * a value that reached its annotation through a wider variable is not + * re-checked. `__tests__/base-schema-closed-face-8347.test.ts` pins both + * directions. Renderer props keep their tolerance: {@link ComponentRendererProps} + * still carries its own signature, and the zod mirror is still + * `.passthrough()` on the rendering face. + * * @example * ```typescript * const schema: BaseSchema = { @@ -313,10 +327,12 @@ export interface BaseSchema { * * ## Why it is refused by name rather than merely deleted * - * `BaseSchema` carries an index signature on the TS side and * `BaseSchemaCore` ends `.passthrough()` on the zod side, so DELETING a - * member does not refuse it — it makes it silently acceptable and - * silently inert, which is the state this retirement exists to leave. The + * member does not refuse it there — it makes it silently acceptable and + * silently inert, which is the state this retirement exists to leave. (The + * TS side carried an index signature with the same effect when this was + * written; objectui#8347 removed it, so a deleted member is refused at the + * authoring site now, but the parse still needs the tombstone.) The * `?: never` twin of the mirror's `aliasKeyRefusal` is what turns the * removal into an answer: `tsc` refuses the key at the authoring site and * the parse names `children` as the remedy. @@ -606,12 +622,6 @@ export interface BaseSchema { * @example { key: 'dialog.close', defaultValue: 'Close dialog' } */ ariaLabel?: string | KeyedI18nLabel; - - /** - * Additional properties specific to the component type. - * This index signature allows type-specific extensions. - */ - [key: string]: any; } /** diff --git a/packages/types/src/complex.ts b/packages/types/src/complex.ts index bb81b4c587..66b0911042 100644 --- a/packages/types/src/complex.ts +++ b/packages/types/src/complex.ts @@ -2387,7 +2387,7 @@ export interface DashboardWidgetSchema * inherited, not restated. Until objectui#11467 the five reached the compiler * only through `BaseSchema`'s `[key: string]: any`, so each was `any` here and * the README's `metric-card` literals compiled on nothing but that signature, - * which objectui#8347 removes. + * which objectui#8347 removed. * * Each member's type is the prop `MetricCard` declares for it, and `trend`'s * vocabulary is the registration's enum. This package depends on neither, so @@ -2559,9 +2559,10 @@ export interface DashboardComponentSchema extends BaseSchema, Omit = Extract; /** - * Utility type INTENDED to make all properties optional except the type. + * Utility type to make all properties optional except the type. * Useful for partial schema definitions in editors. * - * ⚠️ MEASURED READING — it does not currently deliver that (objectui#6397). - * Every instantiation declares exactly ONE property, `type`, and carries a live - * `[key: string]: any`, so it accepts any key at `any`. Measured through the - * TypeScript checker against the emitted `index.d.ts`: - * - * PartialSchema -> 1 declared property: type (source: 61) - * PartialSchema -> 1 declared property: type (source: 67) - * PartialSchema -> 1 declared property: type (source: 42) - * PartialSchema -> 1 declared property: type (source: 27) - * + * ⚠️ It did NOT deliver that until objectui#8347 (objectui#6397 measured it): * `Omit` is `Pick>`, and `keyof T` on a type - * carrying a string index signature is `string | number` — the literal member - * names are ABSORBED. Every `T extends BaseSchema` inherits `BaseSchema`'s - * `[key: string]: any` (objectui#5155), so `Partial>` rebuilds a - * type holding the index signature and none of the named members; the explicit - * `{ type: T['type'] }` half is the only reason the count is 1 and not 0. Same - * mechanism as objectui#6151 (heritage clause) and objectui#6269 (property - * position) — this is its generic mapped-type-alias position. - * - * ⭐ SEQUENCING (objectui#6397 triage, 2026-08-25) — this declaration is - * deliberately left AS WRITTEN. The triage also held that it was not - * repairable in place: `T` is generic, so there is no literal key list to - * `Pick` the way objectui#6269 could for its two concrete schemas, and it read - * every generic re-spelling as collapsing for the same `keyof T` reason. - * - * ⚠️ That last reading is FALSE, and objectui#9256 is the counter-example. A - * key-remapping mapped type (`{ [P in keyof T as P extends K ? never : P]: … }`) - * iterates the named members and the index signature separately, so it drops - * exactly `K` and keeps every other named member — generic in `T` and all. - * `OmitDeclared` in `./form.ts` is that spelling; the E3 slice of objectui#9256 - * re-spelled `InputShorthandSchema` and `UiCalendarSchema` with it, and - * `__tests__/content-channel-e3-residual-9256.test.ts` pins inherited members - * on both faces. So the obstacle to a repair here is not the type system. - * Re-spelling this alias would NARROW what every instantiation accepts (a - * declared member would stop accepting a value of the wrong type), which is a - * contract change and is not made in a comment correction; objectui#6397, the - * card that triaged it, is closed. - * - * It is also not removable here — dropping a published export - * of `@object-ui/types` is a breaking removal of published capability and sits - * on the human floor. Once objectui#5155 removes the root index signature, - * `keyof T` resolves to the literal member union again and this alias starts - * working exactly as its first line promises, with no edit here at all. + * carrying a string index signature is `string | number`, so while every + * `T extends BaseSchema` inherited `BaseSchema`'s `[key: string]: any`, the + * literal member names were absorbed and every instantiation declared exactly + * one property, `type`, beside a live index signature. objectui#8347 removed + * that signature, so `keyof T` is the literal member union again and the + * alias works as written, with no edit here. The collapse pin that recorded + * the old reading (`partial-schema-collapse-pin.test.ts`) asked to be deleted + * on that day, and was; `__tests__/base-schema-closed-face-8347.test.ts` pins + * the working reading. * - * Until then, do not adopt it as protection it does not provide: name the - * concrete schema type, or write `Partial>` over literal keys - * (objectui#6269's repair shape), when you need a real partial. The reading - * above is pinned by `src/__tests__/partial-schema-collapse-pin.test.ts`, which - * goes red — by design — the day objectui#5155 lands. + * ⚠️ A `T` that declares its OWN index signature still collapses the same way; + * reach for `OmitDeclared` (`./form.ts`) there, the key-remapping spelling + * objectui#9256 used. It had no in-repo consumer at the removal (a reading + * taken then, which nothing re-derives); whether to keep or retire it is a + * published-export question for the maintainer (the pin's own removal + * condition asked for that card), ⛔ not something to settle here. */ export type PartialSchema = { type: T['type']; diff --git a/packages/types/src/zod/base.zod.ts b/packages/types/src/zod/base.zod.ts index 5c5e3caf7d..0ec3ef248b 100644 --- a/packages/types/src/zod/base.zod.ts +++ b/packages/types/src/zod/base.zod.ts @@ -294,8 +294,9 @@ type NodeSlotZodType = z.ZodOptional Date: Sun, 4 Oct 2026 09:15:56 +0000 Subject: [PATCH 05/14] docs: the blocks and passages the index-signature removal makes false The listed doc blocks now compile against the closed face: architecture.md's button writes `label`; objectos-integration.mdx's page block authors `regions`, and its object-view nodes drop the adapter written as `dataSource`; the types README's card writes `children`; plugin-calendar.mdx and plugin-map.mdx pass their click handlers as component props instead of node keys. Prose that described `BaseSchema`'s signature as live is corrected (schema-reference, schema-renderer, ci-cd-pipeline, plugin-form, the wrapperClass callouts, and the plugin-form, plugin-dashboard, plugin-calendar and types READMEs), the `examples/dashboard.ts` comment too, and the pending flex changeset gets a dated note (objectui#8347). Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude --- .changeset/11564-flex-bag-children-list.md | 2 + content/docs/api/schema-reference.md | 2 +- content/docs/components/data-display/list.mdx | 2 +- content/docs/components/form/date-picker.mdx | 2 +- content/docs/components/form/select.mdx | 2 +- content/docs/components/form/switch.mdx | 2 +- content/docs/components/form/textarea.mdx | 2 +- content/docs/core/schema-renderer.mdx | 12 ++-- content/docs/guide/architecture.md | 2 +- content/docs/guide/ci-cd-pipeline.md | 4 +- content/docs/guide/objectos-integration.mdx | 46 +++++++----- content/docs/plugins/plugin-calendar.mdx | 61 ++++++++++------ content/docs/plugins/plugin-form.mdx | 8 ++- content/docs/plugins/plugin-map.mdx | 72 +++++++++---------- packages/plugin-calendar/README.md | 14 ++-- packages/plugin-dashboard/README.md | 7 +- packages/plugin-form/README.md | 26 +++---- packages/types/README.md | 15 ++-- packages/types/examples/dashboard.ts | 9 +-- 19 files changed, 163 insertions(+), 127 deletions(-) diff --git a/.changeset/11564-flex-bag-children-list.md b/.changeset/11564-flex-bag-children-list.md index e2a9fb6ef9..a509c73404 100644 --- a/.changeset/11564-flex-bag-children-list.md +++ b/.changeset/11564-flex-bag-children-list.md @@ -16,3 +16,5 @@ The type is the flat `FlexSchema` mirror's own `children` member, by reference, What does NOT move: every zod face and every runtime path. `FlexBlockSchema` keeps `z.array(z.unknown())` for the list, because `@objectstack/spec`'s page walk already judges each entry there, once, at its real path, on the tolerant and the strict face (objectui#11223). The one entry kind where the faces now differ is a nested list, which the walk passes through unvisited and the TypeScript face refuses; `@object-ui/types`' mirror-parity ledger records the divergence. **Migration.** Give each list entry its declared node type (or `DeclaredNode`), declare a custom type in `CustomNodeRegistry`, and flatten a nested list into the one list. + +⚠️ **Dated note, 2026-10-04 — `BaseSchema` loses its index signature — objectui#8347.** At this change, "Node types that extend `BaseSchema` keep its index signature until objectui#8347 removes it, so a misspelled key on one of them still compiles" held. Later in this same release objectui#8347 removed that signature, so a misspelled key on a node type that extends `BaseSchema` no longer compiles either, in the list exactly as in a single child. `.changeset/8347-baseschema-closed-face.md` states what ships. The rest of this entry is kept as the reading of this change. diff --git a/content/docs/api/schema-reference.md b/content/docs/api/schema-reference.md index 83e46c7898..a42fe178e1 100644 --- a/content/docs/api/schema-reference.md +++ b/content/docs/api/schema-reference.md @@ -109,7 +109,7 @@ Two things the table cannot show in a cell: - **A concrete schema may narrow an inherited member, and its own declaration wins.** Many component schemas restate `label`, `description` or `disabled` more narrowly than `BaseSchema` declares them, so the unions above are what a node gets when its own schema does not restate the key. Check the component's own property table before writing a predicate string or a locale map into an inherited slot. - **⚠️ `body` and `children` WERE two channels, not one key with two spellings — and the second one is retired (objectui#6771).** Each renderer read one, the other, both, or neither, and `SchemaRenderer` strips both out of the props bag it spreads — so writing the channel a renderer did not read rendered an EMPTY element, with no error at authoring time, none at validation time and none at render time. That is the defect objectui#8284 named after seven cards had repaired one page of it each, and it was closed per component by measurement: each schema narrows to the channel its renderer actually reads and **tombstones the other as `never`**, refused by name on both published faces (objectui#9254 for the components that read exactly one channel, objectui#9256 for the ones that read neither). objectui#6771 then closed the class at its source: `children` is the one child-list spelling and `body` is refused on `BaseSchema` itself. ⇒ **whether a node type accepts a child list at all is still per component** — check the component's own section, where a node that renders no children tombstones `children` too. -- **This list is exhaustive for *declared* members, not for *accepted* keys.** `BaseSchema` carries an index signature (`[key: string]: any`) and its Zod mirror is `.passthrough()`, so an undeclared key — a misspelling included — is still accepted by both halves. Absence from this table does not mean a key is rejected. +- **This list is exhaustive for *declared* members; the two faces differ on *accepted* keys.** `BaseSchema` carries no index signature (objectui#8347), so on the TypeScript face an undeclared key, a misspelling included, is a compile error in a typed literal. Its tolerant Zod mirror is still `.passthrough()`, so the rendering face (and `safeValidateSchema`) keeps an undeclared key; the derived strict face, `StrictAnyComponentSchema`, refuses it. Absence from this table does not mean the tolerant face rejects a key. --- diff --git a/content/docs/components/data-display/list.mdx b/content/docs/components/data-display/list.mdx index 7ef6fe1b9b..214e9a7db5 100644 --- a/content/docs/components/data-display/list.mdx +++ b/content/docs/components/data-display/list.mdx @@ -42,7 +42,7 @@ required, so the bind-only list above was refused at `items`. The TypeScript fac `wrapperClass` is now typed. The renderer has read it all along — it lands on the wrapper element around the list — but it went undeclared until objectui#7722, - surviving on `BaseSchema`'s index signature and the zod mirror's `.passthrough()`. + surviving on what was then `BaseSchema`'s index signature (objectui#8347 removed it) and the zod mirror's `.passthrough()`. Declared as `string` on both faces, a **non-string** `wrapperClass` that used to parse green is now refused by name — on `ListSchema` and through `safeValidateSchema` alike. Pass the Tailwind class string; a number or an object diff --git a/content/docs/components/form/date-picker.mdx b/content/docs/components/form/date-picker.mdx index 327b6e055f..7e8bb7d687 100644 --- a/content/docs/components/form/date-picker.mdx +++ b/content/docs/components/form/date-picker.mdx @@ -49,7 +49,7 @@ interface DatePickerSchema { `wrapperClass` is now typed. The renderer has read it all along — it lands on the wrapper element around the date picker and its label — but it went undeclared until - objectui#7722, surviving on `BaseSchema`'s index signature and the zod mirror's + objectui#7722, surviving on what was then `BaseSchema`'s index signature (objectui#8347 removed it) and the zod mirror's `.passthrough()`. Declared as `string` on both faces, a **non-string** `wrapperClass` that used to parse green is now refused by name — on `DatePickerSchema` and through `safeValidateSchema` alike. Pass the Tailwind class diff --git a/content/docs/components/form/select.mdx b/content/docs/components/form/select.mdx index 7edefd689d..366d652062 100644 --- a/content/docs/components/form/select.mdx +++ b/content/docs/components/form/select.mdx @@ -27,7 +27,7 @@ interface SelectSchema { `wrapperClass` is now typed. The renderer has read it all along — it lands on the wrapper element around the select and its label — but it went undeclared until - objectui#7722, surviving on `BaseSchema`'s index signature and the zod mirror's + objectui#7722, surviving on what was then `BaseSchema`'s index signature (objectui#8347 removed it) and the zod mirror's `.passthrough()`. Declared as `string` on both faces, a **non-string** `wrapperClass` that used to parse green is now refused by name — on `SelectSchema` and through `safeValidateSchema` alike. Pass the Tailwind class string; a number or diff --git a/content/docs/components/form/switch.mdx b/content/docs/components/form/switch.mdx index ffb26966f1..ba30663104 100644 --- a/content/docs/components/form/switch.mdx +++ b/content/docs/components/form/switch.mdx @@ -25,7 +25,7 @@ interface SwitchSchema { `wrapperClass` is now typed. The renderer has read it all along — it lands on the wrapper element around the switch and its label — but it went undeclared until - objectui#7722, surviving on `BaseSchema`'s index signature and the zod mirror's + objectui#7722, surviving on what was then `BaseSchema`'s index signature (objectui#8347 removed it) and the zod mirror's `.passthrough()`. Declared as `string` on both faces, a **non-string** `wrapperClass` that used to parse green is now refused by name — on `SwitchSchema` and through `safeValidateSchema` alike. Pass the Tailwind class string; a number or diff --git a/content/docs/components/form/textarea.mdx b/content/docs/components/form/textarea.mdx index 4167e2cb2f..48ff61457f 100644 --- a/content/docs/components/form/textarea.mdx +++ b/content/docs/components/form/textarea.mdx @@ -27,7 +27,7 @@ interface TextareaSchema { `wrapperClass` is now typed. The renderer has read it all along — it lands on the wrapper element around the textarea and its label — but it went undeclared until - objectui#7722, surviving on `BaseSchema`'s index signature and the zod mirror's + objectui#7722, surviving on what was then `BaseSchema`'s index signature (objectui#8347 removed it) and the zod mirror's `.passthrough()`. Declared as `string` on both faces, a **non-string** `wrapperClass` that used to parse green is now refused by name — on `TextareaSchema` and through `safeValidateSchema` alike. Pass the Tailwind class string; a number or diff --git a/content/docs/core/schema-renderer.mdx b/content/docs/core/schema-renderer.mdx index 8d3d78ea45..cb840b0b8f 100644 --- a/content/docs/core/schema-renderer.mdx +++ b/content/docs/core/schema-renderer.mdx @@ -34,16 +34,20 @@ The SchemaRenderer automatically handles nested schemas: ## Schema Structure ```ts -interface SchemaNode { +// abridged: `BaseSchema` in @object-ui/types carries the full member list +interface BaseSchema { type: string; // Component type (e.g., 'card', 'button', 'text') id?: string; // Unique identifier className?: string; // Tailwind CSS classes - children?: SchemaNode | SchemaNode[]; // Child components - props?: Record; // Component-specific props - [key: string]: any; // Any other component props + children?: BaseSchema | BaseSchema[]; // Child components } ``` +Every node type extends `BaseSchema` with its own keys, and none of them +carries an index signature (objectui#8347): in a typed literal, a key the +node's type does not declare, a misspelling included, is a compile error. +A child in a slot is checked against its own `type`'s declaration. + ## Error Handling When an unknown component type is encountered, SchemaRenderer displays a helpful error message: diff --git a/content/docs/guide/architecture.md b/content/docs/guide/architecture.md index dd78fe4223..19c80b43cc 100644 --- a/content/docs/guide/architecture.md +++ b/content/docs/guide/architecture.md @@ -378,7 +378,7 @@ function handleClick() { const schema: ButtonSchema = { type: 'button', - text: 'Click me', + label: 'Click me', variant: 'default', // ✅ Type-checked onClick: handleClick, // ✅ a handler, not its name — onClick is () => void | Promise } diff --git a/content/docs/guide/ci-cd-pipeline.md b/content/docs/guide/ci-cd-pipeline.md index 24b48603e9..3db7e70d7b 100644 --- a/content/docs/guide/ci-cd-pipeline.md +++ b/content/docs/guide/ci-cd-pipeline.md @@ -1582,8 +1582,8 @@ trailing comment (which must **not** be reported), an `X as Y` with `X` fabricat in a type import. **Out of scope, deliberately:** compiling the extracted blocks (a separate card — it has pre-existing -reds that need a baseline decision first), and authorable-JSON *key* surfaces, which no type check can -reject while `BaseSchema` carries an index signature and its Zod mirror is `.passthrough()`. +reds that need a baseline decision first), and authorable-JSON *key* surfaces: a JSON block carries no +type annotation, and the Zod mirror `safeValidateSchema` reads is `.passthrough()`. **If it fails:** it names the README, the line of the offending specifier, and which package really exports the name. Run it locally with `pnpm check:readme-exports` after a build, or diff --git a/content/docs/guide/objectos-integration.mdx b/content/docs/guide/objectos-integration.mdx index 169b5c7e36..452cd77418 100644 --- a/content/docs/guide/objectos-integration.mdx +++ b/content/docs/guide/objectos-integration.mdx @@ -113,26 +113,31 @@ export function App() { return (
@@ -214,12 +219,15 @@ const objectStackAdapter = new ObjectStackAdapter({ baseUrl: 'http://localhost:3000/api' }); +// The adapter is handed to the renderer (`` +// or `SchemaRendererProvider`), never written on the node: a node's `dataSource` +// is the spec's per-element binding, not an adapter. const sysUserView: ObjectViewSchema = { type: 'object-view', objectName: 'sys_user', - dataSource: objectStackAdapter, defaultViewType: 'grid', - columns: ['username', 'email', 'role', 'status', 'last_login'] + // The grid's own configuration rides the `table` slot. + table: { columns: ['username', 'email', 'role', 'status', 'last_login'] } }; ``` diff --git a/content/docs/plugins/plugin-calendar.mdx b/content/docs/plugins/plugin-calendar.mdx index fc839f9f32..dcbf6e455c 100644 --- a/content/docs/plugins/plugin-calendar.mdx +++ b/content/docs/plugins/plugin-calendar.mdx @@ -431,10 +431,16 @@ anywhere in the package. ## Event Handling +A click handler is a function, so it is a prop of the `ObjectCalendar` +component a React host renders, never a key on the node: +`ObjectCalendarSchema` declares no `onEventClick` or `onDateClick`, and +authored JSON can never carry a function anyway. + ### Event Click ```tsx -import type { ObjectCalendarSchema } from '@object-ui/types'; +import { ObjectCalendar } from '@object-ui/plugin-calendar'; +import type { DataSource, ObjectCalendarSchema } from '@object-ui/types'; const eventClickCalendar: ObjectCalendarSchema = { type: 'object-calendar', @@ -442,19 +448,28 @@ const eventClickCalendar: ObjectCalendarSchema = { calendar: { startDateField: 'start', titleField: 'title' - }, - onEventClick: (record: any) => { - console.log('Event clicked:', record); - // Open event details - // Navigate to event page } }; + +export function EventClickCalendar({ dataSource }: { dataSource: DataSource }) { + return ( + { + console.log('Event clicked:', record); + // Open event details, or navigate to the event page + }} + /> + ); +} ``` ### Date Click ```tsx -import type { ObjectCalendarSchema } from '@object-ui/types'; +import { ObjectCalendar } from '@object-ui/plugin-calendar'; +import type { DataSource, ObjectCalendarSchema } from '@object-ui/types'; const dateClickCalendar: ObjectCalendarSchema = { type: 'object-calendar', @@ -462,12 +477,21 @@ const dateClickCalendar: ObjectCalendarSchema = { calendar: { startDateField: 'start', titleField: 'title' - }, - onDateClick: (date: Date) => { - console.log('Date clicked:', date); - // Create new event on this date } }; + +export function DateClickCalendar({ dataSource }: { dataSource: DataSource }) { + return ( + { + console.log('Date clicked:', date); + // Create a new event on this date + }} + /> + ); +} ``` ## Examples @@ -485,11 +509,9 @@ const appointmentCalendar: ObjectCalendarSchema = { endDateField: 'appointmentEnd', titleField: 'patientName', colorField: 'appointmentType' - }, - onEventClick: (appointment: any) => { - // Show appointment details } }; +// Show appointment details from the component's `onEventClick` prop (see Event Handling). ``` ### Event Management @@ -505,14 +527,9 @@ const eventCalendar: ObjectCalendarSchema = { endDateField: 'eventEnd', titleField: 'eventTitle', colorField: 'eventCategory' - }, - onEventClick: (event: any) => { - // Navigate to event details - }, - onDateClick: (date: Date) => { - // Create new event } }; +// Navigate from `onEventClick` and create from `onDateClick`, both component props (see Event Handling). ``` ### Task Deadlines @@ -527,11 +544,9 @@ const taskCalendar: ObjectCalendarSchema = { startDateField: 'dueDate', titleField: 'taskTitle', colorField: 'priority' - }, - onEventClick: (task: any) => { - // Open task details } }; +// Open task details from the component's `onEventClick` prop (see Event Handling). ``` ## Direct Component Usage diff --git a/content/docs/plugins/plugin-form.mdx b/content/docs/plugins/plugin-form.mdx index ab5cd7041b..99b8479bf1 100644 --- a/content/docs/plugins/plugin-form.mdx +++ b/content/docs/plugins/plugin-form.mdx @@ -445,9 +445,11 @@ The package README has the full rule, under "What an edit save writes". ### Form with Validation One `validation` object per field, keyed by rule name. The annotation is part of -the example: `FormField` and `FormSchema` both carry an index signature, so an -un-annotated `const schema = { … }` type-checks whatever is written in it, while -`const signUpForm: FormSchema` makes a wrong `validation` shape a compile error. +the example: an un-annotated `const schema = { … }` type-checks whatever is +written in it, while `const signUpForm: FormSchema` makes a wrong `validation` +shape a compile error, and a misspelled form-level key too (`FormSchema` +carries no index signature since objectui#8347; a `FormField` entry still +carries its own). ```typescript import type { FormSchema } from '@object-ui/types'; diff --git a/content/docs/plugins/plugin-map.mdx b/content/docs/plugins/plugin-map.mdx index f2202e9e20..be0bf6d72b 100644 --- a/content/docs/plugins/plugin-map.mdx +++ b/content/docs/plugins/plugin-map.mdx @@ -130,8 +130,8 @@ const schema: ObjectMapBlockNode = { navigation?: NavigationConfig, // What a marker click opens (see Marker click navigation) mapStyle?: string // MapLibre style URL; read before `map.style` }, - onMarkerClick?: (record: any) => void, // A host callback: on the node, in code only className?: string + // `onMarkerClick` is no node key: a React host passes it as a prop (see Marker Click) } ``` @@ -354,13 +354,15 @@ A click handler from a parent view (`onRowClick`) outranks the whole key. ### Marker Click `onMarkerClick` is a function, so it is no member of the `properties` bag (the -spec's row declares none): a schema built in code carries it on the node, beside -the bag. +spec's row declares none) and no key of the node either: a React host passes it +as a prop, and `SchemaRenderer` forwards it to the map. `ObjectMapBlockNode` is +the node with its bag closed, so a misspelled key inside it is a compile error. ```tsx -import type { BaseSchema } from '@object-ui/types'; +import { SchemaRenderer } from '@object-ui/react'; +import type { ObjectMapBlockNode } from '@object-ui/types'; -const clickableMap: BaseSchema = { +const clickableMap: ObjectMapBlockNode = { type: 'object-map', properties: { objectName: 'locations', @@ -369,14 +371,20 @@ const clickableMap: BaseSchema = { longitudeField: 'lng', titleField: 'name' } - }, - onMarkerClick: (location: Record) => { - console.log('Marker clicked:', location); - // Show location details - // Navigate to location page - // Open directions } }; + +export function ClickableMap() { + return ( + ) => { + console.log('Marker clicked:', location); + // Show location details, navigate to the location page, or open directions + }} + /> + ); +} ``` ## Examples @@ -384,9 +392,9 @@ const clickableMap: BaseSchema = { ### Store Locator ```tsx -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectMapBlockNode } from '@object-ui/types'; -const storeLocator: BaseSchema = { +const storeLocator: ObjectMapBlockNode = { type: 'object-map', properties: { objectName: 'retail_locations', @@ -398,20 +406,17 @@ const storeLocator: BaseSchema = { zoom: 10, center: [37.7749, -122.4194] // San Francisco } - }, - onMarkerClick: (store: Record) => { - // Show store details - // Display hours, phone, etc. } } +// Store details (hours, phone) come from `onMarkerClick`, a prop the host passes (see Marker Click). ``` ### Delivery Tracking ```tsx -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectMapBlockNode } from '@object-ui/types'; -const deliveryMap: BaseSchema = { +const deliveryMap: ObjectMapBlockNode = { type: 'object-map', properties: { objectName: 'active_deliveries', @@ -421,20 +426,17 @@ const deliveryMap: BaseSchema = { titleField: 'driver_name', descriptionField: 'delivery_address' } - }, - onMarkerClick: (delivery: Record) => { - // Show delivery details - // Contact driver } } +// Delivery details and contacting the driver come from `onMarkerClick`, a prop the host passes (see Marker Click). ``` ### Real Estate Listings ```tsx -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectMapBlockNode } from '@object-ui/types'; -const propertyMap: BaseSchema = { +const propertyMap: ObjectMapBlockNode = { type: 'object-map', properties: { objectName: 'properties', @@ -445,12 +447,9 @@ const propertyMap: BaseSchema = { descriptionField: 'property_details', zoom: 12 } - }, - onMarkerClick: (property: Record) => { - // Show property details - // Display photos, price, etc. } } +// Property details (photos, price) come from `onMarkerClick`, a prop the host passes (see Marker Click). ``` ### Event Venues @@ -489,20 +488,17 @@ const venueMap = { descriptionField: 'details', zoom: 11 } - }, - onMarkerClick: (venue: Record) => { - // Show venue details - // Book venue } } +// Venue details and booking come from `onMarkerClick`, a prop the host passes (see Marker Click). ``` ### Field Service Map ```tsx -import type { BaseSchema } from '@object-ui/types'; +import type { ObjectMapBlockNode } from '@object-ui/types'; -const serviceMap: BaseSchema = { +const serviceMap: ObjectMapBlockNode = { type: 'object-map', properties: { objectName: 'service_calls', @@ -513,13 +509,9 @@ const serviceMap: BaseSchema = { descriptionField: 'service_type', zoom: 10 } - }, - onMarkerClick: (serviceCall: Record) => { - // Show service call details - // Assign technician - // Get directions } } +// Service-call details, assigning a technician and directions come from `onMarkerClick`, a prop the host passes (see Marker Click). ``` ## Typical Use Cases diff --git a/packages/plugin-calendar/README.md b/packages/plugin-calendar/README.md index 58a4e05208..2f6f7e9a73 100644 --- a/packages/plugin-calendar/README.md +++ b/packages/plugin-calendar/README.md @@ -194,10 +194,16 @@ const backwards: CalendarViewNode = shipped; > **What those two assignments do and do not buy.** They check every key's > **type**, in both directions. They do **not** check a key's **spelling**: -> `CalendarViewSchema` extends `BaseSchema`, whose `[key: string]: any` accepts -> any name, so a key invented or misspelled here is `any` rather than an error -> (objectui#7927). Key-level validity is the strict `@objectstack/spec` twin's -> question, not TypeScript's. +> both assign a declared variable, not a fresh object literal, and TypeScript +> runs its excess-property check only on a fresh literal. So a key renamed in +> the listing is an extra optional member on one side and simply absent on the +> other, and both assignments still compile. A fresh literal IS checked: +> `CalendarViewSchema` extends `BaseSchema`, which carries no index signature +> since objectui#8347, so +> `const node: CalendarViewSchema = { type: 'calendar-view', titleFieldd: 'name' }` +> is a compile error. Metadata that arrives as data is a question for the zod +> faces (the strict `StrictAnyComponentSchema` refuses an unknown key), not for +> TypeScript. There is deliberately **no authorable `events` key**: the renderer computes its events from `data` plus the field-name keys, and drops an authored `events` diff --git a/packages/plugin-dashboard/README.md b/packages/plugin-dashboard/README.md index e9009338ab..4c53608c59 100644 --- a/packages/plugin-dashboard/README.md +++ b/packages/plugin-dashboard/README.md @@ -698,9 +698,10 @@ a card directly in `widgets[]` whose `trend` is outside the three, or whose The widget keys (`colorVariant`, `filter`, `dataset`, …) are declared on the widget arm, `DashboardWidgetSchema`, which takes them from the spec's -`DashboardWidget` row. The component arm declares none of them. Read straight -off a `widgets[]` entry, such a key is typed `any`, supplied by the component -arm's passthrough. This package's own readers take an entry by the slot's +`DashboardWidget` row. The component arm declares none of them, so read +straight off a `widgets[]` entry such a key is a compile error: the union offers +only the keys both arms declare (before objectui#8347 removed `BaseSchema`'s index +signature, the component arm answered it as `any`). This package's own readers take an entry by the slot's element type, `DashboardComponentSchema['widgets'][number]`. The component arm is not assignable to `DashboardWidgetSchema`, whose `type` names no component type, so narrow an entry on `type` first: `metric-card` is the one component diff --git a/packages/plugin-form/README.md b/packages/plugin-form/README.md index d1fb9a8a3c..1a48269c3f 100644 --- a/packages/plugin-form/README.md +++ b/packages/plugin-form/README.md @@ -197,11 +197,12 @@ unknown-component placeholder. | `onSubmit` / `onChange` / `onDirtyChange` / `onCancel` | callbacks | **TypeScript-authored schemas only** — a JSON metadata document cannot carry a function. Metadata pages go through the object-form route instead | | `className`, `id`, `hidden`, … | — | inherited from `BaseSchema` | -⚠️ `FormSchema` extends `BaseSchema`, which declares `[key: string]: any` -(`packages/types/src/base.ts`), so an invented or misspelled key on a form schema -is **not** a compile error — it is simply never read. That is why every example -below is annotated with its real type *and* checked against these key tables: -an un-annotated `const schema = { … }` type-checks whatever is written in it. +`FormSchema` extends `BaseSchema`, which declares no index signature +(objectui#8347), so an invented or misspelled key in a form schema literal +annotated with its type is a compile error. That is why every example below is +annotated with its real type: an un-annotated `const schema = { … }` type-checks +whatever is written in it. (`fields` entries are `FormField`s, which still carry +their own index signature — see below.) ### Form Field @@ -232,8 +233,8 @@ only **required** one: | `colSpan` | `number` | legacy column span (1–4), clamped to the current column count | | `field` | `Record` | the resolved object-field **metadata object**, stashed by the object-bound paths so widgets can read `precision`, `currency`, `reference`, … In the *spec* form-view vocabulary `field` is a string (the referenced field name); that shape ends at `normalizeSectionField` and never reaches a runtime `FormField` | -`FormField` also declares `[key: string]: any`, so an invented key type-checks -here too. Two that a reader might expect, and that are **not** declared: +`FormField` declares its own `[key: string]: any`, so an invented key on a field +still type-checks. Two that a reader might expect, and that are **not** declared: | Not a `FormField` key | Write this instead | |---|---| @@ -800,8 +801,8 @@ const wizard: WizardFormSchema = { ``` `WizardFormSchema` declares no index signature, so an invented key on *this* -type is a real compile error — unlike `ObjectFormSchema`, which inherits -`BaseSchema`'s `[key: string]: any`. +type is a real compile error — and since objectui#8347 removed `BaseSchema`'s +`[key: string]: any`, the same holds for `ObjectFormSchema`. One more route exists and is worth knowing about rather than reinventing: a flat `object-form` can be turned into a stepper on small viewports with @@ -1057,9 +1058,10 @@ nothing: | `dataSource` | **Discarded.** The basic form strips it in both directions — `dataSource: _dataSource` at `form.tsx:304` (`stripRendererOnlyProps`) and `:2168` — so it never reaches a widget and never reaches the DOM. The adapter the fields receive is the context one | | `resource` | **Never read.** It is not declared on `FormSchema` or `ObjectFormSchema` at all. It used to exist elsewhere in the protocol — on `CRUDSchema`, where `CRUDBuilder` set it — but objectui#5373 retired both under ADR-0049, so today the key names nothing anywhere in this package's surface, and no form renderer reads it under any spelling | -Both survive compilation for the reason [Schema API](#schema-api) gives: `FormSchema` -and `ObjectFormSchema` extend `BaseSchema`, which declares `[key: string]: any`, so an -invented key is not a type error — it is simply never read. That is also why the +Both survived compilation while `FormSchema` and `ObjectFormSchema` extended a +`BaseSchema` that declared `[key: string]: any`: an invented key was not a type +error, it was simply never read. objectui#8347 removed that signature, so both are +compile errors in an annotated literal now. That is also why the older version of this section looked like it worked: its `onSubmit` genuinely ran and genuinely saved, but through the adapter its closure captured. The `dataSource` and `resource` keys sitting beside it in the same object were inert. Delete them and diff --git a/packages/types/README.md b/packages/types/README.md index 8b7ad2189c..0f1afa641e 100644 --- a/packages/types/README.md +++ b/packages/types/README.md @@ -90,7 +90,8 @@ import type { DataTableSchema, FlexSchema, CardSchema } from '@object-ui/types'; const dashboard: CardSchema = { type: 'card', title: 'User Management', - content: { + // A card's child channel is `children` (objectui#6771); `content` is no card key. + children: { type: 'data-table', columns: [ { header: 'Name', accessorKey: 'name' }, @@ -424,8 +425,9 @@ Components can nest indefinitely: ```typescript import type { ContainerSchema, FlexBlockNode, HeaderBarSchema, SidebarSchema } from '@object-ui/types'; -// The two leaves are annotated so the nesting below is checked against the -// shipped types rather than absorbed by `BaseSchema`'s index signature. +// The two leaves are annotated with their node types, so each is checked +// against its own declaration (`BaseSchema` carries no index signature to +// absorb a stray key, objectui#8347). // The sidebar draws what it composes through `children` (an app's navigation // lives in the app's metadata, not on this node), and `collapsible: false` // draws it in the page flow, beside `main`. @@ -446,9 +448,10 @@ const main: ContainerSchema = { // An authored `flex` node takes its props, the child list included, in its // `properties` bag. `FlexBlockNode` is that node, its bag closed, and each // entry of the bag's child list is checked as a node of its own `type`. The -// nested `flex` still names its type with `satisfies`: as an entry it also -// matches the post-hoist `FlexSchema`, whose `BaseSchema` index signature -// admits any `properties` until that signature is removed. +// nested `flex` names its type with `satisfies` too. That was needed while the +// post-hoist `FlexSchema` inherited `BaseSchema`'s index signature, which +// admitted any `properties`; objectui#8347 removed it, so the entry is judged +// as `FlexBlockNode` either way, and the annotation stays as documentation. const page: FlexBlockNode = { type: 'flex', properties: { diff --git a/packages/types/examples/dashboard.ts b/packages/types/examples/dashboard.ts index 5f4b6a77f2..33d960bda9 100644 --- a/packages/types/examples/dashboard.ts +++ b/packages/types/examples/dashboard.ts @@ -18,10 +18,11 @@ import type { FlexBlockNode, SidebarSchema, HeaderBarSchema, CardSchema, DataTab // An authored `flex` node takes its props, the child list included, in its // `properties` bag (objectui#11276). `FlexBlockNode` is that node, its bag // closed (objectui#11468), and each entry of the bag's child list is typed as -// a node (objectui#11564). A `flex` entry also matches the post-hoist -// `FlexSchema`, whose `BaseSchema` index signature admits any `properties` -// until objectui#8347 removes it, so the nested `flex` node is checked with -// `satisfies FlexBlockNode`. +// a node (objectui#11564). The nested `flex` node is checked with +// `satisfies FlexBlockNode`: that was needed while a `flex` entry also matched +// the post-hoist `FlexSchema`, whose `BaseSchema` index signature admitted any +// `properties`. objectui#8347 removed the signature, so the entry is judged as +// `FlexBlockNode` either way; the annotation stays as documentation. export const dashboardSchema: FlexBlockNode = { type: 'flex', className: 'h-screen', From 913dee43cc7cc85c55e6119b226eb9b0fd4f0802 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 09:15:56 +0000 Subject: [PATCH 06/14] =?UTF-8?q?docs(agents):=20=C2=A75=20#0.1=20says=20t?= =?UTF-8?q?he=20TypeScript=20authoring=20face=20is=20part=20of=20the=20con?= =?UTF-8?q?tract;=20=C2=A74=20drops=20`props`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Describes what shipped with objectui#8347: `BaseSchema` declares no index signature, a refused key is declared by reference or fixed at the producer, never cast past, and the fresh-literal bound is stated with the pin that measures it. §4's abridged listing drops `props`, which `BaseSchema` does not declare, and #4's `events` sentence names the face that still keeps one. Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude --- AGENTS.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 57af347790..fc9c7f202d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -74,12 +74,12 @@ Every node in the UI tree follows this shape (`@object-ui/types`): interface BaseSchema { type: string; // registry key: 'input', 'grid', 'card' id?: string; // DOM accessibility / event targeting - props?: Record; // element:* config envelope, not a general bag bind?: string; // data binding path: 'user.address.city' className?: string; // Tailwind overrides hidden?: boolean | ExpressionWire; // expression or boolean: "${data.role != 'admin'}" disabled?: boolean | ExpressionWire; // same wire as hidden children?: SchemaNode | SchemaNode[]; // layout slots; primitives admitted too + // no index signature: a key no member declares is a compile error (#0.1) } ``` @@ -90,6 +90,7 @@ interface BaseSchema { - **#-1 — English-only codebase.** This is an international OSS project. All user-facing text (component labels, buttons, titles, errors), code comments, docs (`README.md`, `docs/*.md`), and console/log messages MUST be English. No Chinese or other non-English in those. *(This rule governs the **codebase**; this instruction file may use Chinese in operational sections.)* - **#0 — Strict adherence to `@objectstack/spec`.** All schemas/JSON structures/types MUST follow `@objectstack/spec`. Don't invent schema properties — if the spec says `columns`, don't use `fields`. Check the spec before writing any `interface`/`type`. - **#0.1 — Fix the metadata, not the renderer (contract-first).** Corollary to #0. This is a metadata-driven system: `@objectstack/spec` is the contract between producers and this renderer. When a piece of metadata "doesn't render," ask **first**: *is it spec-compliant? is this the long-term-correct direction?* If the metadata is off-spec, fix it at the **producer** (and have it rejected at authoring/publish) — do **not** add a lenient fallback/alias in the renderer (reading both `columns` and `fields`, coercing a malformed shape, `??`-defaulting around bad input) to make non-compliant metadata "work." A tolerant fallback fossilizes the wrong convention into a second de-facto contract, dilutes the spec, and hides the producer's bug — one strict contract beats N dialects. We own both ends, so Postel's "be liberal in what you accept" does **not** apply (that's for untrusted boundaries). Change the **spec** only when it is genuinely wrong — deliberately, in `@objectstack/spec`, never by accreting renderer-side fallbacks. + - **The TypeScript authoring face is part of the contract (objectui#7927 ruling, executed by objectui#8347).** `BaseSchema` declares no index signature, so a node literal typed with its node type refuses, at the authoring site, a key no declaration names — a misspelling included. When a key you need is refused, this rule applies unchanged: declare it on the node type that reads it, by reference to the spec row, or fix the producer. ⛔ Never make it compile with `as any`, a cast to `Record`, a re-added index signature or an open `type: string` arm. Tolerance stays where it is deliberate: renderer props (`ComponentRendererProps` keeps its signature) and the tolerant zod face (`.passthrough()`); the derived strict zod face, `StrictAnyComponentSchema`, refuses an unknown key in metadata that arrives as data. ⚠️ The bound, said per #9: TypeScript excess-checks only a FRESH object literal, so a value that reached its annotation through a wider variable is not re-checked. `packages/types/src/__tests__/base-schema-closed-face-8347.test.ts` pins both directions and that bound. - **#1 — Protocol-agnostic.** Never hardcode `objectql.find()`. Use the DataSource interface; inject `dataSource` via ``. - **#2 — Docs-driven.** For every feature/refactor, update package `README.md` **and** `content/docs/guide/*.md`. Not done until docs reflect the code. - **#3 — "Shadcn-native" aesthetics.** We are "serializable Shadcn". Follow Shadcn's DOM structure (`CardHeader`/`CardTitle`/`CardContent`). Always expose `className` in schema props so users can override via JSON. @@ -97,7 +98,7 @@ interface BaseSchema { ```json { "type": "action:button", "properties": { "label": "Open details", "actionType": "url", "target": "/users/ada" } } ``` - ⛔ Never author an `events` bag: `BaseSchema` declares no `events` member and no renderer reads `schema.events` — the node is `.passthrough()`, so one authored there is kept, judged by nothing and run by nothing. `ButtonSchema.onClick` is a runtime slot for a host-supplied function and is refused by name for the same reason. + ⛔ Never author an `events` bag: `BaseSchema` declares no `events` member and no renderer reads `schema.events` — a typed literal refuses it (#0.1), but the tolerant zod face is `.passthrough()`, so one that arrives as data is kept, judged by nothing and run by nothing. `ButtonSchema.onClick` is a runtime slot for a host-supplied function and is refused by name for the same reason. - **#5 — Layout as components.** Treat `Grid`/`Stack`/`Container` as first-class. Layout schemas declare responsive columns on the node as `columns` — a number, or a breakpoint object (`columns: { xs: 1, md: 2, lg: 4 }`); never `cols`, which nothing reads (objectui#4001). - **#6 — Type safety over magic.** No `any` — use strict generics. Map `"type": "button"` → React component via a central `ComponentRegistry`. **No `eval()` / runtime dynamic imports** to load components (security). - **#7 — No-Touch zones (Shadcn purity).** `packages/components/src/ui/**/*.tsx` are upstream 3rd-party files overwritten by sync scripts — **never edit their logic/styles**. To change `Button`/`Dialog` behavior: create/edit a wrapper in `packages/components/src/custom/`, import the primitive from `@/ui/...`, and wrap it. From e0a9af67abc4414d3fc4ad80c80d522392bf49e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 09:15:56 +0000 Subject: [PATCH 07/14] chore(changeset): objectui#8347 Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude --- .changeset/8347-baseschema-closed-face.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .changeset/8347-baseschema-closed-face.md diff --git a/.changeset/8347-baseschema-closed-face.md b/.changeset/8347-baseschema-closed-face.md new file mode 100644 index 0000000000..9b8754f1bc --- /dev/null +++ b/.changeset/8347-baseschema-closed-face.md @@ -0,0 +1,18 @@ +--- +'@object-ui/types': minor +'@object-ui/plugin-kanban': patch +'@object-ui/plugin-timeline': patch +--- + +**`BaseSchema` no longer declares `[key: string]: any`** (objectui#8347, executing the objectui#7927 ruling: the TypeScript face is a contract). Every node type extends `BaseSchema`, so a node literal annotated with its node type now refuses a key that no declaration names, a misspelled key included, where it used to type it `any`. The correct spelling compiles as before. + +**Clause-②: yes (mixed)**, shipped as `minor` per this repository's version policy. The removal narrows the TypeScript authoring face of every node type; the `visibleWhen` change below widens both faces to the envelope the spec's own parse writes. + +- **What does not move.** The zod faces keep their accept sets for every key but `visibleWhen`: the tolerant mirror is still `.passthrough()`, so `safeValidateSchema` keeps an undeclared key, and the derived strict face refuses it as before. `ComponentRendererProps`, the renderer props type, keeps its own index signature. Nothing a renderer draws changes. +- **The bound.** TypeScript runs its excess-property check only on a fresh object literal. A value that reached its annotation through a variable of a wider type is not re-checked. +- **`PartialSchema` works as written.** With the signature gone, `keyof T` is the literal member union again, so the alias keeps `T`'s declared members, optional, with `type` required. While the signature stood it declared `type` alone (objectui#6397). +- **`BaseSchema.visibleWhen` is the spec's `EvaluatedExpressionInput`**, by reference: a predicate string, or the `{ dialect, source }` envelope. The zod twin takes `EvaluatedExpressionInputSchema`'s verdict without its transform, so a string parses to itself. A dialect-less envelope, an unknown dialect and a blank predicate are refused, as the spec refuses them. Both faces read `string` before, which refused the envelope a spec parse writes into this key. +- **`@object-ui/plugin-kanban`.** `ObjectKanban` reads the `sort` the element data-source gate writes through a read type private to the package. `ObjectKanbanSchema` still declares no `sort` (objectui#8174). Nothing drawn changes. +- **`@object-ui/plugin-timeline`.** The renderer's private hand-off type declares the `onItemClick` slot `ObjectTimeline` composes; `TimelineSchema` still declares none. Nothing drawn changes. + +**Migration.** Where a literal stops compiling, the key is misspelled (fix it) or not declared on that node type (declare it on the type that reads it, by reference to the `@objectstack/spec` row, or remove it). Do not cast past the error. `props`, the legacy alias of `properties`, is not declared on the TypeScript face; the renderer still reads it, so write `properties`. From 0334c43e025960679ce5616dadee53d2fbe7e155 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 10:01:27 +0000 Subject: [PATCH 08/14] =?UTF-8?q?chore(changeset):=20objectui#8347=20decla?= =?UTF-8?q?res=20`Clause-=E2=91=A1:=20yes=20(narrowing)`;=20plugin-timelin?= =?UTF-8?q?e=20is=20`minor`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The arm is the closed pair `(widening)` / `(narrowing)`, and `(mixed)` read as no arm at all; this repository spells the widen-one-surface, narrow-another shape `yes (narrowing)`. The plugin-timeline bullet now says what ships: an optional `onItemClick` on `TimelineRenderSchema`, the prop type of the exported `TimelineRenderer`, so a one-member widening of an exported type and `minor` (contract review `5978769372`, ② and ① item 6). Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude --- .changeset/8347-baseschema-closed-face.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/8347-baseschema-closed-face.md b/.changeset/8347-baseschema-closed-face.md index 9b8754f1bc..79bb52a605 100644 --- a/.changeset/8347-baseschema-closed-face.md +++ b/.changeset/8347-baseschema-closed-face.md @@ -1,18 +1,18 @@ --- '@object-ui/types': minor '@object-ui/plugin-kanban': patch -'@object-ui/plugin-timeline': patch +'@object-ui/plugin-timeline': minor --- **`BaseSchema` no longer declares `[key: string]: any`** (objectui#8347, executing the objectui#7927 ruling: the TypeScript face is a contract). Every node type extends `BaseSchema`, so a node literal annotated with its node type now refuses a key that no declaration names, a misspelled key included, where it used to type it `any`. The correct spelling compiles as before. -**Clause-②: yes (mixed)**, shipped as `minor` per this repository's version policy. The removal narrows the TypeScript authoring face of every node type; the `visibleWhen` change below widens both faces to the envelope the spec's own parse writes. +**Clause-②: yes (narrowing)**, shipped as `minor` per this repository's version policy. The removal narrows the TypeScript authoring face of every node type; the `visibleWhen` change below widens both faces to the envelope the spec's own parse writes. - **What does not move.** The zod faces keep their accept sets for every key but `visibleWhen`: the tolerant mirror is still `.passthrough()`, so `safeValidateSchema` keeps an undeclared key, and the derived strict face refuses it as before. `ComponentRendererProps`, the renderer props type, keeps its own index signature. Nothing a renderer draws changes. - **The bound.** TypeScript runs its excess-property check only on a fresh object literal. A value that reached its annotation through a variable of a wider type is not re-checked. - **`PartialSchema` works as written.** With the signature gone, `keyof T` is the literal member union again, so the alias keeps `T`'s declared members, optional, with `type` required. While the signature stood it declared `type` alone (objectui#6397). - **`BaseSchema.visibleWhen` is the spec's `EvaluatedExpressionInput`**, by reference: a predicate string, or the `{ dialect, source }` envelope. The zod twin takes `EvaluatedExpressionInputSchema`'s verdict without its transform, so a string parses to itself. A dialect-less envelope, an unknown dialect and a blank predicate are refused, as the spec refuses them. Both faces read `string` before, which refused the envelope a spec parse writes into this key. - **`@object-ui/plugin-kanban`.** `ObjectKanban` reads the `sort` the element data-source gate writes through a read type private to the package. `ObjectKanbanSchema` still declares no `sort` (objectui#8174). Nothing drawn changes. -- **`@object-ui/plugin-timeline`.** The renderer's private hand-off type declares the `onItemClick` slot `ObjectTimeline` composes; `TimelineSchema` still declares none. Nothing drawn changes. +- **`@object-ui/plugin-timeline`.** `TimelineRenderSchema`, the `schema` prop type of the exported `TimelineRenderer`, gains one optional member: the `onItemClick` slot `ObjectTimeline` composes. That is a one-member optional widening of an exported prop type. `TimelineSchema`, the authoring face, still declares no `onItemClick`. Nothing drawn changes. **Migration.** Where a literal stops compiling, the key is misspelled (fix it) or not declared on that node type (declare it on the type that reads it, by reference to the `@objectstack/spec` row, or remove it). Do not cast past the error. `props`, the legacy alias of `properties`, is not declared on the TypeScript face; the renderer still reads it, so write `properties`. From 032b9a2d14e20ee729310aee18c69aa048a0f249 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 11:31:28 +0000 Subject: [PATCH 09/14] docs(types): rewrite the comments objectui#8347 makes false (index signature now gone) Comment and prose bytes only. Every sentence that said `BaseSchema` (or a node type through it) carries, closes with or ends in `[key: string]: any`, every future-tense "objectui#8347 removes", and every TS2578 note that promised a directive goes unused when its member is deleted, now says what is true at this head: the signature is gone, a deletion is refused on a fresh literal and still rides a widened value, and the zod face keeps its `.passthrough()` reasoning, named as the zod face. AGENTS.md section 5 #0.1 gains the package-local hand-off-type route for a host-written runtime key (objectui#6356). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ --- AGENTS.md | 2 +- content/docs/plugins/plugin-chatbot.mdx | 6 +- .../aspect-ratio-demo-content-6773.test.tsx | 6 +- .../test/badge-demo-label-6829.test.tsx | 4 +- .../button-group-retired-keys-7077.test.ts | 4 +- .../test/card-demo-content-6788.test.tsx | 4 +- .../catalog-authored-key-6805-6806.test.tsx | 6 +- .../component-fixture-declared-keys.test.ts | 5 +- .../test/kanban-column-cards-6939.test.tsx | 6 +- .../src/__tests__/action-bar.test.tsx | 4 +- .../src/__tests__/basic-renderers.test.tsx | 2 +- .../__tests__/data-table-row-click.test.tsx | 4 +- .../empty-action-declared-7105.test.tsx | 4 +- .../src/__tests__/form-renderers.test.tsx | 4 +- .../list-title-declared-11347.test.tsx | 2 +- .../page-body-single-node-8310.test.tsx | 14 +-- .../__tests__/required-marker-markup.test.tsx | 4 +- .../span-children-rendering.test.tsx | 14 +-- .../__tests__/span-value-fallback.test.tsx | 12 ++- .../complex/dataTableBindDiagnostic.ts | 4 +- .../select-label-association.test.tsx | 4 +- packages/plugin-calendar/README.md | 10 +- .../plugin-calendar/src/ObjectCalendar.tsx | 4 +- .../calendarUnionReads-8651.test.tsx | 17 ++-- packages/plugin-charts/src/ObjectChart.tsx | 11 ++- .../__tests__/ObjectChart.compareTo.test.tsx | 2 +- .../ObjectChart.schemaAnchor-7946.test.ts | 13 +-- .../ObjectChartBlock.props-8885.test.tsx | 8 +- .../plugin-dashboard/src/ObjectPivotTable.tsx | 13 +-- .../ObjectDataTable.schemaAnchor-6576.test.ts | 6 +- .../widgetDataProviderRetired-7353.test.tsx | 5 +- .../plugin-dashboard/src/widgetDispatch.ts | 4 +- .../plugin-designer/src/DashboardEditor.tsx | 2 +- packages/plugin-form/src/ObjectForm.tsx | 5 +- packages/plugin-gantt/src/ObjectGantt.tsx | 7 +- packages/plugin-gantt/src/index.tsx | 6 +- packages/plugin-grid/src/ObjectGrid.tsx | 15 +-- .../dataTableSchemaSlot-6459.test.ts | 30 +++--- packages/plugin-kanban/src/index.tsx | 5 +- .../ObjectGallery.schemaAnchor-6576.test.ts | 7 +- packages/plugin-map/src/index.tsx | 6 +- .../object-timeline-navigation-8654.test.tsx | 15 +-- .../src/ObjectTree.schemaTyped-8655.test.ts | 17 ++-- packages/plugin-tree/src/ObjectTree.tsx | 13 ++- packages/plugin-view/src/ObjectView.tsx | 3 +- ...tView.kanbanConditionalFormatting.test.tsx | 2 +- packages/react/src/SchemaRenderer.tsx | 7 +- .../SchemaNode.reconciliation.test.ts | 4 +- .../__tests__/SchemaRenderer.aria.test.tsx | 4 +- .../SchemaRenderer.errorBoundary.test.tsx | 4 +- .../SchemaRenderer.expressions.test.tsx | 3 +- ...enderer.scopedStyleSchemaIdentity.test.tsx | 2 +- ...r.unevaluatedExpressionDiagnostic.test.tsx | 2 +- .../react/src/utils/propsBagDiagnostic.ts | 5 +- .../action-callback-retired-7068.test.ts | 13 ++- .../ai-zero-read-members-retired-8178.test.ts | 16 ++-- .../src/__tests__/ai-zod-arms-10859.test.ts | 2 +- ...rt-dialog-footer-keys-refusal-7963.test.ts | 5 +- .../alert-dialog-read-dialect-7104.test.ts | 2 +- .../app-actions-retired-7469.test.ts | 5 +- ...pp-menu-item-shortcut-refusal-7719.test.ts | 3 +- .../__tests__/authoring-nodes-11364.test.ts | 11 ++- ...authoring-nodes-bag-carriers-11468.test.ts | 6 +- .../src/__tests__/base-bind-declared.test.ts | 35 ++++--- .../base-schema-hidden-predicate.test.ts | 6 +- .../base-schema-label-vocabulary.test.ts | 14 +-- ...ase-schema-predicate-envelope-7530.test.ts | 5 +- .../base-schema-visible-predicate.test.ts | 8 +- .../button-group-doc-surface-6347.test.ts | 5 +- .../calendar-flat-color-allday-8466.test.ts | 19 ++-- .../__tests__/chart-data-model-7113.test.ts | 10 +- .../chart-inline-data-retired.test.ts | 4 +- .../chatbot-authoring-face-keys.test.ts | 3 +- .../chatbot-dark-keys-retired-7703.test.ts | 16 ++-- .../chatbot-display-mode-retired.test.ts | 25 ++--- ...-registration-authoring-faces-7655.test.ts | 16 ++-- .../checkbox-wrapper-class-6938.test.ts | 5 +- .../collapsible-open-refusal-8236.test.ts | 5 +- ...hboard-aria-retired-contract-twins.test.ts | 2 +- ...ard-widget-slot-component-arm-7952.test.ts | 9 +- .../data-table-declared-keys-6882.test.ts | 17 ++-- .../data-table-toolbar-retired.test.ts | 14 +-- .../designer-members-retired-11434.test.ts | 3 +- .../__tests__/designer-zod-arms-10859.test.ts | 2 +- .../detail-view-related-retired-7997.test.ts | 5 +- .../disabled-twin-symmetry-7087.test.ts | 5 +- .../flat-arm-responsive-styles-10872.test.ts | 5 +- ...ating-chatbot-trigger-icon-retired.test.ts | 5 +- .../src/__tests__/gantt-declared-keys.test.ts | 36 ++++--- ...-dependency-field-deprecated-alias.test.ts | 12 ++- .../gantt-flat-config-declared-keys.test.ts | 30 +++--- .../__tests__/gantt-search-keys-10250.test.ts | 14 +-- .../grid-flat-columns-retired-11505.test.ts | 5 +- .../handler-keys-json-refusal-6124.test.ts | 5 +- ...ndler-keys-string-any-mirrors-7344.test.ts | 10 +- .../inline-locale-declared-face-9092.test.ts | 8 +- .../input-otp-separator-11365.test.ts | 6 +- .../kanban-calendar-filter-sort-8174.test.ts | 22 +++-- .../markdown-inert-keys-retired-6972.test.ts | 21 +++-- .../mirror-only-published-keys-9729.test.ts | 4 +- .../__tests__/node-slot-union-11466.test.ts | 6 +- ...object-calendar-record-source-7313.test.ts | 5 +- .../object-chart-axis-config-10518.test.ts | 6 +- ...art-legacy-axis-keys-retired-10608.test.ts | 5 +- .../object-chart-undeclared-keys-8885.test.ts | 15 +-- ...ta-table-dataprovider-retired-7353.test.ts | 5 +- .../object-data-table-page-size-11348.test.ts | 5 +- .../object-gallery-filter-9309.test.ts | 9 +- .../object-grid-declared-keys-11068.test.ts | 15 +-- ...t-grid-default-sort-tombstone-5861.test.ts | 12 ++- ...ct-grid-show-filters-retired-11068.test.ts | 12 ++- ...kanban-allow-collapse-retired-8801.test.ts | 16 ++-- ...ct-kanban-calendar-navigation-8652.test.ts | 14 +-- .../object-kanban-card-title-9606.test.ts | 26 +++-- ...bject-kanban-columns-declared-8913.test.ts | 5 +- .../object-kanban-group-by-limit-7322.test.ts | 22 +++-- ...-kanban-on-quick-add-retired-11234.test.ts | 7 +- ...ject-kanban-quick-add-retired-8285.test.ts | 12 ++- .../__tests__/object-tree-filter-9549.test.ts | 9 +- .../object-view-slot-key-lists.test.ts | 6 +- .../__tests__/object-view-spec-parity.test.ts | 19 ++-- .../object-view-unmirrored-keys-7779.test.ts | 9 +- .../src/__tests__/p1-spec-alignment.test.ts | 16 ++-- .../__tests__/page-body-arity-8310.test.ts | 18 ++-- .../pivot-drilldown-retired-10932.test.ts | 4 +- .../registered-type-arms-10859-b2.test.ts | 2 +- ...eference-named-list-view-keys-7923.test.ts | 13 +-- .../sidebar-declared-surface-11465.test.ts | 7 +- .../stack-schema-emitted-members.test.ts | 10 +- .../__tests__/text-value-retired-6951.test.ts | 12 ++- .../timeline-dead-keys-retired-6170.test.ts | 14 +-- .../__tests__/timeline-declared-keys.test.ts | 39 ++++---- .../timeline-timescale-retired.test.ts | 21 +++-- .../src/__tests__/toast-button-keys.test.ts | 26 ++--- .../tree-view-data-optional-6939.test.ts | 6 +- .../tree-view-data-retired-6951.test.ts | 8 +- .../twins-spec-by-reference-9736.test.ts | 13 ++- .../undeclared-but-consumed-keys-6150.test.ts | 16 ++-- .../unmirrored-declared-round5-6152.test.ts | 6 +- .../widget-schema-anchors-6576.test.ts | 20 ++-- .../widget-schema-anchors-7946.test.ts | 12 ++- .../wrapper-class-declared-7722.test.ts | 5 +- .../src/__tests__/zod-mirror-parity.test.ts | 47 +++++----- packages/types/src/ai.ts | 17 ++-- packages/types/src/app.ts | 10 +- packages/types/src/authoring-nodes.ts | 8 +- packages/types/src/base.ts | 9 +- packages/types/src/complex.ts | 38 ++++---- packages/types/src/crud.ts | 4 +- packages/types/src/data-display.ts | 38 ++++---- packages/types/src/designer.ts | 12 ++- packages/types/src/feedback.ts | 2 +- packages/types/src/form.ts | 16 ++-- packages/types/src/index.ts | 2 +- packages/types/src/layout.ts | 43 +++++---- packages/types/src/navigation.ts | 5 +- packages/types/src/objectql.ts | 94 ++++++++++--------- packages/types/src/overlay.ts | 4 +- packages/types/src/reports.ts | 10 +- packages/types/src/views.ts | 11 ++- packages/types/src/zod/base.zod.ts | 2 +- packages/types/src/zod/objectql.zod.ts | 5 +- packages/types/src/zod/overlay.zod.ts | 5 +- packages/types/src/zod/views.zod.ts | 6 +- 164 files changed, 994 insertions(+), 753 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fc9c7f202d..02bb43170f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,7 +90,7 @@ interface BaseSchema { - **#-1 — English-only codebase.** This is an international OSS project. All user-facing text (component labels, buttons, titles, errors), code comments, docs (`README.md`, `docs/*.md`), and console/log messages MUST be English. No Chinese or other non-English in those. *(This rule governs the **codebase**; this instruction file may use Chinese in operational sections.)* - **#0 — Strict adherence to `@objectstack/spec`.** All schemas/JSON structures/types MUST follow `@objectstack/spec`. Don't invent schema properties — if the spec says `columns`, don't use `fields`. Check the spec before writing any `interface`/`type`. - **#0.1 — Fix the metadata, not the renderer (contract-first).** Corollary to #0. This is a metadata-driven system: `@objectstack/spec` is the contract between producers and this renderer. When a piece of metadata "doesn't render," ask **first**: *is it spec-compliant? is this the long-term-correct direction?* If the metadata is off-spec, fix it at the **producer** (and have it rejected at authoring/publish) — do **not** add a lenient fallback/alias in the renderer (reading both `columns` and `fields`, coercing a malformed shape, `??`-defaulting around bad input) to make non-compliant metadata "work." A tolerant fallback fossilizes the wrong convention into a second de-facto contract, dilutes the spec, and hides the producer's bug — one strict contract beats N dialects. We own both ends, so Postel's "be liberal in what you accept" does **not** apply (that's for untrusted boundaries). Change the **spec** only when it is genuinely wrong — deliberately, in `@objectstack/spec`, never by accreting renderer-side fallbacks. - - **The TypeScript authoring face is part of the contract (objectui#7927 ruling, executed by objectui#8347).** `BaseSchema` declares no index signature, so a node literal typed with its node type refuses, at the authoring site, a key no declaration names — a misspelling included. When a key you need is refused, this rule applies unchanged: declare it on the node type that reads it, by reference to the spec row, or fix the producer. ⛔ Never make it compile with `as any`, a cast to `Record`, a re-added index signature or an open `type: string` arm. Tolerance stays where it is deliberate: renderer props (`ComponentRendererProps` keeps its signature) and the tolerant zod face (`.passthrough()`); the derived strict zod face, `StrictAnyComponentSchema`, refuses an unknown key in metadata that arrives as data. ⚠️ The bound, said per #9: TypeScript excess-checks only a FRESH object literal, so a value that reached its annotation through a wider variable is not re-checked. `packages/types/src/__tests__/base-schema-closed-face-8347.test.ts` pins both directions and that bound. + - **The TypeScript authoring face is part of the contract (objectui#7927 ruling, executed by objectui#8347).** `BaseSchema` declares no index signature, so a node literal typed with its node type refuses, at the authoring site, a key no declaration names — a misspelling included. When a key you need is refused, this rule applies unchanged: declare it on the node type that reads it, by reference to the spec row, or fix the producer; a key only the host writes at runtime is typed on a package-local hand-off type where its producer and consumer live, never on the authoring face (objectui#6356). ⛔ Never make it compile with `as any`, a cast to `Record`, a re-added index signature or an open `type: string` arm. Tolerance stays where it is deliberate: renderer props (`ComponentRendererProps` keeps its signature) and the tolerant zod face (`.passthrough()`); the derived strict zod face, `StrictAnyComponentSchema`, refuses an unknown key in metadata that arrives as data. ⚠️ The bound, said per #9: TypeScript excess-checks only a FRESH object literal, so a value that reached its annotation through a wider variable is not re-checked. `packages/types/src/__tests__/base-schema-closed-face-8347.test.ts` pins both directions and that bound. - **#1 — Protocol-agnostic.** Never hardcode `objectql.find()`. Use the DataSource interface; inject `dataSource` via ``. - **#2 — Docs-driven.** For every feature/refactor, update package `README.md` **and** `content/docs/guide/*.md`. Not done until docs reflect the code. - **#3 — "Shadcn-native" aesthetics.** We are "serializable Shadcn". Follow Shadcn's DOM structure (`CardHeader`/`CardTitle`/`CardContent`). Always expose `className` in schema props so users can override via JSON. diff --git a/content/docs/plugins/plugin-chatbot.mdx b/content/docs/plugins/plugin-chatbot.mdx index c4bcfaf140..c8ea06e8d6 100644 --- a/content/docs/plugins/plugin-chatbot.mdx +++ b/content/docs/plugins/plugin-chatbot.mdx @@ -147,9 +147,9 @@ Each registration has its own importable authoring-face type in nineteen shared rows below are one declaration the two newer faces pick off `ChatbotSchema` by name, and a scoped row is declared only on the face(s) whose registration reads it. A key a registration ignores is therefore not a declared -member of its type. It still type-checks (`BaseSchema` ends in an index -signature, so an unlisted key is `any` rather than an error) and still parses -(the Zod twins are `.passthrough()`). It is then dropped silently at render +member of its type. In a typed literal it is a compile error (objectui#8347 +removed the `BaseSchema` index signature that typed an unlisted key `any`), but +it still parses (the Zod twins are `.passthrough()`). It is then dropped silently at render time, on all three registrations alike. `chatbot-floating` used to be the exception: its registration ended its panel element with an unfiltered props spread, so three keys its type does not declare (`processVisibility`, `surface`, diff --git a/examples/schema-catalog/test/aspect-ratio-demo-content-6773.test.tsx b/examples/schema-catalog/test/aspect-ratio-demo-content-6773.test.tsx index d74976f330..e0df317e9d 100644 --- a/examples/schema-catalog/test/aspect-ratio-demo-content-6773.test.tsx +++ b/examples/schema-catalog/test/aspect-ratio-demo-content-6773.test.tsx @@ -38,9 +38,9 @@ * and an empty ratio box clears it on the wrapper Radix draws for the ratio * itself. Its stronger control — the entry's own authored strings on screen — * is scoped to `NEWLY_REGISTERED_CATEGORIES`, which this family is not in. - * Nor could a parse have caught it: `BaseSchema` is `.passthrough()` and - * carries `[key: string]: any`, so `content` is accepted by zod and by tsc - * alike (the objectui#6157 class-3 shape). `check-doc-component-types.mjs` + * Nor could a parse have caught it: `BaseSchema` is `.passthrough()`, so + * `content` is accepted by zod (and was by tsc, through `[key: string]: any`, + * until objectui#8347) (the objectui#6157 class-3 shape). `check-doc-component-types.mjs` * rules the question out by name — "NOT in scope, deliberately: whether the * snippet's OTHER keys are read by the renderer the type resolves to". * diff --git a/examples/schema-catalog/test/badge-demo-label-6829.test.tsx b/examples/schema-catalog/test/badge-demo-label-6829.test.tsx index b488220534..517ecc6cb0 100644 --- a/examples/schema-catalog/test/badge-demo-label-6829.test.tsx +++ b/examples/schema-catalog/test/badge-demo-label-6829.test.tsx @@ -93,8 +93,8 @@ * * Arm A restored four demos and did not close the class: `children` was * declared on `BaseSchema`, accepted by every other container renderer, - * refused by neither zod nor tsc (`BaseSchema` is `.passthrough()` with an - * index signature), and consumed by the pipeline before it could leak to the + * refused by neither zod nor tsc (`BaseSchema` was `.passthrough()` with an + * index signature then), and consumed by the pipeline before it could leak to the * DOM — so the next author who wrote `children` on a badge drew an empty pill * again. Arm B (teaching `badge.tsx` to read `children`) widened a published * renderer's read set, which AGENTS.md #0.1 governs, and was left to diff --git a/examples/schema-catalog/test/button-group-retired-keys-7077.test.ts b/examples/schema-catalog/test/button-group-retired-keys-7077.test.ts index 231a2f0f53..9f09eafe5c 100644 --- a/examples/schema-catalog/test/button-group-retired-keys-7077.test.ts +++ b/examples/schema-catalog/test/button-group-retired-keys-7077.test.ts @@ -15,8 +15,8 @@ * All six fixtures in this category authored keys the shipped types do not * declare — `buttons[].value` ×17, `buttons[].icon` ×8, group-level `value` ×2 * and `selectionMode` ×2, 29 occurrences over six files. Nothing went red: - * `BaseSchema` is `.passthrough()` and carries `[key: string]: any`, so all 29 - * PARSED GREEN and type-checked — admitted unexamined, not refused (the reading + * `BaseSchema` is `.passthrough()` and carried `[key: string]: any` until + * objectui#8347, so all 29 PARSED GREEN and type-checked — admitted unexamined, not refused (the reading * `component-fixture-declared-keys.test.ts` and * `undeclared-but-consumed-keys-6150.test.ts` both record), and * `catalog-gallery-render.test.tsx` fails only on an unregistered `type`. So the diff --git a/examples/schema-catalog/test/card-demo-content-6788.test.tsx b/examples/schema-catalog/test/card-demo-content-6788.test.tsx index 9e716f04f9..8c0097be64 100644 --- a/examples/schema-catalog/test/card-demo-content-6788.test.tsx +++ b/examples/schema-catalog/test/card-demo-content-6788.test.tsx @@ -72,8 +72,8 @@ * 1. DECLARED — read off the shipped `CardSchema`'s own zod shape, not a * hand-copied list, so it follows the platform instead of yesterday's * vocabulary. `.success` is NOT the probe here and could not be: - * `BaseSchema` is `.passthrough()` and carries `[key: string]: any`, so - * `content` parses green and type-checks. The structural read is the only + * `BaseSchema` is `.passthrough()`, so `content` parses green (and it + * type-checked through `[key: string]: any` until objectui#8347). The structural read is the only * instrument that sees it. * 2. READ — the keys `card.tsx` reads, copied as literals on purpose: they * are the contract this file is about, and a renderer that starts reading diff --git a/examples/schema-catalog/test/catalog-authored-key-6805-6806.test.tsx b/examples/schema-catalog/test/catalog-authored-key-6805-6806.test.tsx index e86bb0fed8..3afe8018f8 100644 --- a/examples/schema-catalog/test/catalog-authored-key-6805-6806.test.tsx +++ b/examples/schema-catalog/test/catalog-authored-key-6805-6806.test.tsx @@ -104,9 +104,9 @@ * is objectui#6810, an open `needs-user-decision` card; this file deliberately * covers only the two renderers these two cards name. * - * Nor could a parse have caught either: `BaseSchema` is `.passthrough()` and - * carries `[key: string]: any`, so `content` is accepted by zod and by tsc - * alike. `check-doc-component-types.mjs` rules the question out by name. + * Nor could a parse have caught either: `BaseSchema` is `.passthrough()`, so + * `content` is accepted by zod (and was by tsc, through `[key: string]: any`, + * until objectui#8347). `check-doc-component-types.mjs` rules the question out by name. * * Module-scope import of `@object-ui/components`, not `beforeAll` (AGENTS.md * §测试纪律): registering the renderers is an unbounded module load and must diff --git a/examples/schema-catalog/test/component-fixture-declared-keys.test.ts b/examples/schema-catalog/test/component-fixture-declared-keys.test.ts index 931a192052..cdd135d3ee 100644 --- a/examples/schema-catalog/test/component-fixture-declared-keys.test.ts +++ b/examples/schema-catalog/test/component-fixture-declared-keys.test.ts @@ -37,8 +37,9 @@ * is therefore round-trip equality, not `.success`. * 3. **Declared-elsewhere, refused by neither** — `direction` on a * `radio-group`. `BaseSchema` is `.passthrough()` (`zod/base.zod.js:171`) - * AND carries `[key: string]: any` (`base.d.ts`), so zod and tsc both - * ACCEPT the key. The authority is that `RadioGroupSchema` declares + * AND carried `[key: string]: any` (`base.d.ts`) until objectui#8347, so + * zod and tsc both ACCEPTED the key (tsc refuses it in a typed literal + * now). The authority is that `RadioGroupSchema` declares * `orientation` (`form.d.ts:377`, `zod/form.zod.js:263`) and nothing reads * `direction`. The probe must be structural — asserting `.success` here * would assert nothing at all. diff --git a/examples/schema-catalog/test/kanban-column-cards-6939.test.tsx b/examples/schema-catalog/test/kanban-column-cards-6939.test.tsx index f04555e72d..47a9cf2fb4 100644 --- a/examples/schema-catalog/test/kanban-column-cards-6939.test.tsx +++ b/examples/schema-catalog/test/kanban-column-cards-6939.test.tsx @@ -259,9 +259,9 @@ describe('objectui#6939 — the mirror now accepts the spelling every board read // is STILL green, which is the finding that card had to make rather than // the one it was sent to make. Two corrections to the paragraph above: // - // 1. declaring on this face NARROWS, it does not widen — `BaseSchema` - // carries `[key: string]: any` / `.passthrough()`, so a declaration - // can only add validation where there was none; + // 1. declaring on this face NARROWED, it did not widen — `BaseSchema` + // carried `[key: string]: any` / `.passthrough()` then, so a + // declaration could only add validation where there was none; // 2. `columns[].items` was never refused BY NAME even on the retired // arm. `KanbanColumnSchema` is a plain (strip-postured) object, so // `items` was accepted and dropped there too; what refused the diff --git a/packages/components/src/__tests__/action-bar.test.tsx b/packages/components/src/__tests__/action-bar.test.tsx index d8f3f54b8f..43b631be9a 100644 --- a/packages/components/src/__tests__/action-bar.test.tsx +++ b/packages/components/src/__tests__/action-bar.test.tsx @@ -11,8 +11,8 @@ import type { ActionBarSchema } from '@object-ui/types'; /** * Types each literal below as the `action:bar` node it is rather than as the - * `BaseSchema` `renderComponent` accepts (objectui#11347): once `BaseSchema`'s - * index signature is gone (objectui#8347), a literal checked against + * `BaseSchema` `renderComponent` accepts (objectui#11347): since objectui#8347 + * removed `BaseSchema`'s index signature, a literal checked against * `BaseSchema` may author only `BaseSchema`'s own keys. */ const actionBar = (schema: ActionBarSchema): ActionBarSchema => schema; diff --git a/packages/components/src/__tests__/basic-renderers.test.tsx b/packages/components/src/__tests__/basic-renderers.test.tsx index 704553caf3..9893221fdc 100644 --- a/packages/components/src/__tests__/basic-renderers.test.tsx +++ b/packages/components/src/__tests__/basic-renderers.test.tsx @@ -30,7 +30,7 @@ import type { /** * Each literal below is typed as the node it is rather than as the * `BaseSchema` `renderComponent` accepts, children included (objectui#11347): - * once `BaseSchema`'s index signature is gone (objectui#8347), a literal + * since objectui#8347 removed `BaseSchema`'s index signature, a literal * checked against `BaseSchema` or `SchemaNode` may author only `BaseSchema`'s * own keys. */ diff --git a/packages/components/src/__tests__/data-table-row-click.test.tsx b/packages/components/src/__tests__/data-table-row-click.test.tsx index 32cbcc031c..f953fe016d 100644 --- a/packages/components/src/__tests__/data-table-row-click.test.tsx +++ b/packages/components/src/__tests__/data-table-row-click.test.tsx @@ -29,8 +29,8 @@ import type { DataTableSchema } from '@object-ui/types'; /** * Types a literal below as the node it is rather than as the `BaseSchema` - * `renderComponent` accepts (objectui#11347): once `BaseSchema`'s index - * signature is gone (objectui#8347), a literal checked against `BaseSchema` + * `renderComponent` accepts (objectui#11347): since objectui#8347 removed + * `BaseSchema`'s index signature, a literal checked against `BaseSchema` * may author only `BaseSchema`'s own keys. */ const dataTableNode = (schema: DataTableSchema): DataTableSchema => schema; diff --git a/packages/components/src/__tests__/empty-action-declared-7105.test.tsx b/packages/components/src/__tests__/empty-action-declared-7105.test.tsx index be9d2a61de..2f3960ce3d 100644 --- a/packages/components/src/__tests__/empty-action-declared-7105.test.tsx +++ b/packages/components/src/__tests__/empty-action-declared-7105.test.tsx @@ -22,7 +22,7 @@ * `className`, so the designer could not offer the key either; * * only the docs row said the capability existed. * - * The read compiled because `BaseSchema` ends in `[key: string]: any`, and the + * The read compiled because `BaseSchema` ended in `[key: string]: any`, and the * renderer spelled it `(schema as any).action` — which is also why * objectui#6150's census, scanning for `schema.KEY`, could not see this reader. * @@ -107,7 +107,7 @@ const DEMO_ACTION: ButtonSchema = { variant: 'default', }; // `DEMO_ACTION` is typed as the `button` node it is (objectui#11347): the slot is -// `SchemaNode`, and once `BaseSchema`'s index signature is gone (objectui#8347) +// `SchemaNode`, and since objectui#8347 removed `BaseSchema`'s index signature // a literal checked against it may author only `BaseSchema`'s own keys. const DEMO = { type: 'empty', diff --git a/packages/components/src/__tests__/form-renderers.test.tsx b/packages/components/src/__tests__/form-renderers.test.tsx index 696d7b2d72..3f4e37fc7d 100644 --- a/packages/components/src/__tests__/form-renderers.test.tsx +++ b/packages/components/src/__tests__/form-renderers.test.tsx @@ -30,8 +30,8 @@ import type { /** * Types a literal below as the node it is rather than as the `BaseSchema` - * `renderComponent` accepts, children included (objectui#11347): once - * `BaseSchema`'s index signature is gone (objectui#8347), a literal checked + * `renderComponent` accepts, children included (objectui#11347): since + * objectui#8347 removed `BaseSchema`'s index signature, a literal checked * against `BaseSchema` or `SchemaNode` may author only `BaseSchema`'s own keys. * The literals that deliberately author a retired or undeclared key stay * untyped, on purpose. diff --git a/packages/components/src/__tests__/list-title-declared-11347.test.tsx b/packages/components/src/__tests__/list-title-declared-11347.test.tsx index 5ebd3b09f2..ac9591118f 100644 --- a/packages/components/src/__tests__/list-title-declared-11347.test.tsx +++ b/packages/components/src/__tests__/list-title-declared-11347.test.tsx @@ -12,7 +12,7 @@ * The renderer has read `schema.title` all along, drawing it as a heading above * the list, and the `list` registration publishes it as an input. Until * objectui#11347 no declaration carried it, so it survived only on - * `BaseSchema`'s index signature, which objectui#8347 removes. The installed + * `BaseSchema`'s index signature, which objectui#8347 removed. The installed * `@objectstack/spec` has no row for `list`, and a published producer authors * the key: the `skills/objectui` expressions guide teaches * `{ "type": "list", "title": "Team", "ordered": true, ... }`. So the key is diff --git a/packages/components/src/__tests__/page-body-single-node-8310.test.tsx b/packages/components/src/__tests__/page-body-single-node-8310.test.tsx index 21b5294d76..f8ef211124 100644 --- a/packages/components/src/__tests__/page-body-single-node-8310.test.tsx +++ b/packages/components/src/__tests__/page-body-single-node-8310.test.tsx @@ -32,13 +32,15 @@ * `packages/types/src/__tests__/page-body-arity-8310.test.ts`. * - **objectui#8912 — evidence, and it is RENDER OUTPUT.** The three * `statistic` labels the flagship authors must appear in the rendered tree. - * ⛔ No compile-time check can stand in for this leg: `BaseSchema` is - * `.passthrough()` with an `[key: string]: any` index signature, so the - * defective key validated, type-checked, rode onto the node and drew nothing. + * ⛔ No compile-time check could stand in for this leg when it was written: + * `BaseSchema` was `.passthrough()` with an `[key: string]: any` index + * signature, so the defective key validated, type-checked, rode onto the + * node and drew nothing. * `check:doc-types` only asks whether a fence's `type` literal is registered - * (`grid` is), and `check:doc-examples` compiles against that same index - * signature. Both are green on the defect BY DESIGN (objectui#4823), so a - * green from either is not a reading of this card. + * (`grid` is), and `check:doc-examples` compiled against that same index + * signature (objectui#8347 removed it). Both were green on the defect BY + * DESIGN (objectui#4823), so a green from either is not a reading of this + * card. * * ## ⚠️ Why the negative-space leg renders the OLD spelling * diff --git a/packages/components/src/__tests__/required-marker-markup.test.tsx b/packages/components/src/__tests__/required-marker-markup.test.tsx index c07d582f0d..dbe0ff9a6e 100644 --- a/packages/components/src/__tests__/required-marker-markup.test.tsx +++ b/packages/components/src/__tests__/required-marker-markup.test.tsx @@ -58,8 +58,8 @@ import type { CheckboxSchema, ElementTextInputNode, InputSchema, SelectSchema, T /** * Types a literal below as the node it is rather than as the `BaseSchema` - * `renderComponent` accepts (objectui#11347): once `BaseSchema`'s index - * signature is gone (objectui#8347), a literal checked against `BaseSchema` + * `renderComponent` accepts (objectui#11347): since objectui#8347 removed + * `BaseSchema`'s index signature, a literal checked against `BaseSchema` * may author only `BaseSchema`'s own keys. */ const inputNode = (schema: InputSchema): InputSchema => schema; diff --git a/packages/components/src/__tests__/span-children-rendering.test.tsx b/packages/components/src/__tests__/span-children-rendering.test.tsx index 2345834e4f..33b9e0f921 100644 --- a/packages/components/src/__tests__/span-children-rendering.test.tsx +++ b/packages/components/src/__tests__/span-children-rendering.test.tsx @@ -27,10 +27,12 @@ * fossilize a second de-facto contract for the one type whose declaration never * named it (Commandment #0.1). The third case below pins that. * - * Note on what the type surface can and cannot say: `body` was never a type - * ERROR on a span, because `BaseSchema` declares both child keys and carries an - * index signature. So the read side is the only place this can be stated, which - * is exactly why it is stated here rather than left to review. + * Note on what the type surface could and could not say when this was written: + * `body` was not a type ERROR on a span, because `BaseSchema` declared both + * child keys and carried an index signature (`body` has since become a + * `?: never` tombstone, and objectui#8347 removed the signature). So the read + * side was the only place this could be stated, which is exactly why it is + * stated here rather than left to review. */ import { describe, it, expect } from 'vitest'; @@ -44,8 +46,8 @@ import '../renderers'; /** * Types a child literal below as the `text` node it is (objectui#11347): a - * `children` entry is checked against `SchemaNode`, and once `BaseSchema`'s - * index signature is gone (objectui#8347) a literal checked against it may + * `children` entry is checked against `SchemaNode`, and since objectui#8347 + * removed `BaseSchema`'s index signature a literal checked against it may * author only `BaseSchema`'s own keys. */ const textNode = (schema: TextSchema): TextSchema => schema; diff --git a/packages/components/src/__tests__/span-value-fallback.test.tsx b/packages/components/src/__tests__/span-value-fallback.test.tsx index e621594a81..ef8ecf5a54 100644 --- a/packages/components/src/__tests__/span-value-fallback.test.tsx +++ b/packages/components/src/__tests__/span-value-fallback.test.tsx @@ -34,9 +34,11 @@ * position. `body` stays refused — see `span-children-rendering.test.tsx`; it is * declared nowhere for this type, whereas `value` is declared twice. * - * What the type surface can and cannot say, same as on #5027: `BaseSchema` - * carries an index signature, so no spelling here is ever a TS error. The read - * side is the only place the contract can be stated, which is why it is pinned + * What the type surface could and could not say when this was written, same as + * on #5027: `BaseSchema` carried an index signature, so no spelling here was a + * TS error (objectui#8347 removed it, so an undeclared spelling in a typed + * literal is one now). The read side is the only place the precedence contract + * can be stated, which is why it is pinned * rather than left to review. */ @@ -51,8 +53,8 @@ import '../renderers'; /** * Types a child literal below as the `text` node it is (objectui#11347): a - * `children` entry is checked against `SchemaNode`, and once `BaseSchema`'s - * index signature is gone (objectui#8347) a literal checked against it may + * `children` entry is checked against `SchemaNode`, and since objectui#8347 + * removed `BaseSchema`'s index signature a literal checked against it may * author only `BaseSchema`'s own keys. */ const textNode = (schema: TextSchema): TextSchema => schema; diff --git a/packages/components/src/renderers/complex/dataTableBindDiagnostic.ts b/packages/components/src/renderers/complex/dataTableBindDiagnostic.ts index 2a11e8af7a..9741ff76d2 100644 --- a/packages/components/src/renderers/complex/dataTableBindDiagnostic.ts +++ b/packages/components/src/renderers/complex/dataTableBindDiagnostic.ts @@ -33,8 +33,8 @@ * EMPTY_ROWS` off the node, and the file calls no such hook. * * A `bind` on a `data-table` is nevertheless accepted by every gate: the TS - * side by `BaseSchema`'s `[key: string]: any`, the zod side by `BaseSchema` - * being `.passthrough()` (which `DataTableSchema.extend(…)` inherits). So the + * side because `BaseSchema` declares `bind` for every node (objectui#6357), the + * zod side by `BaseSchema` being `.passthrough()` (which `DataTableSchema.extend(…)` inherits). So the * author gets a table drawing a correct-looking header over the "No results * found" empty state, with no error, no warning and no diagnostic — the * hardest failure shape for a human OR an AI author to self-check, because a diff --git a/packages/components/src/renderers/form/__tests__/select-label-association.test.tsx b/packages/components/src/renderers/form/__tests__/select-label-association.test.tsx index 7e5bbd85d8..b022663af2 100644 --- a/packages/components/src/renderers/form/__tests__/select-label-association.test.tsx +++ b/packages/components/src/renderers/form/__tests__/select-label-association.test.tsx @@ -39,8 +39,8 @@ import type { SelectSchema } from '@object-ui/types'; /** * Types a literal below as the node it is rather than as the `BaseSchema` - * `renderComponent` and `SchemaRenderer` accept (objectui#11347): once `BaseSchema`'s index - * signature is gone (objectui#8347), a literal checked against `BaseSchema` + * `renderComponent` and `SchemaRenderer` accept (objectui#11347): since objectui#8347 removed + * `BaseSchema`'s index signature, a literal checked against `BaseSchema` * may author only `BaseSchema`'s own keys. */ const selectNode = (schema: SelectSchema): SelectSchema => schema; diff --git a/packages/plugin-calendar/README.md b/packages/plugin-calendar/README.md index 2f6f7e9a73..69b645eee4 100644 --- a/packages/plugin-calendar/README.md +++ b/packages/plugin-calendar/README.md @@ -303,11 +303,11 @@ import type { ObjectCalendarSchema } from '@object-ui/types'; // What this annotation buys, and what it does not - measured, objectui#7925. // It type-checks the VALUES of the declared keys: `defaultView: 'agenda'` and // `calendar: { titleField: 42 }` are both compile errors, and -// `check:doc-snippets` re-runs that check on every commit. It does NOT check -// key NAMES - this interface extends `BaseSchema`, whose `[key: string]: any` -// admits any spelling, and the `calendar` block is open the same way, so a -// misspelt key still compiles clean. Read the block as type-checked values, -// never as a guarded key set. +// `check:doc-snippets` re-runs that check on every commit. Top-level key NAMES +// are checked too since objectui#8347 removed `BaseSchema`'s `[key: string]: any`, +// but NOT inside the `calendar` block, which is open (its type is inferred from +// a `.passthrough()` schema), so a misspelt key there still compiles clean. Read +// the `calendar` block as type-checked values, never as a guarded key set. const schema: ObjectCalendarSchema = { type: 'object-calendar', objectName: 'events', diff --git a/packages/plugin-calendar/src/ObjectCalendar.tsx b/packages/plugin-calendar/src/ObjectCalendar.tsx index b7f1e6f997..78e6b41efe 100644 --- a/packages/plugin-calendar/src/ObjectCalendar.tsx +++ b/packages/plugin-calendar/src/ObjectCalendar.tsx @@ -94,8 +94,8 @@ import { * fifteen. * * ⚠️ But the cast count is NOT twelve, and an earlier cut of this comment said - * it was. `ObjectGridSchema` carries `BaseSchema`'s `[key: string]: any`, so on - * the UNION the five `CalendarSchema`-only keys resolved through that index + * it was. `ObjectGridSchema` carried `BaseSchema`'s `[key: string]: any` (until + * objectui#8347), so on the UNION the five `CalendarSchema`-only keys resolved through that index * signature and compiled with no cast at all. The checker on the union itself: * DECLARED 8 of 15 (`colorField` `dateField` `defaultView` `endField` `filter` * `objectName` `sort` `titleField`), NOT declared 7 (`allDayField` `calendar` diff --git a/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx b/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx index 34e64ac512..b154eef8fd 100644 --- a/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx +++ b/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx @@ -21,8 +21,8 @@ * * ⚠️ Note what the union itself answers, because it is easy to get backwards * and this file's first cut did: on the UNION only SEVEN of the fifteen reads - * were undeclared. `ObjectGridSchema` carries `BaseSchema`'s index signature, - * so the five `CalendarSchema`-only keys resolved through it and compiled with + * were undeclared. `ObjectGridSchema` carried `BaseSchema`'s index signature + * (until objectui#8347), so the five `CalendarSchema`-only keys resolved through it and compiled with * no cast — silently typed `any`, which is the defect rather than the absence * of one. Per-ARM is the reading the card tabled; per-UNION is the reading the * compiler acts on; they are different numbers and both are measured here. @@ -132,9 +132,11 @@ * * ## The ceiling, stated rather than assumed (objectui#5155 / #7927) * - * `BaseSchema` ends in `[key: string]: any` and its mirror is `.passthrough()`, - * so declaring a key buys VALUE validation and never buys rejection of a - * MISSPELLING. The counter-probe below pins that honestly, so nobody reads this + * The mirror is `.passthrough()`, so declaring a key buys VALUE validation and + * never buys rejection of a MISSPELLING on that face. (The TS face refused none + * either while `BaseSchema` ended in `[key: string]: any`; objectui#8347 removed + * it, so a typed literal refuses a misspelling now.) The counter-probe below + * pins the mirror half honestly, so nobody reads this * file as claiming more than it does. */ @@ -264,8 +266,9 @@ type Expect = T; /** * `Equal`, not `extends`: a union arm carrying `BaseSchema`'s index signature - * makes an UNDECLARED member read `any`, and a one-way check accepts `any` on - * both sides — which is precisely the before-state this card removes. + * (every extender did until objectui#8347) made an UNDECLARED member read `any`, + * and a one-way check accepts `any` on both sides — which is precisely the + * before-state this card removed. */ export type assertionSchemaPropIsThePublishedElementSchema = Expect>; diff --git a/packages/plugin-charts/src/ObjectChart.tsx b/packages/plugin-charts/src/ObjectChart.tsx index 4424db15c9..a9d0469349 100644 --- a/packages/plugin-charts/src/ObjectChart.tsx +++ b/packages/plugin-charts/src/ObjectChart.tsx @@ -452,11 +452,12 @@ export { extractRecords } from '@object-ui/core'; * * With the anchor, a wrong VALUE TYPE on a declared key is a compile error at * the producer (`xAxisKey: 42`, `series: 'x'`, `type: 'chart'`, and every - * `BaseSchema` member — `visible: 42`). ⚠️ A MISSPELLED key is still accepted: - * `BaseSchema` carries `[key: string]: any` (objectui#5155), the same ceiling - * objectui#6576 accepted knowingly. `__tests__/ObjectChart.schemaAnchor-7946.test.ts` - * pins both halves, the ceiling included, so the anchor is not read as more - * than it is. + * `BaseSchema` member — `visible: 42`). ⚠️ A MISSPELLED key was still accepted + * while `BaseSchema` carried `[key: string]: any` (objectui#5155), the same + * ceiling objectui#6576 accepted knowingly; objectui#8347 removed it, so a + * misspelled key in a fresh literal is refused now. + * `__tests__/ObjectChart.schemaAnchor-7946.test.ts` pins both halves, the + * flipped ceiling row included, so the anchor is not read as more than it is. */ export interface ObjectChartProps { /** diff --git a/packages/plugin-charts/src/__tests__/ObjectChart.compareTo.test.tsx b/packages/plugin-charts/src/__tests__/ObjectChart.compareTo.test.tsx index bbfe16327b..c705264edc 100644 --- a/packages/plugin-charts/src/__tests__/ObjectChart.compareTo.test.tsx +++ b/packages/plugin-charts/src/__tests__/ObjectChart.compareTo.test.tsx @@ -109,7 +109,7 @@ const comparisonFromOf = (src: { aggregate: any }) => * What it buys: objectui#8885 (PR objectui#8895) declares `compareTo` on * `ObjectChartSchema` bound to that same symbol. With `unknown` here, the * literal below stops compiling the moment the two PRs are UNIONISED — a defect - * neither branch can see alone, because on this branch the key still rides + * neither branch could see alone, because on this branch the key still rode * `BaseSchema`'s index signature. Measured on the merge of the two heads: the * union's `tsc -p tsconfig.test.json` reported this line, and it was masked in * the obvious reading because `type-check` is `tsc --noEmit && tsc -p diff --git a/packages/plugin-charts/src/__tests__/ObjectChart.schemaAnchor-7946.test.ts b/packages/plugin-charts/src/__tests__/ObjectChart.schemaAnchor-7946.test.ts index 8fd3b4e60f..d31a3bde98 100644 --- a/packages/plugin-charts/src/__tests__/ObjectChart.schemaAnchor-7946.test.ts +++ b/packages/plugin-charts/src/__tests__/ObjectChart.schemaAnchor-7946.test.ts @@ -37,12 +37,13 @@ * * ## The ceiling, stated rather than assumed (objectui#5155) * - * Anchoring buys DECLARED members their declared types. It does NOT buy - * rejection of a MISSPELLING: `BaseSchema` carries `[key: string]: any`, which - * `ObjectChartSchema` inherits, so `xAxisKy` compiles. objectui#6576 accepted - * that cost knowingly for the gallery; the counter-probe at the bottom keeps it - * visible so nobody reads this anchor as more than it is. Closing it is - * objectui#5155, not this card. + * Anchoring buys DECLARED members their declared types. It did NOT buy + * rejection of a MISSPELLING while `BaseSchema` carried `[key: string]: any`, + * which `ObjectChartSchema` inherited, so `xAxisKy` compiled. objectui#6576 + * accepted that cost knowingly for the gallery; the counter-probe at the bottom + * kept it visible so nobody read this anchor as more than it was. Closing it was + * objectui#5155's, not this card's: objectui#8347 closed it, and the + * counter-probe is flipped. * * The schema type's own members, the read census and the source-level pins are * in `packages/types/src/__tests__/widget-schema-anchors-7946.test.ts`. diff --git a/packages/plugin-charts/src/__tests__/ObjectChartBlock.props-8885.test.tsx b/packages/plugin-charts/src/__tests__/ObjectChartBlock.props-8885.test.tsx index a06ad6069f..79e896bb33 100644 --- a/packages/plugin-charts/src/__tests__/ObjectChartBlock.props-8885.test.tsx +++ b/packages/plugin-charts/src/__tests__/ObjectChartBlock.props-8885.test.tsx @@ -28,10 +28,10 @@ * * ## The ceiling, stated rather than assumed * - * A closed PROPS type refuses a misspelled prop NAME. It does not refuse a - * misspelled key INSIDE the node: `BaseSchema` carries `[key: string]: any`, - * the protocol's passthrough. The counter-probe at the bottom keeps that - * visible so nobody reads this pin as more than it is. + * A closed PROPS type refuses a misspelled prop NAME. It did not refuse a + * misspelled key INSIDE the node while `BaseSchema` carried + * `[key: string]: any`; objectui#8347 removed it, and the counter-probe at the + * bottom that kept that visible is flipped (see its note). */ import { describe, it, expect } from 'vitest'; diff --git a/packages/plugin-dashboard/src/ObjectPivotTable.tsx b/packages/plugin-dashboard/src/ObjectPivotTable.tsx index 13284e7bcc..4e4cd6a1d7 100644 --- a/packages/plugin-dashboard/src/ObjectPivotTable.tsx +++ b/packages/plugin-dashboard/src/ObjectPivotTable.tsx @@ -44,10 +44,11 @@ const EMPTY_ROWS = Object.freeze([]) as unknown as any[]; * below, and intersecting it with the tombstone would collapse that member to * `never`: the one pivot that drills could not be authored with a drill. * - * Key remapping, not `Omit`: `PivotTableSchema` extends `BaseSchema`, whose - * `[key: string]: any` widens `keyof` to all of `string`, so - * `Omit` would keep the index signature and drop - * every declared member. + * Key remapping, not `Omit`: written while `PivotTableSchema` inherited + * `BaseSchema`'s `[key: string]: any`, which widened `keyof` to all of + * `string`, so `Omit` would have kept the index + * signature and dropped every declared member. objectui#8347 removed the + * signature. */ type PivotTableSchemaWithoutDrillDown = { [K in keyof PivotTableSchema as K extends 'drillDown' ? never : K]: PivotTableSchema[K]; @@ -66,8 +67,8 @@ export interface ObjectPivotTableProps { * writes it and no reader was added. * * A `?: never` tombstone, not a plain deletion: `PivotTableSchema` extends - * `BaseSchema`, whose `[key: string]: any` would absorb a deleted member - * silently at any value. Licensed by prong 1 of the discriminator + * `BaseSchema`, whose `[key: string]: any` absorbed a deleted member + * silently at any value until objectui#8347 (a widened value still would). Licensed by prong 1 of the discriminator * (objectui#5941, #7526, in its amended form, `5f8190c8c`): it names the live * replacement, `objectName`. A JSON-authored value is refused by name at * parse time too, since objectui#11440: the `object-pivot` arm of diff --git a/packages/plugin-dashboard/src/__tests__/ObjectDataTable.schemaAnchor-6576.test.ts b/packages/plugin-dashboard/src/__tests__/ObjectDataTable.schemaAnchor-6576.test.ts index 43ac96e1aa..9b41407445 100644 --- a/packages/plugin-dashboard/src/__tests__/ObjectDataTable.schemaAnchor-6576.test.ts +++ b/packages/plugin-dashboard/src/__tests__/ObjectDataTable.schemaAnchor-6576.test.ts @@ -30,8 +30,10 @@ * - WIDENS, in declaration only: `drillDown` / `onRowClick` are now DECLARED * with real types — before, they compiled through the index signature as * `any`, which is why a wrong-shaped `drillDown` compiled too. - * - UNCHANGED, pinned honestly: an unknown key still compiles, because - * `BaseSchema`'s `[key: string]: any` is inherited (objectui#5155). + * - UNCHANGED then, pinned honestly: an unknown key still compiled, because + * `BaseSchema`'s `[key: string]: any` was inherited (objectui#5155). + * objectui#8347 removed it, and that row is flipped: an unknown key is + * refused now. */ import { describe, it, expect } from 'vitest'; diff --git a/packages/plugin-dashboard/src/__tests__/widgetDataProviderRetired-7353.test.tsx b/packages/plugin-dashboard/src/__tests__/widgetDataProviderRetired-7353.test.tsx index 2364860a1a..a9a025e0c0 100644 --- a/packages/plugin-dashboard/src/__tests__/widgetDataProviderRetired-7353.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/widgetDataProviderRetired-7353.test.tsx @@ -206,8 +206,9 @@ describe('objectui#7353 — DashboardGridLayout: the table and pivot nodes carry * consumer-side half of the types tombstone: red on a stale `dist`. * * Why a tombstone and not a deletion: both carriers extend `BaseSchema`, whose - * `[key: string]: any` absorbs a deleted member silently at any value — an - * authored `dataProvider` would still compile and still do nothing. The + * `[key: string]: any` absorbed a deleted member silently at any value until + * objectui#8347 (a widened value still would) — an authored `dataProvider` + * would still compile there and still do nothing. The * `@ts-expect-error` directives are real enforcement because this package's * `type-check` chains `tsc -p tsconfig.test.json`; vitest erases them. */ diff --git a/packages/plugin-dashboard/src/widgetDispatch.ts b/packages/plugin-dashboard/src/widgetDispatch.ts index 2daed62abc..3c792cef6a 100644 --- a/packages/plugin-dashboard/src/widgetDispatch.ts +++ b/packages/plugin-dashboard/src/widgetDispatch.ts @@ -51,8 +51,8 @@ import type { MetricWidgetProps } from './MetricWidget'; * {@link isSlotComponentEntry} first (objectui#11598, N2 A), the read rule * `DashboardComponentSchema.widgets` states. The component arm declares none * of them, and a component node in the slot draws itself whatever else it - * carries; reading one off the entry whichever arm it is compiled only through - * `BaseSchema`'s index signature, which objectui#8347 removes. + * carries; reading one off the entry whichever arm it was compiled only through + * `BaseSchema`'s index signature, which objectui#8347 removed. */ export type DashboardWidgetSlotEntry = DashboardComponentSchema['widgets'][number]; diff --git a/packages/plugin-designer/src/DashboardEditor.tsx b/packages/plugin-designer/src/DashboardEditor.tsx index c36b87233e..08bce46f2e 100644 --- a/packages/plugin-designer/src/DashboardEditor.tsx +++ b/packages/plugin-designer/src/DashboardEditor.tsx @@ -100,7 +100,7 @@ type DashboardWidgetEntry = DashboardComponentSchema['widgets'][number]; * widget arm alone, so every such read below narrows with this first * (objectui#11598, N2 A): the component arm declares none of them, and reading * one off the entry whichever arm it was compiled only through `BaseSchema`'s - * index signature, which objectui#8347 removes. + * index signature, which objectui#8347 removed. * * The twin of plugin-dashboard's `isSlotComponentEntry` (`widgetDispatch.ts`), * which this package cannot import: it does not depend on plugin-dashboard diff --git a/packages/plugin-form/src/ObjectForm.tsx b/packages/plugin-form/src/ObjectForm.tsx index 2ae1112530..10053fabef 100644 --- a/packages/plugin-form/src/ObjectForm.tsx +++ b/packages/plugin-form/src/ObjectForm.tsx @@ -159,8 +159,9 @@ type ObjectFormLabelKey = (typeof OBJECT_FORM_LABEL_KEYS)[number]; /** * `ObjectFormSchema` as every presentation below reads it: the seven * `I18nLabel` members already resolved to a string. A homomorphic mapped type - * rather than `Omit`, because `BaseSchema`'s index signature makes `Omit` - * collapse to that signature and drop every declared member. + * rather than `Omit`, because `BaseSchema`'s index signature made `Omit` + * collapse to that signature and drop every declared member (until + * objectui#8347 removed it). */ type LocalizedObjectFormSchema = { [K in keyof ObjectFormSchema]: K extends ObjectFormLabelKey ? string : ObjectFormSchema[K]; diff --git a/packages/plugin-gantt/src/ObjectGantt.tsx b/packages/plugin-gantt/src/ObjectGantt.tsx index 77f2d782b6..b9a8e46f94 100644 --- a/packages/plugin-gantt/src/ObjectGantt.tsx +++ b/packages/plugin-gantt/src/ObjectGantt.tsx @@ -311,9 +311,10 @@ export interface ObjectGanttProps { * objectui#6051 declared what the FLAT branch reads: the 24 flattened * `GanttConfig` keys `getGanttConfig`'s first branch consumes, plus the * `staticData` / `filter` / `sort` the fetch path reads. The grid-style - * `{ gantt: { … } }` block keeps working exactly as before and is still read - * through the index signature — declaring it is the one change that would not - * have been additive, and it is severed to objectui#6475. The registered + * `{ gantt: { … } }` block kept working exactly as before and was still read + * through the index signature — declaring it was the one change that would not + * have been additive, so it was severed to objectui#6475, which has since + * declared it (`gantt?: GanttConfig`). The registered * renderer (`index.tsx`) still passes `schema: any`, so no runtime shape is * turned away either way. */ diff --git a/packages/plugin-gantt/src/index.tsx b/packages/plugin-gantt/src/index.tsx index 7848b3afa5..38398f1004 100644 --- a/packages/plugin-gantt/src/index.tsx +++ b/packages/plugin-gantt/src/index.tsx @@ -212,9 +212,9 @@ ComponentRegistry.register('object-gantt', ObjectGanttRenderer, { * * ## Why unregistering is the whole retirement here — measured, not assumed * - * ⚠️ `BaseSchema` closes with `[key: string]: any` and `BaseSchemaCore` ends - * `.passthrough()`, so a dropped MEMBER KEY is KEPT, not refused (the - * objectui#7664 failure). That hazard does not reach a TYPE NAME on this + * ⚠️ `BaseSchemaCore` ends `.passthrough()` (and `BaseSchema` closed with + * `[key: string]: any` until objectui#8347), so a dropped MEMBER KEY is KEPT, + * not refused, on the zod face (the objectui#7664 failure). That hazard does not reach a TYPE NAME on this * surface, and the reason is structural rather than lucky: no schema face in * `@object-ui/types` ever declared `gantt` as a component node type — measured * whole-repo, zero declarations, against a firing control of two for diff --git a/packages/plugin-grid/src/ObjectGrid.tsx b/packages/plugin-grid/src/ObjectGrid.tsx index 004444984f..197738d44a 100644 --- a/packages/plugin-grid/src/ObjectGrid.tsx +++ b/packages/plugin-grid/src/ObjectGrid.tsx @@ -1092,15 +1092,17 @@ export type ObjectGridColumn = * PLUS an undeclared `bogusKeyForProbe6459: true` WRITTEN OUT LONGHAND in * the (fresh) literal → `tsc --noEmit` exit 0, ZERO diagnostics. * - * The reason is `BaseSchema`'s `[key: string]: any` index signature, which - * `DataTableSchema` inherits: under an index signature EVERY key is a member, + * The reason was `BaseSchema`'s `[key: string]: any` index signature, which + * `DataTableSchema` inherited until objectui#8347: under an index signature + * EVERY key is a member, * so excess-property checking never has a non-member to refuse — at a fresh * literal, through a spread, anywhere. It is the terminal case of the rule the * `options` tombstone above records ("a pin enforced by a key's non-membership * silently stops enforcing the moment the key becomes a member"): with an * index signature there is no non-membership to enforce with, ever. * - * `RemoveIndexSignature` strips it — DERIVED from `DataTableSchema`, never a + * `RemoveIndexSignature` stripped it (since objectui#8347 it strips nothing on + * `DataTableSchema`) — DERIVED from `DataTableSchema`, never a * hand-copied member list, so a member added there tomorrow flows in on its * own and two enumerations of one vocabulary never exist. With the signature * gone, the same probe goes red (TS2353 naming the key), measured at both @@ -1189,7 +1191,7 @@ type RemoveIndexSignature = { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K]; }; -/** `DataTableSchema`'s DECLARED members only — the index signature stripped. */ +/** `DataTableSchema`'s DECLARED members only — any index signature stripped (it has none since objectui#8347). */ export type DeclaredDataTableSchema = RemoveIndexSignature; /** @@ -1234,8 +1236,9 @@ export type DeclaredDataTableSchema = RemoveIndexSignature; * pinned, so a member added or removed here reddens too. ⚠️ The transplant is * NOT literal in one respect, deliberately: the probe reads * `DeclaredDataTableSchema`, not `DataTableSchema`, because the index signature - * stripped above makes the raw type answer "declared" to EVERY key — measured, - * and pinned in that suite as its own control. + * stripped above made the raw type answer "declared" to EVERY key — measured, + * and pinned in that suite as its own control, which is flipped since + * objectui#8347 removed the signature. * * ⇒ Anything in this census that a reader would otherwise have to re-measure by * hand is still prose; what is mechanical is the ENTRY CONDITION of the two diff --git a/packages/plugin-grid/src/__tests__/dataTableSchemaSlot-6459.test.ts b/packages/plugin-grid/src/__tests__/dataTableSchemaSlot-6459.test.ts index f8a76927c5..3af9fe65bb 100644 --- a/packages/plugin-grid/src/__tests__/dataTableSchemaSlot-6459.test.ts +++ b/packages/plugin-grid/src/__tests__/dataTableSchemaSlot-6459.test.ts @@ -22,10 +22,11 @@ * both writers are literals sitting DIRECTLY in annotated positions, so * excess-property checking is the live instrument — PROVIDED the type has no * index signature. `BaseSchema`'s `[key: string]: any` (which `DataTableSchema` - * inherits) makes every key a member, and a check that refuses non-members has - * nothing to refuse when non-membership cannot exist. `RemoveIndexSignature` in - * `ObjectGrid.tsx` is what turns the annotation from inert to able-to-fail, - * and the pins below hold each half of that claim separately. + * inherited until objectui#8347) made every key a member, and a check that + * refuses non-members has nothing to refuse when non-membership cannot exist. + * `RemoveIndexSignature` in `ObjectGrid.tsx` is what turned the annotation from + * inert to able-to-fail (it strips nothing since objectui#8347), and the pins + * below hold each half of that claim separately. */ import { describe, it, expect } from 'vitest'; import type { DataTableSchema, TableColumn } from '@object-ui/types'; @@ -60,11 +61,12 @@ type HeldLocally = 'UNDECLARED by DataTableSchema — this hold is LOAD-BEARING. Re-derive the census prose in ObjectGrid.tsx (the census section, and the ObjectGridDataTableSchemaHolds docblock under it) before changing anything.'; /** - * ⚠️ Probes `DeclaredDataTableSchema`, NOT `DataTableSchema`, and that is not - * cosmetic: `DataTableSchema` inherits `BaseSchema`'s `[key: string]: any`, so - * `string extends keyof DataTableSchema` and the membership question is - * ALWAYS-TRUE against it — an instrument with no `false` to give. The third - * test below pins both halves of that, so the choice is measured, not asserted. + * ⚠️ Probes `DeclaredDataTableSchema`, NOT `DataTableSchema`, and that was not + * cosmetic: `DataTableSchema` inherited `BaseSchema`'s `[key: string]: any` + * until objectui#8347, so `string extends keyof DataTableSchema` and the + * membership question was ALWAYS-TRUE against it — an instrument with no + * `false` to give. The control test below pinned both halves of that, and is + * flipped since objectui#8347 made the raw type able to answer `false` too. */ type HoldVerdict = K extends keyof DeclaredDataTableSchema ? DeclaredUpstream @@ -279,12 +281,12 @@ describe('objectui#6459 — the schema slot annotation is an instrument, not a d /** * ⭐ THE CONTROL ON THE CHOICE OF TYPE, which is the one place this transplant * is NOT literal. Read against the raw `DataTableSchema`, the membership - * question is ALWAYS-TRUE: `BaseSchema`s `[key: string]: any` makes - * `string extends keyof DataTableSchema`, so a nonsense key is a member. A + * question was ALWAYS-TRUE: `BaseSchema`s `[key: string]: any` made + * `string extends keyof DataTableSchema`, so a nonsense key was a member. A * gate written the obvious way — asking `DataTableSchema` whether it declares - * the held key — would therefore be an instrument that can only answer "yes", - * green forever: the same failure objectui#7201 was filed about, one level up. - * The first line pins that blindness; the second pins that the strip + * the held key — would therefore have been an instrument that can only answer + * "yes", green forever: the same failure objectui#7201 was filed about, one + * level up. The first line pinned that blindness; the second pins that the strip * `ObjectGrid.tsx` already derives is what restores a usable `false`. * * The first line went red when objectui#8347 dropped `BaseSchema`'s index diff --git a/packages/plugin-kanban/src/index.tsx b/packages/plugin-kanban/src/index.tsx index a2fee7368f..011a248f1f 100644 --- a/packages/plugin-kanban/src/index.tsx +++ b/packages/plugin-kanban/src/index.tsx @@ -365,8 +365,9 @@ export const KanbanRenderer: React.FC = ({ schema, objectFi * * ## Why unregistering is the whole retirement * - * ⚠️ `BaseSchema` closes with `[key: string]: any` and `BaseSchemaCore` ends - * `.passthrough()`, so a dropped MEMBER KEY is KEPT, not refused (objectui#7664). + * ⚠️ `BaseSchemaCore` ends `.passthrough()` (and `BaseSchema` closed with + * `[key: string]: any` until objectui#8347), so a dropped MEMBER KEY is KEPT, + * not refused, on the zod face (objectui#7664). * That hazard needs a schema face to arise on, and this key never had one: * measured whole-repo, `@object-ui/types` declares `kanban-ui` as a component * node type ZERO times (firing control: `object-kanban`, 2 — `objectql.ts` and diff --git a/packages/plugin-list/src/__tests__/ObjectGallery.schemaAnchor-6576.test.ts b/packages/plugin-list/src/__tests__/ObjectGallery.schemaAnchor-6576.test.ts index 0f06c2f3d9..aef6bfe9b6 100644 --- a/packages/plugin-list/src/__tests__/ObjectGallery.schemaAnchor-6576.test.ts +++ b/packages/plugin-list/src/__tests__/ObjectGallery.schemaAnchor-6576.test.ts @@ -23,9 +23,10 @@ * - WIDENS: every `BaseSchema` member is now writable. `visibleWhen` — a real * base member — was a compile error on the literal. * - NARROWS: `type` is now required, and pinned to the registry key. - * - UNCHANGED, pinned honestly: an unknown key still compiles, because - * `BaseSchema`'s `[key: string]: any` is inherited (objectui#5155). The - * ruling accepted that cost; the counter-probe below keeps it visible. + * - UNCHANGED then, pinned honestly: an unknown key still compiled, because + * `BaseSchema`'s `[key: string]: any` was inherited (objectui#5155). The + * ruling accepted that cost; objectui#8347 removed the signature, and the + * counter-probe below is flipped: an unknown key is refused now. * * The schema type's own members and the widget's read census are pinned in * `packages/types/src/__tests__/widget-schema-anchors-6576.test.ts`. diff --git a/packages/plugin-map/src/index.tsx b/packages/plugin-map/src/index.tsx index fe787831e5..50ac8ca93d 100644 --- a/packages/plugin-map/src/index.tsx +++ b/packages/plugin-map/src/index.tsx @@ -141,9 +141,9 @@ ComponentRegistry.register('object-map', ObjectMapRenderer, { * * ## Why unregistering is the whole retirement here — measured, not assumed * - * ⚠️ `BaseSchema` closes with `[key: string]: any` and `BaseSchemaCore` ends - * `.passthrough()`, so a dropped MEMBER KEY is KEPT, not refused (the - * objectui#7664 failure). That hazard needs a schema face to arise on, and this + * ⚠️ `BaseSchemaCore` ends `.passthrough()` (and `BaseSchema` closed with + * `[key: string]: any` until objectui#8347), so a dropped MEMBER KEY is KEPT, + * not refused, on the zod face (the objectui#7664 failure). That hazard needs a schema face to arise on, and this * TYPE NAME never had one: in `@object-ui/types` the literal `'map'` appears * only in the stored view-type unions (`NamedListView.type`, * `defaultViewType`), never as a component node type, against a firing control diff --git a/packages/plugin-timeline/src/__tests__/object-timeline-navigation-8654.test.tsx b/packages/plugin-timeline/src/__tests__/object-timeline-navigation-8654.test.tsx index 1887ff268d..027d3de1e4 100644 --- a/packages/plugin-timeline/src/__tests__/object-timeline-navigation-8654.test.tsx +++ b/packages/plugin-timeline/src/__tests__/object-timeline-navigation-8654.test.tsx @@ -139,9 +139,10 @@ type Equal = type Expect = T; /** - * `Equal`, not `extends`: an undeclared member reads `any` through - * `BaseSchema`'s index signature, and `any` passes a one-way check on both - * sides — which is precisely the before-state this card removes. + * `Equal`, not `extends`: an undeclared member read `any` through + * `BaseSchema`'s index signature until objectui#8347, and `any` passes a + * one-way check on both sides — which is precisely the before-state this card + * removed. */ export type assertionTimelineNavigationIsTheSpecType = Expect>; @@ -150,9 +151,11 @@ export type assertionEqualCanFail = Expect