diff --git a/.changeset/calendar-view-mode-agenda-retired-5740.md b/.changeset/calendar-view-mode-agenda-retired-5740.md new file mode 100644 index 000000000..f50f849b4 --- /dev/null +++ b/.changeset/calendar-view-mode-agenda-retired-5740.md @@ -0,0 +1,31 @@ +--- +'@object-ui/types': minor +--- + +`'agenda'` leaves `CalendarViewMode` and the zod `CalendarViewModeSchema` +(objectui#5740 — the value-level residue of objectui#5667's key-level +convergence of `CalendarViewSchema` on the registered `calendar-view` +renderer's measured read set; ADR-0049 enforce-or-remove). + +The union declared a value nothing enforced: the registered renderer's `view` +input declares `enum: ['month','week','day']`, `resolveAuthoredView` resolves +any off-enum value — `'agenda'` included — to `undefined` (the component's +`'month'` default), and `CalendarView` renders no agenda view. An author +writing the type-legal, zod-valid `view: 'agenda'` got a month calendar with +no error or warning. No in-repo, example, or catalog app authors +`view: 'agenda'` (measured during objectui#5667's sweep and re-measured for +this change, including the objectstack tree). + +**This narrows the accept set — unlike #5667's key retirements, which created +no new rejections.** `view` is a declared key, and declared keys are validated +even under `.passthrough()`, so `view: 'agenda'` is now a **validation error +that previously parsed green** (an `invalid_value` issue on the `view` path, +offering `month`/`week`/`day`). Undeclared keys still pass through unchanged. +Breaking on the published zod surface; ships as `minor` per this repo's +version-alignment policy (majors track `@objectstack`). + +The runtime boundary is unchanged: an off-union `view` in raw metadata still +falls back to the component's `'month'` default at the renderer, and the +registry input already declared the three-value enum. Docblocks, the schema +reference table, and the zod `describe` no longer teach an `'agenda'` +fallback. diff --git a/content/docs/api/schema-reference.md b/content/docs/api/schema-reference.md index 6a65a83a7..fea5bf427 100644 --- a/content/docs/api/schema-reference.md +++ b/content/docs/api/schema-reference.md @@ -1076,7 +1076,7 @@ authored `events` is dropped by design, objectui#4433). | `endDateField` | `string` | Record field for the event end date/time. Default `"end"`. | | `allDayField` | `string` | Record field for the all-day flag. Default `"allDay"`. | | `colorField` | `string` | Record field for the event color. Default `"color"`. | -| `view` | `CalendarViewMode` | View mode: `"month"`, `"week"`, `"day"`. Any other value falls back to `"month"`. | +| `view` | `CalendarViewMode` | View mode: `"month"`, `"week"`, `"day"` — the full union. `"agenda"` was retired in objectui#5740 and now fails validation. Default `"month"`. | | `currentDate` | `string \| Date` | Initial calendar date — an ISO date string when authored as JSON. | | `allowCreate` | `boolean` | Show the "New event" affordance; clicking it dispatches a `create` action. Default `false`. | | `onEventClick` | `function` | Host-only: forwarded when a React host supplies a function; authored JSON cannot produce one. | diff --git a/packages/types/src/__tests__/calendar-view-mode-agenda-retired.test.ts b/packages/types/src/__tests__/calendar-view-mode-agenda-retired.test.ts new file mode 100644 index 000000000..a5319c755 --- /dev/null +++ b/packages/types/src/__tests__/calendar-view-mode-agenda-retired.test.ts @@ -0,0 +1,117 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Retirement pin — `'agenda'` leaves `CalendarViewMode` and + * `CalendarViewModeSchema` (objectui#5740, the value-level residue of + * objectui#5667's key-level convergence on the `calendar-view` renderer's + * measured read set). + * + * The declared union named `'agenda'`, but the registered renderer's `view` + * input declares `enum: ['month','week','day']`, `resolveAuthoredView` + * resolves any off-enum value to `undefined` (the component's `'month'` + * default), and `CalendarView` renders no agenda view — so an author writing + * the type-legal, zod-valid `view: 'agenda'` got a month calendar with no + * error. Declared ≠ enforced, on a published surface (ADR-0049). + * + * ⚠️ Unlike #5667's key retirements, this one NARROWS the accept set: `view` + * is a DECLARED key, and declared keys are validated even under + * `.passthrough()`, so `view: 'agenda'` — which parsed green before — is now + * a validation error. That is exactly why the zod half is pinned by the + * REFUSAL ENVELOPE (a `view`-path `invalid_value` issue naming the surviving + * vocabulary) rather than a bare `success === false`, and why the passthrough + * control below pins that the rejection is declared-key validation, not a + * strictness change. The TS half erases at runtime, so it is pinned with + * `@ts-expect-error`, which is real enforcement here because + * `packages/types/tsconfig.test.json` is chained from this package's + * `type-check` script (#3009). + * + * The runtime resolver is deliberately NOT changed: an off-union value in raw + * metadata still falls back to `'month'` at the renderer boundary + * (`calendar-view-renderer.propsContract.test.tsx` pins that branch). + */ + +import { describe, it, expect } from 'vitest'; +import { CalendarViewModeSchema, CalendarViewSchema } from '../zod/complex.zod.js'; +import type { CalendarViewMode } from '../complex.js'; + +/** The retired value, and the survivors the renderer actually renders. */ +const RETIRED = 'agenda'; +const SURVIVORS = ['month', 'week', 'day'] as const; + +describe('CalendarViewModeSchema — the shared enum no longer offers the retired value', () => { + it('is exactly the rendered set', () => { + expect(CalendarViewModeSchema.options).toEqual([...SURVIVORS]); + }); + + it('rejects the retired value with an invalid_value issue', () => { + const result = CalendarViewModeSchema.safeParse(RETIRED); + expect(result.success).toBe(false); + if (result.success) return; + expect(result.error.issues).toHaveLength(1); + expect(result.error.issues[0].code).toBe('invalid_value'); + }); +}); + +describe('CalendarViewSchema.view — the NEW rejection this retirement creates', () => { + it("rejects `view: 'agenda'` on the `view` path, where it previously parsed green", () => { + const result = CalendarViewSchema.safeParse({ type: 'calendar-view', view: RETIRED }); + + expect(result.success).toBe(false); + if (result.success) return; + + // The envelope, not just the verdict: exactly one issue, on the `view` + // path, coded as a closed-vocabulary violation. A bare `success === false` + // would also pass if the node had been rejected for an unrelated reason. + // + // The offending value is deliberately NOT asserted to appear in the + // message: measured on zod 4, an `invalid_value` issue carries the ALLOWED + // `values` and no echo of the input. + const viewIssues = result.error.issues.filter((i) => i.path.join('.') === 'view'); + expect(viewIssues).toHaveLength(1); + expect(viewIssues[0].code).toBe('invalid_value'); + + // The shrink, read off the refusal: the retired value is gone from the + // offered vocabulary and every survivor is still in it. + const offered = (viewIssues[0] as { values?: unknown[] }).values ?? []; + expect(offered).not.toContain(RETIRED); + for (const survivor of SURVIVORS) expect(offered).toContain(survivor); + }); + + it('still accepts every rendered view mode in full', () => { + // Full green parses, not merely "no `view` issue": a value-level shrink + // that quietly invalidated a survivor would otherwise go unseen. + for (const survivor of SURVIVORS) { + const result = CalendarViewSchema.safeParse({ type: 'calendar-view', view: survivor }); + expect(result.success).toBe(true); + } + }); + + it('passthrough control: an UNDECLARED key still parses — the rejection above is declared-key validation, not a strictness change', () => { + // `BaseSchema` is `.passthrough()`. This pin keeps the two facts apart: + // unknown keys pass (unchanged since #5667), while a declared key's value + // is validated (the new rejection above). If this control ever reds, the + // schema's strictness changed — a different contract decision than #5740. + const result = CalendarViewSchema.safeParse({ + type: 'calendar-view', + someUndeclaredKey: RETIRED, + }); + expect(result.success).toBe(true); + }); +}); + +describe('the published TS twin no longer offers the retired value', () => { + it('`CalendarViewMode` rejects it at compile time and keeps the survivors', () => { + // @ts-expect-error — 'agenda' left the union (objectui#5740). + const retired: CalendarViewMode = RETIRED; + expect(retired).toBe(RETIRED); + + const survivors: CalendarViewMode[] = [...SURVIVORS]; + expect(survivors).toEqual([...SURVIVORS]); + }); +}); diff --git a/packages/types/src/complex.ts b/packages/types/src/complex.ts index b272f3ac0..25e25ebae 100644 --- a/packages/types/src/complex.ts +++ b/packages/types/src/complex.ts @@ -127,9 +127,14 @@ export interface KanbanSchema extends BaseSchema { } /** - * Calendar view mode + * Calendar view mode — the registered `calendar-view` renderer's rendered set. + * + * `'agenda'` was retired from this union (objectui#5740): no view ever + * rendered it — the renderer resolved it to the `'month'` default — and no + * measured app authors it (ADR-0049 enforce-or-remove, the value-level + * residue of objectui#5667's key-level convergence). */ -export type CalendarViewMode = 'month' | 'week' | 'day' | 'agenda'; +export type CalendarViewMode = 'month' | 'week' | 'day'; /** * Calendar event @@ -225,9 +230,9 @@ export interface CalendarViewSchema extends BaseSchema { /** * Calendar view mode. * - * The registered renderer renders `'month' | 'week' | 'day'` and falls back - * to `'month'` for any other value — including `'agenda'`, which - * {@link CalendarViewMode} still names. + * {@link CalendarViewMode} equals the renderer's rendered set since + * objectui#5740 retired `'agenda'`; at runtime the renderer still resolves + * any off-union value in raw metadata to the `'month'` default. * @default 'month' */ view?: CalendarViewMode; diff --git a/packages/types/src/zod/complex.zod.ts b/packages/types/src/zod/complex.zod.ts index 2f437c2b2..de3d5f874 100644 --- a/packages/types/src/zod/complex.zod.ts +++ b/packages/types/src/zod/complex.zod.ts @@ -66,9 +66,13 @@ export const KanbanSchema = BaseSchema.extend({ }); /** - * Calendar View Mode + * Calendar View Mode — the registered renderer's rendered set. + * + * `'agenda'` was retired (objectui#5740): no view ever rendered it, and no + * measured app authors it. `view` is a DECLARED key, so this retirement is a + * new rejection — see the accept-set note on {@link CalendarViewSchema}. */ -export const CalendarViewModeSchema = z.enum(['month', 'week', 'day', 'agenda']); +export const CalendarViewModeSchema = z.enum(['month', 'week', 'day']); /** * Calendar Event Schema @@ -97,6 +101,12 @@ export const CalendarEventSchema = z.object({ * `BaseSchema` is `.passthrough()`, so the retired keys are not REJECTED here * — they are simply no longer declared or type-checked. The material accept * change is that `events` is no longer required. + * + * Value-level residue (objectui#5740): `'agenda'` left + * `CalendarViewModeSchema`. Unlike the key retirements above, this IS a new + * rejection — `view` is a declared key, and declared keys are validated even + * under `.passthrough()` — so `view: 'agenda'`, which parsed green before, + * now fails with an `invalid_value` issue on the `view` path. */ export const CalendarViewSchema = BaseSchema.extend({ type: z.literal('calendar-view'), @@ -118,7 +128,7 @@ export const CalendarViewSchema = BaseSchema.extend({ allDayField: z.string().optional().describe("Record field for the all-day flag (default 'allDay')"), colorField: z.string().optional().describe("Record field for the event color (default 'color')"), view: CalendarViewModeSchema.optional().describe( - "View mode (the renderer renders 'month' | 'week' | 'day'; other values fall back to 'month')", + "View mode — 'month' | 'week' | 'day', the renderer's rendered set ('agenda' was retired: objectui#5740)", ), currentDate: z .union([z.string(), z.date()])