From 5c2ff8413d8b1127296afa15ee1a2f5dd6531f72 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 07:01:10 +0000 Subject: [PATCH 1/7] fix(plugin-calendar): type ObjectCalendar at the published object-calendar schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ObjectCalendarComponentProps.schema` was `ObjectGridSchema | CalendarSchema` — a grid's schema plus a plugin-local interface absent from this package's barrel. Neither arm is the schema of the element this renderer is registered as, so of the fifteen keys it reads off the node, four were declared on neither arm and eight on exactly one, and every one of them needed a cast to read. Measured with the TypeScript checker (`getPropertyOfType`, never a grep): `ObjectCalendarSchema` already declared eleven of the fifteen. - `ObjectCalendarComponentProps.schema` is now `ObjectCalendarSchema`. Breaking for a host that passed an `object-grid` node or a bare `type: 'calendar'` literal; neither is a node this renderer is registered for. The shape matches every sibling widget, `plugin-map` included, which is registered under two tags with one published props type exactly as this package is. - `ObjectCalendarSchema.calendar` is declared on both published faces, derived from the spec's `CalendarConfigSchema` plus objectui's own `allDayField` and kept `.passthrough()` — value validation where there was none, no new refusal. - The `dateField` / `endField` alias rungs are retired: no producer, fixture, test or doc writes them, and the spec refuses both by name in both positions. - Two pins re-anchored off spellings this change had to move (objectui#8832), and the new spec dependency registered in `SPEC_DERIVED_PAIRS`. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt --- .changeset/olive-buckets-scream.md | 12 + .../plugin-calendar/src/ObjectCalendar.tsx | 171 ++++++-- .../calendarUnionReads-8651.test.tsx | 382 ++++++++++++++++++ ...object-calendar-record-source-7313.test.ts | 52 ++- .../src/__tests__/zod-mirror-parity.test.ts | 7 + packages/types/src/objectql.ts | 31 +- packages/types/src/zod/objectql.zod.ts | 63 +++ 7 files changed, 663 insertions(+), 55 deletions(-) create mode 100644 .changeset/olive-buckets-scream.md create mode 100644 packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx diff --git a/.changeset/olive-buckets-scream.md b/.changeset/olive-buckets-scream.md new file mode 100644 index 0000000000..fe56a529ad --- /dev/null +++ b/.changeset/olive-buckets-scream.md @@ -0,0 +1,12 @@ +--- +"@object-ui/plugin-calendar": minor +"@object-ui/types": minor +--- + +fix(plugin-calendar): type `ObjectCalendar` at the published `object-calendar` schema, and declare the `calendar` container + +`ObjectCalendarComponentProps.schema` was the union `ObjectGridSchema | CalendarSchema` — a grid's schema, plus a plugin-local interface absent from this package's barrel. Neither arm is the schema of the element this renderer is registered as, so twelve of the fifteen keys it reads off the node were undeclared on the union and had to be read through a cast. `ObjectCalendarSchema` already declared eleven of them. + +- `ObjectCalendarComponentProps.schema` is now `ObjectCalendarSchema`. **Breaking for a React host that passed an `object-grid` node or a `type: 'calendar'` literal to ``** — neither was ever a node this renderer is registered for. `ObjectCalendarProps`, the deprecated alias, follows it. +- `ObjectCalendarSchema.calendar` is declared on both published faces, bound to `@objectstack/spec`'s `CalendarConfigSchema` plus objectui's own `allDayField`. The container is kept `.passthrough()`, so nothing that parsed before is refused now; what is new is value validation — `calendar: 42` and `calendar: { startDateField: 42 }` are refused where both were admitted unexamined. +- The `dateField` / `endField` alias rungs inside `getCalendarConfig` are retired. Nothing wrote them: no producer, fixture, test or doc in this repo, and `@objectstack/spec` refuses both by name at the flat position and inside `CalendarConfigSchema`. A node configured only through a retired spelling now draws the existing "Calendar configuration required" refusal instead of resolving. diff --git a/packages/plugin-calendar/src/ObjectCalendar.tsx b/packages/plugin-calendar/src/ObjectCalendar.tsx index 771ce5c749..a5abb3d8c4 100644 --- a/packages/plugin-calendar/src/ObjectCalendar.tsx +++ b/packages/plugin-calendar/src/ObjectCalendar.tsx @@ -23,7 +23,7 @@ */ import React, { useEffect, useState, useCallback, useMemo } from 'react'; -import type { ObjectGridSchema, DataSource, CalendarConfig } from '@object-ui/types'; +import type { ObjectCalendarSchema, DataSource, CalendarConfig, ViewData } from '@object-ui/types'; import { CalendarView, type CalendarViewEvent } from './CalendarView'; import { usePullToRefresh } from '@object-ui/mobile'; import { @@ -74,18 +74,36 @@ import { ValueDataSource, } from '@object-ui/core'; -export interface CalendarSchema { - type: 'calendar'; - objectName?: string; - dateField?: string; - endField?: string; - titleField?: string; - colorField?: string; - filter?: any; - sort?: any; - /** Initial view mode */ - defaultView?: 'month' | 'week' | 'day'; -} +/** + * ⛔ RETIRED (objectui#8651) — a plugin-local `CalendarSchema` used to sit here, + * and `ObjectCalendarComponentProps.schema` below was the union of it with + * `ObjectGridSchema`. Both arms are gone; the prop is now the published schema + * of the element this file is registered as (`index.tsx`, both tags). + * + * What the union cost, measured with the TypeScript checker on the current tree + * (`getPropertyOfType`, never a grep — objectui#8410): of the fifteen keys this + * renderer reads off the node, FOUR were declared on neither arm and EIGHT on + * exactly one, so every one of them needed a cast to read — while + * `ObjectCalendarSchema` already declared ELEVEN of the fifteen. + * + * The two arms failed differently and neither was salvageable: + * + * - `ObjectGridSchema` is `type: 'object-grid'`. No producer hands this + * component one: the single call site is this package's `index.tsx`, whose + * two registrations both publish `OBJECT_CALENDAR_INPUTS`. + * - the local interface was absent from this package's barrel, so no importer + * could name it, and it SHADOWED `@object-ui/types`' own published + * `CalendarSchema` — the date-picker primitive reachable at `ui:calendar` + * only (objectui#8499). Its two distinctive members, the retired spellings + * noted on `getCalendarConfig` below, had no producer at all. + * + * The shape this leaves is the family's: `ObjectKanban` takes + * `ObjectKanbanSchema`, `ObjectGantt` takes `ObjectGanttSchema`, `ObjectMap` + * takes `ObjectMapSchema` — and `plugin-map` is registered under two tags with + * one published props type, exactly as this package is. + * + * Pinned in `__tests__/calendarUnionReads-8651.test.tsx`. + */ /** * Props of the `ObjectCalendar` React component. @@ -104,7 +122,7 @@ export interface CalendarSchema { * no importer breaks. Tripwire: `__tests__/spec-symbol-4650.test.ts`. */ export interface ObjectCalendarComponentProps { - schema: ObjectGridSchema | CalendarSchema; + schema: ObjectCalendarSchema; dataSource?: DataSource; className?: string; /** Pre-fetched records passed by a parent (e.g. ObjectView). When provided, skips internal data fetching. */ @@ -205,20 +223,49 @@ type ObjectCalendarConfig = CalendarConfig & { allDayField?: string; }; -function getCalendarConfig(schema: ObjectGridSchema | CalendarSchema): ObjectCalendarConfig | null { - // The declared configuration container. - if ((schema as any).calendar) { - return (schema as any).calendar as ObjectCalendarConfig; +/** + * ⛔ TWO ALIAS RUNGS RETIRED HERE (objectui#8651), and they were the only + * members the retired plugin-local interface had that this schema does not. + * + * The flat branch below used to read a second spelling under each of the two + * date bindings — one for the start field, one for the end — falling back to it + * when the canonical spelling was absent. Censused on the current tree, each + * with a control that hits: ZERO producers anywhere in this repo write either + * onto a calendar node (every hit belongs to the sibling `timeline` widget), + * ZERO fixtures, ZERO tests and ZERO docs teach them, and `@objectstack/spec` + * refuses both BY NAME — with an `unrecognized_keys` diagnostic at the flat + * position AND inside `CalendarConfigSchema`, which is a `strictObject` of four + * keys. So no conforming author could write them and no write door could store + * them. + * + * ⛔ Per AGENTS.md #0.1 the remedy for a second spelling belongs at the + * producer, and there is no producer: a tolerant rung kept here would be a + * second de-facto contract with nothing on the other end of it. The node that + * carried ONLY a retired spelling now falls through to `null`, which the + * "Calendar configuration required" refusal screen answers by name — the same + * degradation objectui#7711 chose for the same reason, one key over. Pinned, + * with its positive control, in `__tests__/calendarUnionReads-8651.test.tsx`. + * + * ⛔ No casts remain in this function. Every key it reads is now a declared + * member of the parameter type, which is the whole point of objectui#8651 — and + * `packages/types/src/__tests__/calendar-flat-color-allday-8466.test.ts` reads + * this function's reads as a FACT rather than as a spelling (objectui#8832), so + * dropping them moves no verdict there. + */ +function getCalendarConfig(schema: ObjectCalendarSchema): ObjectCalendarConfig | null { + // The declared configuration container — read FIRST, as the spec declares it. + if (schema.calendar) { + return schema.calendar as ObjectCalendarConfig; } - - // Check for flat properties (used by ObjectView) - if ((schema as any).startDateField || (schema as any).dateField) { + + // The flat spelling, which `ObjectView` and `ListView` emit. + if (schema.startDateField) { return { - startDateField: (schema as any).startDateField || (schema as any).dateField, - endDateField: (schema as any).endDateField || (schema as any).endField, - titleField: (schema as any).titleField, - colorField: (schema as any).colorField, - allDayField: (schema as any).allDayField + startDateField: schema.startDateField, + endDateField: schema.endDateField, + titleField: schema.titleField, + colorField: schema.colorField, + allDayField: schema.allDayField } as ObjectCalendarConfig; } @@ -281,7 +328,7 @@ export const ObjectCalendar: React.FC = ({ // this component rather than remounting it. const [unscheduledOpen, setUnscheduledOpen] = useState(false); const isMobile = useIsMobile(); - const schemaDefaultView = (schema as any).defaultView as 'month' | 'week' | 'day' | undefined; + const schemaDefaultView = schema.defaultView; // Lazy initializer: read window.innerWidth synchronously so SSR-friendly // useIsMobile (which returns false on first render) doesn't lock us into // a 24-hour day grid on phones. @@ -336,9 +383,33 @@ export const ObjectCalendar: React.FC = ({ // the registration's description promises) AND reaches the component as the // `data` PROP through `index.tsx`'s `resolveExternalData`, which is what // actually draws it. - const dataConfig = useMemo(() => resolveRecordSourceConfig(schema, 'array'), [ - (schema as any).data, - (schema as any).staticData, + // + // ⚠️ NAMED SEAM, and the reason the three members are passed one by one + // instead of handing the whole node over (objectui#8651). The shared + // resolver's PARAMETER declares `data?: ViewData`, but its own `dataArm` + // contract — and its `authoredDataIsOnTheDeclaredArm` predicate, which takes + // `unknown` — admit an ARRAY on the `'array'` arm, which is exactly the arm + // this block declares (`ObjectCalendarSchema.data`, objectui#9239/#8348). So + // the signature contradicts the function's own documented contract, and the + // retired union hid it: `ObjectGridSchema.data` is `ViewData`, so the call + // type-checked while saying something this block does not mean. + // + // ⛔ That is an upstream defect in `@object-ui/core`, NOT a licence to widen + // anything here (AGENTS.md #0.1) — and `packages/core/` is outside this + // card's file surface. Reported rather than patched. This spelling passes + // only the three members the resolver documents itself as reading, with the + // `data` member named at the arm this block declares, so the RUNTIME value + // reaching the predicate is byte-for-byte the one `schema.data` held before. + const dataConfig = useMemo(() => resolveRecordSourceConfig( + { + objectName: schema.objectName, + data: schema.data as ViewData | undefined, + staticData: schema.staticData, + }, + 'array', + ), [ + schema.data, + schema.staticData, schema.objectName, ]); // Every key `getCalendarConfig` reads, and nothing it does not. @@ -350,12 +421,18 @@ export const ObjectCalendar: React.FC = ({ // filter change for nothing. // // Dropping it also removes an accidental co-trigger, so the three flat keys - // the function reads but this list never named are added in the same edit: - // `startDateField` and `endDateField` (the canonical half of the - // `dateField` / `endField` pairs below, which WERE named) and `allDayField`. - // Before this change a simultaneous `filter` change could recompute the memo - // and pick those up by luck; that luck is now gone, so the list has to be - // honest. Pinned in `__tests__/ObjectCalendar.filterIsNotAConfigSlot-7711.test.tsx`. + // the function reads but this list never named were added in the same edit: + // `startDateField`, `endDateField` and `allDayField`. Before that change a + // simultaneous `filter` change could recompute the memo and pick those up by + // luck; that luck is now gone, so the list has to be honest. Pinned in + // `__tests__/ObjectCalendar.filterIsNotAConfigSlot-7711.test.tsx`. + // + // ⛔ Two more entries left this list in objectui#8651, and they are NOT a + // third accidental omission: the two retired alias rungs noted on + // `getCalendarConfig` above. The function no longer reads either spelling, so + // depending on them would be the mirror-image dishonesty — a dependency on a + // key that can no longer change the result. This list is exactly what the + // function reads, in both directions. // // ⭐ objectui#8026 — `allDayField` is now LOAD-BEARING here, not merely // honest. When #7711 named it, nothing read the key, so the dependency could @@ -368,14 +445,12 @@ export const ObjectCalendar: React.FC = ({ // now depend on this entry live in // `__tests__/ObjectCalendar.allDayFieldIsHonoured-8026.test.tsx`. const calendarConfig = useMemo(() => getCalendarConfig(schema), [ - (schema as any).calendar, - (schema as any).startDateField, - (schema as any).dateField, - (schema as any).endDateField, - (schema as any).endField, - (schema as any).titleField, - (schema as any).colorField, - (schema as any).allDayField + schema.calendar, + schema.startDateField, + schema.endDateField, + schema.titleField, + schema.colorField, + schema.allDayField ]); const hasInlineData = dataConfig?.provider === 'value'; /** @@ -800,6 +875,14 @@ export const ObjectCalendar: React.FC = ({ // CLOSED, not open — do not re-open it as a cleanup. If bucket-vocabulary // unification ever becomes a product direction that is a fresh ruling, // with visual-regression evidence across all four surfaces in one stroke. + // ⛔ The ONE cast objectui#8651 left standing, deliberately. `navigation` is + // objectui#8652's key: the maintainer ruled B there — declare it on the + // platform element schemas first, then mirror — and that card is `pm:blocked` + // on objectstack#17987. Its declaredness verdict at this read site is + // UNCHANGED by this card: through the retired union it was undeclared too + // (one arm declared it, and a union member is declared only when EVERY arm + // does), and it is undeclared on `ObjectCalendarSchema`. Ledgered by name, and + // asserted to be STILL READ, in `__tests__/calendarUnionReads-8651.test.tsx`. const navConfig = (schema as any).navigation ?? { mode: 'drawer' }; const navIsOverlay = navConfig.mode === 'drawer' || navConfig.mode === 'modal' || navConfig.mode === 'split' || navConfig.mode === 'popover'; const navigation = useNavigationOverlay({ diff --git a/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx b/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx new file mode 100644 index 0000000000..5cd58a7e5f --- /dev/null +++ b/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx @@ -0,0 +1,382 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#8651 — `ObjectCalendar` is typed at the PUBLISHED schema of the + * element it is registered as, and every key it reads off the node is declared + * there. + * + * ## What was measured, and why the card's three-way split collapsed to two + * + * The card filed twelve keys in two classes — four undeclared on both arms of + * `ObjectGridSchema | CalendarSchema`, eight declared on exactly ONE arm — and + * asked, as its FIRST question, whether that union is right at all. Measured + * with the TypeScript checker on the current tree (`getPropertyOfType`, never a + * grep — objectui#8410), the answer decides eleven of the twelve at once: + * + * `ObjectCalendarSchema` — this repo's published interface for the + * `object-calendar` element, and its zod mirror — ALREADY declares eleven of + * the fifteen keys this renderer reads, `allDayField`, `startDateField`, + * `endDateField`, `titleField`, `colorField` and `defaultView` among them + * (objectui#8466, #8174, #8314, #9239). Neither arm of the union it was + * actually annotated with is that interface. + * + * ⇒ the reads were never undeclared for want of a declaration; they were + * undeclared because the props type pointed somewhere else. Both arms go: + * + * - `ObjectGridSchema` is `type: 'object-grid'`. Nothing hands this component + * one — the single production call site is this package's own + * `index.tsx`, and both of its registrations (`object-calendar` and + * `calendar`) publish `OBJECT_CALENDAR_INPUTS`, the `object-calendar` + * surface. + * - `CalendarSchema` was declared LOCALLY in `ObjectCalendar.tsx`, absent + * from this package's barrel — so no importer could ever name it — and it + * SHADOWED `@object-ui/types`' own published `CalendarSchema`, which is the + * date-picker primitive (`form.ts`, reachable at `ui:calendar` only, + * objectui#8499). Two layers, one word. + * + * The shape this leaves is the one every sibling widget in the family already + * has: `ObjectKanban` takes `ObjectKanbanSchema`, `ObjectGantt` takes + * `ObjectGanttSchema`, `ObjectMap` takes `ObjectMapSchema` — and `plugin-map` + * is registered under TWO tags (`object-map` and `map`) with a single published + * props type, exactly as this package is. + * + * ## The two keys that do NOT come along, and their separate exits + * + * - `calendar` is declared by NO face of this repo, while + * `@objectstack/spec`'s `ComponentPropsMap['object-calendar']` declares it + * as the configuration container AND this package's registration `inputs` + * publishes it AND `getCalendarConfig` reads it FIRST. Triage's ruling for + * the family (objectui#8327, comment `5619610246`) makes that exit + * mechanical: declared in spec ⇒ align the mirror. Declared here. + * - `dateField` / `endField` were declared ONLY on the retired local + * interface and read only as fallback rungs beneath the canonical + * spellings. Nothing writes them: zero producers, zero fixtures, zero + * tests, zero docs — each census run below with a control that hits — and + * the spec refuses both BY NAME at the flat position and inside + * `CalendarConfigSchema`. ⇒ retired, per AGENTS.md #0.1: the remedy for a + * second spelling belongs at the producer, and there is no producer. + * + * ## ⛔ `navigation` is NOT ruled here, and its verdict is INVARIANT + * + * objectui#8652 carries the `navigation` family; the maintainer ruled B + * (declare on the platform element schemas first, then mirror), the spec half + * is objectstack#17987, and #8652 is `pm:blocked` on it. This card must not + * rule, declare, retire or touch it. + * + * ⭐ It does not, and that is measurable rather than asserted: through the + * UNION the key was already undeclared (`ObjectGridSchema` declares it, + * `CalendarSchema` does not, and a union member is declared only when EVERY arm + * declares it), and on `ObjectCalendarSchema` it is undeclared too. Same + * verdict at the read site before and after; the read itself is untouched. It + * is ledgered BY NAME below, with an assertion that it is STILL READ — a stale + * exception is a hole (the objectui#8885 ledger discipline). + * + * ## What the population assertion is, and why it is not a written-down list + * + * AGENTS.md #9: a claim that needs a live population is defended by an + * instrument that re-derives it every run, never by a figure in prose. So the + * central row below takes BOTH sides from the tree at run time — the keys read + * come from a cast-aware census over `ObjectCalendar.tsx` (objectui#6576's + * `schemaReads`, comment-masked so a retired spelling surviving in prose cannot + * move a verdict), and the keys declared come from the zod mirror's own + * `.shape`, never from parse acceptance, which under `.passthrough()` cannot + * tell "declared" from "admitted unexamined" (objectui#8466's instrument). + * + * The TS face is held to the same set by the standing `zod-mirror-parity` + * ratchet plus the compile-time pins in this file, so one row covers both + * published faces without this file re-implementing a second checker. + * + * ## 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 + * file as claiming more than it does. + */ + +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +import { ComponentPropsMap } from '@objectstack/spec/ui'; +import { ObjectCalendarSchema as ObjectCalendarMirror } from '@object-ui/types/zod'; +import type { ObjectCalendarSchema } from '@object-ui/types'; + +import { ObjectCalendar, type ObjectCalendarComponentProps } from '../ObjectCalendar'; + +// @ts-expect-error — plain-JS shared helper, intentionally untyped (`allowJs: false`) +import { maskComments } from '../../../../scripts/js-comment-mask.mjs'; + +/** Local annotation, since the import above is untyped — the call site stays checked. */ +const mask: (source: string) => string = maskComments; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = join(HERE, '..', '..', '..', '..'); +const CALENDAR_READER = 'packages/plugin-calendar/src/ObjectCalendar.tsx'; + +/** + * The ONE key this card deliberately does not rule on — objectui#8652's remit, + * maintainer-ruled B and blocked on objectstack#17987. Every entry must still + * be READ; see the header for why the ledger asserts that and nothing else. + */ +const LEDGERED_OTHER_CARD_READS = ['navigation'] as const; + +/** The two spellings this card retires. */ +const RETIRED_SPELLINGS = ['dateField', 'endField'] as const; + +/** Their canonical twins — the control that makes a zero above a reading. */ +const CANONICAL_TWINS = ['startDateField', 'endDateField'] as const; + +/** A key nothing reads and nothing declares: the both-ways control. */ +const CONTROL_KEY = 'zzqxNoSuchField'; + +/* ── Instruments ──────────────────────────────────────────────────────────── */ + +function readRepo(rel: string): string { + return readFileSync(join(REPO_ROOT, rel), 'utf8'); +} + +/** + * Every key read off the schema node, in EITHER form: the bare `schema.KEY` and + * the cast `(schema as any).KEY`. Both spell the same FACT — the renderer reads + * this key — and only one spells it through a cast that this card's cleanup + * deletes. Reading the fact rather than the spelling is what stops these + * verdicts reddening on that cleanup (objectui#8832). Borrowed verbatim from + * `types/src/__tests__/calendar-flat-color-allday-8466.test.ts`. + */ +const SCHEMA_READ = /(?:\bschema\b|\(\s*schema\s+as\s+[^)]*\))\s*\.\s*([A-Za-z_$][\w$]*)/g; + +function schemaReads(source: string): Set { + return new Set([...source.matchAll(SCHEMA_READ)].map((m) => m[1])); +} + +/** The renderer's reads, with prose masked so a retired spelling in a comment cannot vote. */ +function rendererReads(): Set { + return schemaReads(mask(readRepo(CALENDAR_READER))); +} + +/** Declared membership, off the mirror's OWN shape — never off parse acceptance. */ +function shapeKeys(schema: unknown): string[] { + return Object.keys((schema as { shape: Record }).shape); +} + +/** + * ⚠️ `ComponentPropsMap` entries are read through `Record` on + * purpose: `_def` is a zod INTERNAL for which the spec publishes no type, so a + * hand-written shape for it would be a local assertion about a third-party + * runtime that nothing re-checks. The same reading, and the same decision, as + * `calendar-flat-color-allday-8466.test.ts`. + */ +function specShapeKeys(type: string): string[] { + const entry = (ComponentPropsMap as unknown as Record)[type]; + const def = entry._def; + const shape = typeof def.shape === 'function' ? def.shape() : def.shape; + return Object.keys(shape); +} + +/* ── Compile-time pins (compiled by `tsc -p tsconfig.test.json`) ───────────── */ + +type Equal = + (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; +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. + */ +export type assertionSchemaPropIsThePublishedElementSchema = + Expect>; +/** The helper can FAIL — synthetic control. */ +export type assertionEqualCanFail = Expect, false>>; + +/* ── 1. The props type ────────────────────────────────────────────────────── */ + +describe('objectui#8651 — the props type is the published element schema', () => { + it('accepts the flat calendar vocabulary contextually typed, with NO cast', () => { + const schema: ObjectCalendarComponentProps['schema'] = { + type: 'object-calendar', + objectName: 'duly_task', + startDateField: 'kickoff', + endDateField: 'wrapup', + titleField: 'nickname', + colorField: 'status_colour', + allDayField: 'is_all_day', + defaultView: 'week', + }; + expect(schema.startDateField).toBe('kickoff'); + expect(schema.defaultView).toBe('week'); + }); + + it('accepts the nested `calendar` container the spec names, contextually typed', () => { + const schema: ObjectCalendarComponentProps['schema'] = { + type: 'object-calendar', + objectName: 'duly_task', + calendar: { startDateField: 'kickoff', endDateField: 'wrapup', titleField: 'nickname' }, + }; + expect(schema.calendar?.startDateField).toBe('kickoff'); + }); +}); + +/* ── 2. The population: every read is declared, or ledgered by name ────────── */ + +describe('objectui#8651 — every key read off the node is declared on that schema', () => { + it('no read is undeclared, with `navigation` ledgered by name', () => { + const reads = rendererReads(); + const declared = new Set(shapeKeys(ObjectCalendarMirror)); + const undeclared = [...reads] + .filter((key) => !declared.has(key)) + .filter((key) => !(LEDGERED_OTHER_CARD_READS as readonly string[]).includes(key)) + .sort(); + expect(undeclared, `undeclared reads on ObjectCalendarSchema: ${undeclared.join(', ')}`) + .toEqual([]); + }); + + it('the ledger is not stale: every carve-out is STILL read', () => { + const reads = rendererReads(); + for (const key of LEDGERED_OTHER_CARD_READS) { + expect([...reads], `${key} is ledgered but no longer read — the exception is a hole`) + .toContain(key); + } + }); + + it('⛔ and `navigation` stays UNDECLARED here — objectui#8652 owns it, not this card', () => { + expect(shapeKeys(ObjectCalendarMirror)).not.toContain('navigation'); + }); + + it('CONTROL: both halves of the row above can fail', () => { + // The census sees a read that is there… + expect([...rendererReads()]).toContain('objectName'); + // …and does not invent one that is not. + expect(rendererReads().has(CONTROL_KEY)).toBe(false); + // The declared set is a real set, not everything. + expect(shapeKeys(ObjectCalendarMirror)).toContain('startDateField'); + expect(shapeKeys(ObjectCalendarMirror)).not.toContain(CONTROL_KEY); + // And the census fires on both spellings, so the cast cleanup cannot mute it. + const probe = schemaReads('const a = schema.alphaKey; const b = (schema as any).betaKey;'); + expect([...probe].sort()).toEqual(['alphaKey', 'betaKey']); + }); +}); + +/* ── 3. `calendar`, the container the spec names ───────────────────────────── */ + +describe('objectui#8651 — `calendar` is declared, on the grounds the spec already set', () => { + it('the spec declares it at the flat position (control: a nonsense key on the same call)', () => { + expect(specShapeKeys('object-calendar')).toContain('calendar'); + const oc = (ComponentPropsMap as unknown as Record)['object-calendar']; + expect(oc.safeParse({ objectName: 'duly_task', calendar: { startDateField: 'kickoff' } }).success) + .toBe(true); + expect(oc.safeParse({ objectName: 'duly_task', [CONTROL_KEY]: 'x' }).success).toBe(false); + }); + + it('the mirror declares it — read off the shape, not off acceptance', () => { + expect(shapeKeys(ObjectCalendarMirror)).toContain('calendar'); + }); + + it('…and now VALUE-validates it, which is the whole of what declaring buys', () => { + const node = { type: 'object-calendar', objectName: 'duly_task' }; + expect(ObjectCalendarMirror.safeParse({ ...node, calendar: 42 }).success).toBe(false); + const ok = ObjectCalendarMirror.safeParse({ + ...node, + calendar: { startDateField: 'kickoff', titleField: 'nickname', allDayField: 'is_all_day' }, + }); + expect(ok.success, JSON.stringify(ok.error?.issues)).toBe(true); + }); + + it('the objectui#7927 ceiling is UNCHANGED: a misspelling is still admitted', () => { + const node = { type: 'object-calendar', objectName: 'duly_task' }; + expect(ObjectCalendarMirror.safeParse({ ...node, calender: { startDateField: 'k' } }).success) + .toBe(true); + }); +}); + +/* ── 4. The retired spellings ──────────────────────────────────────────────── */ + +describe('objectui#8651 — the `dateField` / `endField` alias rungs are retired', () => { + it('the renderer no longer reads either spelling', () => { + const reads = rendererReads(); + for (const key of RETIRED_SPELLINGS) { + expect([...reads], `${key} is still read off the node`).not.toContain(key); + } + }); + + it('CONTROL: the same census DOES see the canonical twins, so the zero is a reading', () => { + const reads = rendererReads(); + for (const key of CANONICAL_TWINS) expect([...reads]).toContain(key); + }); + + it('neither is declared on the mirror either', () => { + const declared = shapeKeys(ObjectCalendarMirror); + for (const key of RETIRED_SPELLINGS) expect(declared).not.toContain(key); + }); +}); + +/* ── 5. …and the retirement is observable on screen ───────────────────────── */ + +vi.mock('@object-ui/plugin-detail', async (importOriginal) => ({ + ...(await importOriginal()), + RecordDetailPanel: () => null, + deriveRecordPageHref: () => null, +})); + +const OBJECT_SCHEMA = { + name: 'duly_task', + nameField: 'subject', + fields: { + id: { name: 'id', type: 'text' }, + subject: { name: 'subject', type: 'text' }, + nickname: { name: 'nickname', type: 'text' }, + kickoff: { name: 'kickoff', type: 'datetime' }, + }, +}; + +const ROW = { + id: 'r1', + subject: 'Default display name', + nickname: 'Authored event title', + kickoff: '2026-03-01T09:00:00.000Z', +}; + +function makeDataSource() { + return { + find: vi.fn(async () => ({ data: [ROW], total: 1 })), + findOne: vi.fn(), + create: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + getObjectSchema: vi.fn(async () => OBJECT_SCHEMA), + } as any; +} + +const REFUSAL = /Calendar configuration required/i; + +describe('objectui#8651 — the retirement reaches the screen', () => { + it('a node configured ONLY through the retired spelling draws the refusal screen', async () => { + render( + , + ); + await waitFor(() => expect(screen.getByText(REFUSAL)).toBeTruthy()); + }); + + it('CONTROL: the canonical spelling on the same rows draws a calendar, not the refusal', async () => { + render( + , + ); + await waitFor(() => expect(screen.queryByText(REFUSAL)).toBeNull()); + }); +}); diff --git a/packages/types/src/__tests__/object-calendar-record-source-7313.test.ts b/packages/types/src/__tests__/object-calendar-record-source-7313.test.ts index 6843829bd8..3f918b5f06 100644 --- a/packages/types/src/__tests__/object-calendar-record-source-7313.test.ts +++ b/packages/types/src/__tests__/object-calendar-record-source-7313.test.ts @@ -49,6 +49,11 @@ import { dirname, join } from 'node:path'; // measure it rather than restate it. `@objectstack/spec` is a declared // dependency of this package; `ComponentPropsMap` is its published UI surface. import { ComponentPropsMap } from '@objectstack/spec/ui'; +// @ts-expect-error — plain-JS shared helper, intentionally untyped (`allowJs: false`) +import { maskComments } from '../../../../scripts/js-comment-mask.mjs'; + +/** Local annotation, since the import above is untyped — the call site stays checked. */ +const mask: (source: string) => string = maskComments; import { ObjectCalendarSchema, ObjectGanttSchema, safeValidateSchema } from '../zod/index.zod'; import { BaseSchema } from '../zod/base.zod'; @@ -377,20 +382,47 @@ describe('objectui#7313 — `data` and `staticData` are DECLARED, not passthroug describe('objectui#7313 — the declaration names a live read, in the declared order', () => { it('the renderer resolves its records through the shared ladder, on the ARRAY arm', () => { - const src = readFileSync(join(REPO_ROOT, RENDERER), 'utf8'); - // ⭐ objectui#8348 — the arm is part of the call now, and asserting it here - // is what keeps this row honest. The previous spelling looked for the bare - // `resolveRecordSourceConfig(schema)`, which this file's own renderer - // satisfies from a DOCBLOCK line that merely names the function — so it - // would have stayed green through a call site that had stopped existing. - expect(src, `${RENDERER} no longer calls the shared ladder with its declared arm`).toContain( - "resolveRecordSourceConfig(schema, 'array')", - ); + // ⭐ objectui#8348 — the arm is part of the call, and asserting it here is + // what keeps this row honest. The spelling before that card looked for the + // bare `resolveRecordSourceConfig(schema)`, which this renderer satisfies + // from a DOCBLOCK line that merely names the function — so it would have + // stayed green through a call site that had stopped existing. + // + // ⭐ objectui#8651 re-anchored it a second time, for the objectui#8832 + // reason: the literal `resolveRecordSourceConfig(schema, 'array')` pinned + // how the FIRST ARGUMENT is written, and that card had to change it — the + // ladder's parameter declares `data?: ViewData` while this block's + // published `data` row is the ARRAY arm, so the three members it documents + // itself as reading are now passed one by one. The call, the arm and the + // refusal of the other arm are the FACTS; the argument's shape is + // formatting. So the arms are read out of the call's own argument list, + // located by paren matching, with comments masked FIRST — which retires the + // docblock false green structurally rather than by wording. + const src = mask(readFileSync(join(REPO_ROOT, RENDERER), 'utf8')); + const at = src.indexOf('resolveRecordSourceConfig('); + expect(at, `${RENDERER} no longer calls the shared ladder at all`).toBeGreaterThan(-1); + let depth = 0; + let end = at + 'resolveRecordSourceConfig'.length; + for (; end < src.length; end += 1) { + if (src[end] === '(') depth += 1; + else if (src[end] === ')') { + depth -= 1; + if (depth === 0) break; + } + } + const call = src.slice(at, end + 1); // The arm is the one `ComponentPropsMap['object-calendar'].data` declares // (`z.array(z.unknown())`, "Pre-fetched records"), which is why it is // `'array'` here and `'view-data'` on `object-grid` / `object-map` / // `object-gantt`. - expect(src).not.toContain("resolveRecordSourceConfig(schema, 'view-data')"); + expect(call, `${RENDERER} no longer calls the shared ladder with its declared arm`) + .toContain("'array'"); + expect(call).not.toContain("'view-data'"); + // CONTROL: the slice really is the call and not the whole file — a paren + // match that ran away would swallow the other arm's name from elsewhere and + // make the refusal above unfailable. + expect(call.length).toBeLessThan(src.length); + expect(call.endsWith(')')).toBe(true); }); it('the ladder reads `data`, then `staticData`, then `objectName` — the order the refinement rests on', () => { diff --git a/packages/types/src/__tests__/zod-mirror-parity.test.ts b/packages/types/src/__tests__/zod-mirror-parity.test.ts index 9a316eef6a..dcf8b1961e 100644 --- a/packages/types/src/__tests__/zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts @@ -3181,6 +3181,13 @@ const SPEC_DERIVED_PAIRS: readonly string[] = [ // i18n label union, or the widget's comparison directive moves ONE side too. // Either way it is exactly what this list exists to make legible rather than // mysterious. + // objectui#8651: the `calendar` CONTAINER is the spec's own + // `SpecCalendarConfigSchema` — spec-derived, extended with objectui's single + // local knob (`allDayField`, the lane objectui#8466 took for the flat spelling + // of the same vocabulary) and kept `.passthrough()`. So a spec bump that moves + // the four-key calendar config vocabulary moves ONE side of this pair, which + // is exactly what this list exists to make legible rather than mysterious. + 'objectql.zod.ts#ObjectCalendarSchema', 'objectql.zod.ts#ObjectChartSchema', 'objectql.zod.ts#ObjectGallerySchema', 'objectql.zod.ts#ObjectGanttSchema', diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index cc68e92401..5feaafbbdb 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -27,7 +27,7 @@ import type { DrillDownConfig } from './data-display.js'; import type { BulkActionOperation } from '@objectstack/spec/ui'; import type { FormField } from './form.js'; // ListView type is now derived from the zod schema (issue #2231) — see ListViewSchema below. -import type { ListViewInferred } from './zod/objectql.zod.js'; +import type { ListViewInferred, ObjectCalendarBlockConfig } from './zod/objectql.zod.js'; // ============================================================================ // Spec-Canonical Types — imported from @objectstack/spec/ui @@ -2843,6 +2843,35 @@ export interface ObjectCalendarSchema extends BaseSchema { data?: SpecObjectCalendarProps['data']; /** Inline records, wrapped into a `{ provider: 'value' }` config by `getDataConfig`. */ staticData?: any[]; + /** + * The configuration container, and the FIRST thing this element's renderer + * reads: `plugin-calendar/src/ObjectCalendar.tsx`'s `getCalendarConfig` + * returns this block whole when it is present, and only falls through to the + * flat members below when it is not. + * + * `@objectstack/spec` declares it — + * `ComponentPropsMap['object-calendar'].calendar` — and this package's + * registration `inputs` publishes it, so authors are offered the key. Both + * published faces of THIS package stayed silent about it until objectui#8651, + * which is the objectui#6914 class: the value rode {@link BaseSchema}'s + * `[key: string]: any` here and `.passthrough()` on the mirror, admitted and + * never examined. `calendar: 42` type-checked, parsed green, and drew an + * empty calendar. + * + * DERIVED from the mirror rather than re-spelled, so the two faces cannot + * fork — the same construction {@link ListViewSchema} uses through + * `ListViewInferred`. What the mirror declares is the five members + * `ObjectCalendar`'s events pass destructures out of the resolved config: the + * spec's four plus objectui's own `allDayField`, on the lane objectui#8466 + * took for the flat spelling of the same vocabulary. + * + * ⛔ `defaultView` is deliberately NOT a member of this container even though + * a list VIEW's calendar block carries one: this renderer seeds its view state + * from {@link ObjectCalendarSchema.defaultView}, the FLAT member below, and + * never looks inside here. The container stays `.passthrough()`, so a block + * carrying it still parses — it is simply not advertised. + */ + calendar?: ObjectCalendarBlockConfig; /** Field for event start */ startDateField?: string; /** Field for event end */ diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index 428c92716f..39d4287d1a 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -671,6 +671,52 @@ const CalendarConfig = stripImportedDefaults(SpecCalendarConfigSchema).partial() defaultView: z.enum(['month', 'week', 'day']).optional().describe("Initial calendar view mode — 'month' | 'week' | 'day' ('agenda' was retired: objectui#5784)"), }).passthrough(); +/** + * The `object-calendar` ELEMENT's configuration container — a DIFFERENT contract + * from {@link CalendarConfig} above, which is a list VIEW's calendar block, and + * the reason the two are not one const (objectui#8651). + * + * Both derive from the same spec object. They differ on exactly one member, and + * the difference is a read site, not a preference: + * + * - a VIEW's block carries `defaultView`, and `ListView` LIFTS it onto the + * node it builds (`plugin-list/src/ListView.tsx`, the `calendar` branch), + * so the key is honoured from there. + * - this ELEMENT's block does not, because `ObjectCalendar` seeds its view + * state from the FLAT `defaultView` member of this schema and never looks + * inside the container. Declaring it here would advertise a write this + * renderer drops — `plugin-calendar/src/index.tsx` names that as this + * gate's own failure mode one layer in. + * + * The members declared are exactly the five `ObjectCalendar.tsx`'s events pass + * destructures out of the resolved config: the spec's four, plus objectui's own + * `allDayField`. That key has no spec counterpart — `CalendarConfigSchema` is a + * `strictObject` of four keys and refuses it BY NAME — and it is the same + * objectui-local lane objectui#8466 took for the FLAT spelling of this + * vocabulary, on this same interface, for the same renderer. + * + * ⛔ `.passthrough()` is kept, so this declaration REFUSES NOTHING that parses + * today: a `calendar` block carrying `defaultView`, or any other unexamined + * key, still parses exactly as it did through `BaseSchema`'s own + * `.passthrough()`. What it adds is VALUE validation on the five named members + * — `calendar: 42` and `calendar: { startDateField: 42 }` are refused where + * both were admitted unexamined before. + */ +const ObjectCalendarBlockConfigSchema = stripImportedDefaults(SpecCalendarConfigSchema).partial().extend({ + // objectui-local, no spec counterpart — see objectui#8466 for the measurement + // and the lane. The renderer honours it in BOTH positions: this container and + // the flat member of the node. + allDayField: z.string().optional().describe("Field carrying the all-day flag — objectui-local (the spec's CalendarConfigSchema refuses it by name); LOAD-BEARING since objectui#8026"), +}).passthrough(); + +/** + * The inferred twin of {@link ObjectCalendarBlockConfigSchema}, exported so the + * TS face of `ObjectCalendarSchema.calendar` can DERIVE from this mirror rather + * than re-spell it. Two faces, one declaration — the same construction + * `ListViewSchema` already uses through `ListViewInferred`. + */ +export type ObjectCalendarBlockConfig = z.infer; + const GalleryConfig = stripImportedDefaults(SpecGalleryConfigSchema).partial().extend({ /** @deprecated legacy alias for the spec's `coverField` */ imageField: z.string().optional().describe('Deprecated alias for coverField'), @@ -1232,6 +1278,23 @@ export const ObjectCalendarSchema = BaseSchema.extend({ // `unknown[]`, which is exactly what `z.array(z.unknown())` infers here. data: z.array(z.unknown()).optional().describe('Pre-fetched records — an ARRAY, drawn in place of the calendar\'s own query; read FIRST by the record-source ladder. Mirrors ComponentPropsMap[\'object-calendar\'].data — the { provider, items } config object is refused by kind on this block (objectui#9239, ruling objectui#8348)'), staticData: z.array(z.any()).optional().describe('Inline records, wrapped into a { provider: value } data config — read SECOND by getDataConfig'), + // objectui#8651 — the configuration container the SPEC declares for this + // element (`ComponentPropsMap['object-calendar'].calendar`), which this + // package's registration `inputs` already publishes and which + // `ObjectCalendar.tsx`'s `getCalendarConfig` reads FIRST, ahead of the flat + // members below. Neither published face of this package named it: it rode + // `BaseSchema`'s `.passthrough()` here and its `[key: string]: any` on the TS + // side — admitted, never examined — so `calendar: 42` and + // `calendar: { startDateField: 42 }` both parsed green and then produced a + // calendar that drew nothing. + // + // The exit is the mechanical one triage ruled for this family (objectui#8327, + // comment 5619610246): the key IS declared by `@objectstack/spec`, so this + // mirror aligns to it rather than forking the contract. Mirrored at the SAME + // requiredness as `../objectql.ts` (both optional) so the zod-mirror-parity + // ratchet stays at zero drift for this pair, exactly as the `filter`/`sort` + // and `colorField`/`allDayField` pairs below. + calendar: ObjectCalendarBlockConfigSchema.optional().describe('Calendar configuration container — startDateField, endDateField, titleField, colorField (plus objectui\'s allDayField); read FIRST by getCalendarConfig, ahead of the flat spelling'), startDateField: z.string().optional().describe('Start date field'), endDateField: z.string().optional().describe('End date field'), titleField: z.string().optional().describe('Title field'), From 40c825c7ff4ee5d95a5e7860419d56d6d0bd739b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 07:34:38 +0000 Subject: [PATCH 2/7] fix(scripts): drop the resolved CalendarSchema entry from the collision baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Test (shard 4/4)` reddened on the objectui#6273 ratchet: the baseline listed a `CalendarSchema` collision between `packages/plugin-calendar/src/ObjectCalendar.tsx` and `packages/types/src/form.ts`, and the previous commit resolved it by deleting the plugin-local declaration. The ratchet refuses to let a resolved entry sit in the baseline, so the entry comes down with the fix. This NARROWS the exemption set; nothing is added, loosened, skipped or disabled. Verified it went stale because of the fix and not because the detector lost sight of either file: with the collision put back on disk under a trap — proved to have reached disk by hash, restored by hash equality against the HEAD blob — the same gate goes green at 11/11, so it still walks both files and still pairs them. Its own `scans a population that has not collapsed` row passes in both readings. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt --- .../one-authority-per-exported-name-6273.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/__tests__/one-authority-per-exported-name-6273.test.ts b/scripts/__tests__/one-authority-per-exported-name-6273.test.ts index a8ff6027ff..ef937e811a 100644 --- a/scripts/__tests__/one-authority-per-exported-name-6273.test.ts +++ b/scripts/__tests__/one-authority-per-exported-name-6273.test.ts @@ -364,7 +364,15 @@ const KNOWN_COLLISIONS: ReadonlyMap = new Map([ // `packages/types/src/navigation.ts`; data-display's were a strict SUBSET copy, so // that file re-points at navigation's one authority. ['CalendarEvent', ['packages/plugin-calendar/src/index.tsx', 'packages/types/src/complex.ts']], // the ruled-on objectui#5044 alias — see the header - ['CalendarSchema', ['packages/plugin-calendar/src/ObjectCalendar.tsx', 'packages/types/src/form.ts']], + // `CalendarSchema` sat here, colliding between + // `packages/plugin-calendar/src/ObjectCalendar.tsx` and + // `packages/types/src/form.ts`. Two unrelated meanings behind one word: the + // plugin's was the calendar VIEW's props schema, `@object-ui/types`' is the + // date-picker primitive reachable at `ui:calendar` only (objectui#8499). The + // remedy was the DELETE branch — the plugin-local one was absent from that + // package's barrel, so no importer could name it, and objectui#8651 measured + // that `ObjectCalendar`'s props belong at the published `ObjectCalendarSchema` + // instead. One authority remains, in `@object-ui/types`. ['ChatMessage', ['packages/plugin-chatbot/src/ChatbotEnhanced.tsx', 'packages/types/src/complex.ts']], ['ChatToolInvocation', ['packages/plugin-chatbot/src/ChatbotEnhanced.tsx', 'packages/types/src/complex.ts']], // `ComboboxOption` sat here, colliding between From 47649c858498eb50ee677a87dfc3b7034fc37d48 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 09:10:33 +0000 Subject: [PATCH 3/7] =?UTF-8?q?fix(plugin-calendar):=20keep=20the=20dateFi?= =?UTF-8?q?eld/endField=20rungs=20=E2=80=94=20the=20retirement=20census=20?= =?UTF-8?q?was=20false?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contract review FAIL, four items plus one. The load-bearing one is a live behaviour regression, and it means the chosen exit was wrong rather than the wording. 1. The zero-producer census behind the retirement was FALSE. The producer does not write the key literally, it SPREADS it: `ListView`'s `case 'calendar':` flattens the authored block onto the node it emits, objectui's own published `ListViewSchema` accepts `calendar.dateField`, and `resolveTimelineDateBinding` documents it as the pre-#2231 alias for `startDateField`. A word-boundary text census is structurally blind to a key arriving through a spread. Reproduced by mounting the producer, capturing the emitted node (`titleField` + flat `dateField`, no `startDateField`) and rendering that exact node on both trees: merge-base draws, the retiring tree refused. ⇒ the exit is ROUTE TO THE PRODUCER, not retire. The rungs stay, both keys are ledgered by name, and the ledger asserts the producer still flattens so it reddens when `ListView` is fixed instead of rotting. 2. The cast count was contradicted by the checker. On the UNION seven of the fifteen reads were undeclared, not twelve: `ObjectGridSchema`'s index signature supplied the five `CalendarSchema`-only keys as `any`. Corrected on both the changeset and the source docblock, with the per-arm and per-union readings kept apart. 3. `ObjectCalendarBlockConfig` was unreachable — the same "measurably unreachable" property this card removed from the plugin-local `CalendarSchema`, one layer over. Now exported from `@object-ui/types`; verified by compiling a consumer import with a `@ts-expect-error` negative control, and by the same probe failing TS2305 at the merge-base. 4. "No conforming author could write them" was false nested: the element's `calendar` slot is `z.unknown().optional()`, not `CalendarConfigSchema`, so it refuses nothing. The grounds on both faces now say the spec settles the KEY and objectui settles the SHAPE. 6. The re-anchored control in the objectui#7313 pin could only come back green: a runaway paren match is shorter than the file and ends in a paren. Neither could the obvious repairs — a runaway closes on `useMemo`'s paren, stays balanced, and swallows only a dependency array, which is a legal argument. Re-anchored on the call's ARGUMENT LIST instead, so the arity assertion fires; proven by mutation. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt --- .changeset/olive-buckets-scream.md | 9 +- .../plugin-calendar/src/ObjectCalendar.tsx | 114 ++++++++---- .../calendarUnionReads-8651.test.tsx | 162 +++++++++++++++--- ...object-calendar-record-source-7313.test.ts | 55 +++++- packages/types/src/index.ts | 1 + packages/types/src/objectql.ts | 25 ++- packages/types/src/zod/objectql.zod.ts | 28 ++- 7 files changed, 310 insertions(+), 84 deletions(-) diff --git a/.changeset/olive-buckets-scream.md b/.changeset/olive-buckets-scream.md index fe56a529ad..a69a49e68c 100644 --- a/.changeset/olive-buckets-scream.md +++ b/.changeset/olive-buckets-scream.md @@ -5,8 +5,9 @@ fix(plugin-calendar): type `ObjectCalendar` at the published `object-calendar` schema, and declare the `calendar` container -`ObjectCalendarComponentProps.schema` was the union `ObjectGridSchema | CalendarSchema` — a grid's schema, plus a plugin-local interface absent from this package's barrel. Neither arm is the schema of the element this renderer is registered as, so twelve of the fifteen keys it reads off the node were undeclared on the union and had to be read through a cast. `ObjectCalendarSchema` already declared eleven of them. +`ObjectCalendarComponentProps.schema` was the union `ObjectGridSchema | CalendarSchema` — a grid's schema, plus a plugin-local interface absent from this package's barrel. Neither arm is the schema of the element this renderer is registered as. Measured with the TypeScript checker: of the fifteen keys the renderer reads off the node, seven were undeclared on that union and had to be read through a cast, while five more resolved only through `ObjectGridSchema`'s index signature and were therefore typed `any` — admitted, never examined. `ObjectCalendarSchema` already declared eleven of the fifteen. -- `ObjectCalendarComponentProps.schema` is now `ObjectCalendarSchema`. **Breaking for a React host that passed an `object-grid` node or a `type: 'calendar'` literal to ``** — neither was ever a node this renderer is registered for. `ObjectCalendarProps`, the deprecated alias, follows it. -- `ObjectCalendarSchema.calendar` is declared on both published faces, bound to `@objectstack/spec`'s `CalendarConfigSchema` plus objectui's own `allDayField`. The container is kept `.passthrough()`, so nothing that parsed before is refused now; what is new is value validation — `calendar: 42` and `calendar: { startDateField: 42 }` are refused where both were admitted unexamined. -- The `dateField` / `endField` alias rungs inside `getCalendarConfig` are retired. Nothing wrote them: no producer, fixture, test or doc in this repo, and `@objectstack/spec` refuses both by name at the flat position and inside `CalendarConfigSchema`. A node configured only through a retired spelling now draws the existing "Calendar configuration required" refusal instead of resolving. +- `ObjectCalendarComponentProps.schema` is now `ObjectCalendarSchema`. **Breaking for a React host that passed an `object-grid` node or a `type: 'calendar'` literal to `ObjectCalendar`** — neither was ever a node this renderer is registered for. `ObjectCalendarProps`, the deprecated alias, follows it. +- `ObjectCalendarSchema.calendar` is declared on both published faces. The spec declares the KEY; it does **not** declare its shape — `ComponentPropsMap['object-calendar'].calendar` is `z.unknown().optional()` and accepts anything at that position — so the member list is objectui's own: the four `CalendarConfigSchema` names plus objectui's `allDayField`, which is what the renderer reads out of the block. The container keeps `.passthrough()`, so nothing that parsed before is refused; what is new is value validation, and `calendar: 42` and `calendar: { startDateField: 42 }` are refused where both were admitted unexamined. +- `@object-ui/types` now exports the type `ObjectCalendarBlockConfig`, so that published member has a name an importer can write. +- ⚠️ The `dateField` / `endField` alias rungs in `getCalendarConfig` are **kept**, and routed to the producer. An earlier revision of this change retired them on a census that was false: `ListView` flattens an authored `calendar` block onto the node it emits and objectui's own `ListViewSchema` accepts `calendar.dateField`, so a view authored that way renders today and would have drawn "Calendar configuration required". No behaviour changes for either spelling; the remedy belongs in `ListView`'s calendar branch and is filed separately. diff --git a/packages/plugin-calendar/src/ObjectCalendar.tsx b/packages/plugin-calendar/src/ObjectCalendar.tsx index a5abb3d8c4..0a4c7cf9db 100644 --- a/packages/plugin-calendar/src/ObjectCalendar.tsx +++ b/packages/plugin-calendar/src/ObjectCalendar.tsx @@ -80,11 +80,26 @@ import { * `ObjectGridSchema`. Both arms are gone; the prop is now the published schema * of the element this file is registered as (`index.tsx`, both tags). * - * What the union cost, measured with the TypeScript checker on the current tree - * (`getPropertyOfType`, never a grep — objectui#8410): of the fifteen keys this + * What the union cost, measured with the TypeScript checker on the merge-base + * (`getPropertyOfType`, never a grep — objectui#8410). Of the fifteen keys this * renderer reads off the node, FOUR were declared on neither arm and EIGHT on - * exactly one, so every one of them needed a cast to read — while - * `ObjectCalendarSchema` already declared ELEVEN of the fifteen. + * exactly one, while `ObjectCalendarSchema` already declared ELEVEN of the + * 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 + * 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` + * `data` `endDateField` `navigation` `startDateField` `staticData`), nonsense + * control `false`. ⇒ SEVEN reads needed a cast, not twelve. + * + * ⭐ That reading is worse for the old shape, not better: those five compiled + * silently as `any` — admitted through an index signature, never examined — + * which is the objectui#6914 defect itself rather than an absence of one. A + * cast at least announces the gap. The union's own index signature is `false`, + * which is why the other seven had to be cast. * * The two arms failed differently and neither was salvageable: * @@ -224,33 +239,50 @@ type ObjectCalendarConfig = CalendarConfig & { }; /** - * ⛔ TWO ALIAS RUNGS RETIRED HERE (objectui#8651), and they were the only - * members the retired plugin-local interface had that this schema does not. + * The two PRE-#2231 alias spellings `getCalendarConfig` still honours, declared + * ONLY as a cast target and deliberately NOT as schema members — see the + * routing note on `getCalendarConfig`. `@objectstack/spec` refuses both by name + * at the flat position, so a declaration here would make this repo accept what + * the platform refuses. + */ +type CalendarAliasRungs = { dateField?: string; endField?: string }; + +/** + * ⚠️ TWO ALIAS RUNGS SURVIVE HERE, and objectui#8651 ROUTES THEM TO THE + * PRODUCER rather than retiring them. An earlier cut of that card DID retire + * them, on a census that was FALSE; the census was the defect, so the reasoning + * is recorded here rather than the conclusion it produced. + * + * What the false census said: zero producers anywhere in this repo write either + * spelling onto a calendar node. What it could not see: the producer does not + * write the key LITERALLY, it SPREADS it. `plugin-list/src/ListView.tsx`'s + * `case 'calendar':` ends by flattening the authored block onto the node it + * emits — `...(schema.options?.calendar || {})` then `...(schema.calendar || {})` + * — objectui's own published `ListViewSchema` accepts `calendar.dateField`, and + * `resolveTimelineDateBinding` in that same file documents it in terms: + * *"`dateField` is the pre-#2231 alias for `startDateField`"*, and honours it. + * A word-boundary text census is structurally blind to a key that arrives + * through a spread. * - * The flat branch below used to read a second spelling under each of the two - * date bindings — one for the start field, one for the end — falling back to it - * when the canonical spelling was absent. Censused on the current tree, each - * with a control that hits: ZERO producers anywhere in this repo write either - * onto a calendar node (every hit belongs to the sibling `timeline` widget), - * ZERO fixtures, ZERO tests and ZERO docs teach them, and `@objectstack/spec` - * refuses both BY NAME — with an `unrecognized_keys` diagnostic at the flat - * position AND inside `CalendarConfigSchema`, which is a `strictObject` of four - * keys. So no conforming author could write them and no write door could store - * them. + * MEASURED by mounting the producer on `calendar: { dateField, titleField }`, + * capturing the `object-calendar` node it really emits — a `titleField` and a + * flat `dateField`, NO `startDateField` — and rendering that exact node on both + * trees: the merge-base draws a calendar, the retiring tree drew "Calendar + * configuration required". A live, published authoring path stopped rendering + * with every gate green. `endField` degraded more quietly: the node still + * resolved through `startDateField` and the end binding was silently dropped. * - * ⛔ Per AGENTS.md #0.1 the remedy for a second spelling belongs at the - * producer, and there is no producer: a tolerant rung kept here would be a - * second de-facto contract with nothing on the other end of it. The node that - * carried ONLY a retired spelling now falls through to `null`, which the - * "Calendar configuration required" refusal screen answers by name — the same - * degradation objectui#7711 chose for the same reason, one key over. Pinned, - * with its positive control, in `__tests__/calendarUnionReads-8651.test.tsx`. + * ⇒ AGENTS.md #0.1 points these at the PRODUCER — the one translation point in + * `ListView.tsx`'s calendar branch, which already lifts `startDateField`, + * `endDateField`, `titleField` and `defaultView` and should normalise these two + * the same way. That file is outside objectui#8651's declared file surface, so + * the card ROUTES rather than retires: the rungs stay, and both keys are + * LEDGERED BY NAME in `__tests__/calendarUnionReads-8651.test.tsx`, which + * asserts they are still read AND that the producer still flattens — so when + * the producer is fixed the ledger reddens and the rungs can go. * - * ⛔ No casts remain in this function. Every key it reads is now a declared - * member of the parameter type, which is the whole point of objectui#8651 — and - * `packages/types/src/__tests__/calendar-flat-color-allday-8466.test.ts` reads - * this function's reads as a FACT rather than as a spelling (objectui#8832), so - * dropping them moves no verdict there. + * ⛔ Do not re-retire these on a text census. The only census that can answer + * this question runs the producer. */ function getCalendarConfig(schema: ObjectCalendarSchema): ObjectCalendarConfig | null { // The declared configuration container — read FIRST, as the spec declares it. @@ -259,10 +291,18 @@ function getCalendarConfig(schema: ObjectCalendarSchema): ObjectCalendarConfig | } // The flat spelling, which `ObjectView` and `ListView` emit. - if (schema.startDateField) { + // + // ⛔ The two alias rungs are spelled as a cast ON `schema` at each read site, + // never through a renamed local. The repo's read census (objectui#6576's + // `schemaReads`, used by + // `types/src/__tests__/calendar-flat-color-allday-8466.test.ts` and by this + // card's own pin) matches `schema.KEY` and `(schema as T).KEY` — a local alias + // would hide these two reads from the very instrument meant to enumerate them, + // which is the same class of blindness that produced the false census above. + if (schema.startDateField || (schema as CalendarAliasRungs).dateField) { return { - startDateField: schema.startDateField, - endDateField: schema.endDateField, + startDateField: schema.startDateField || (schema as CalendarAliasRungs).dateField, + endDateField: schema.endDateField || (schema as CalendarAliasRungs).endField, titleField: schema.titleField, colorField: schema.colorField, allDayField: schema.allDayField @@ -427,12 +467,10 @@ export const ObjectCalendar: React.FC = ({ // luck; that luck is now gone, so the list has to be honest. Pinned in // `__tests__/ObjectCalendar.filterIsNotAConfigSlot-7711.test.tsx`. // - // ⛔ Two more entries left this list in objectui#8651, and they are NOT a - // third accidental omission: the two retired alias rungs noted on - // `getCalendarConfig` above. The function no longer reads either spelling, so - // depending on them would be the mirror-image dishonesty — a dependency on a - // key that can no longer change the result. This list is exactly what the - // function reads, in both directions. + // ⚠️ The two alias rungs are listed too, and they have to be: this list is + // exactly what `getCalendarConfig` reads, in BOTH directions. A cut of + // objectui#8651 dropped them alongside a retirement that broke a live + // authoring path; the rungs came back, so these came back with them. // // ⭐ objectui#8026 — `allDayField` is now LOAD-BEARING here, not merely // honest. When #7711 named it, nothing read the key, so the dependency could @@ -447,7 +485,9 @@ export const ObjectCalendar: React.FC = ({ const calendarConfig = useMemo(() => getCalendarConfig(schema), [ schema.calendar, schema.startDateField, + (schema as CalendarAliasRungs).dateField, schema.endDateField, + (schema as CalendarAliasRungs).endField, schema.titleField, schema.colorField, schema.allDayField diff --git a/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx b/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx index 5cd58a7e5f..d647a9342d 100644 --- a/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx +++ b/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx @@ -16,9 +16,17 @@ * The card filed twelve keys in two classes — four undeclared on both arms of * `ObjectGridSchema | CalendarSchema`, eight declared on exactly ONE arm — and * asked, as its FIRST question, whether that union is right at all. Measured - * with the TypeScript checker on the current tree (`getPropertyOfType`, never a + * with the TypeScript checker on the merge-base (`getPropertyOfType`, never a * grep — objectui#8410), the answer decides eleven of the twelve at once: * + * ⚠️ 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 + * 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. + * * `ObjectCalendarSchema` — this repo's published interface for the * `object-calendar` element, and its zod mirror — ALREADY declares eleven of * the fifteen keys this renderer reads, `allDayField`, `startDateField`, @@ -54,13 +62,22 @@ * publishes it AND `getCalendarConfig` reads it FIRST. Triage's ruling for * the family (objectui#8327, comment `5619610246`) makes that exit * mechanical: declared in spec ⇒ align the mirror. Declared here. - * - `dateField` / `endField` were declared ONLY on the retired local - * interface and read only as fallback rungs beneath the canonical - * spellings. Nothing writes them: zero producers, zero fixtures, zero - * tests, zero docs — each census run below with a control that hits — and - * the spec refuses both BY NAME at the flat position and inside - * `CalendarConfigSchema`. ⇒ retired, per AGENTS.md #0.1: the remedy for a - * second spelling belongs at the producer, and there is no producer. + * - `dateField` / `endField` are ROUTED TO THE PRODUCER, the third exit. + * ⛔ An earlier cut of this card RETIRED them on a census that was FALSE, + * and the false census is recorded here because it is the transferable + * part. It said: zero producers write either spelling onto a calendar node. + * It could not see the producer because the producer does not write the key + * LITERALLY — `plugin-list/src/ListView.tsx`'s `case 'calendar':` SPREADS + * the authored block flat onto the node it emits, objectui's own published + * `ListViewSchema` accepts `calendar.dateField`, and + * `resolveTimelineDateBinding` in that file documents it as *"the pre-#2231 + * alias for `startDateField`"* and honours it. A word-boundary text census + * is structurally blind to a key arriving through a spread. Measured by + * mounting the producer: `calendar: { dateField, titleField }` emits a node + * with a flat `dateField` and NO `startDateField`, which the merge-base + * draws and the retiring tree refused. ⇒ the remedy is at the producer, + * which is outside this card's file surface, so the rungs STAY and both + * keys are ledgered below with the producer named. * * ## ⛔ `navigation` is NOT ruled here, and its verdict is INVARIANT * @@ -130,12 +147,23 @@ const CALENDAR_READER = 'packages/plugin-calendar/src/ObjectCalendar.tsx'; */ const LEDGERED_OTHER_CARD_READS = ['navigation'] as const; -/** The two spellings this card retires. */ -const RETIRED_SPELLINGS = ['dateField', 'endField'] as const; +/** + * The two PRE-#2231 alias spellings this card ROUTES TO THE PRODUCER rather + * than declaring or retiring. Both are genuinely undeclared on + * `ObjectCalendarSchema` and must stay so — `@objectstack/spec` refuses both by + * name at the flat position, so declaring them would make this repo accept what + * the platform refuses. Each entry is asserted to be STILL READ, and the + * PRODUCER is asserted to still flatten its block, so when `ListView` is fixed + * this ledger reddens rather than rotting into a permanent exemption. + */ +const ROUTED_TO_PRODUCER = ['dateField', 'endField'] as const; -/** Their canonical twins — the control that makes a zero above a reading. */ +/** Their canonical twins — the control that makes any zero above a reading. */ const CANONICAL_TWINS = ['startDateField', 'endDateField'] as const; +/** The producer that makes the two aliases reachable, and the branch that does it. */ +const PRODUCER = 'packages/plugin-list/src/ListView.tsx'; + /** A key nothing reads and nothing declares: the both-ways control. */ const CONTROL_KEY = 'zzqxNoSuchField'; @@ -233,9 +261,10 @@ describe('objectui#8651 — every key read off the node is declared on that sche it('no read is undeclared, with `navigation` ledgered by name', () => { const reads = rendererReads(); const declared = new Set(shapeKeys(ObjectCalendarMirror)); + const exempt = new Set([...LEDGERED_OTHER_CARD_READS, ...ROUTED_TO_PRODUCER]); const undeclared = [...reads] .filter((key) => !declared.has(key)) - .filter((key) => !(LEDGERED_OTHER_CARD_READS as readonly string[]).includes(key)) + .filter((key) => !exempt.has(key)) .sort(); expect(undeclared, `undeclared reads on ObjectCalendarSchema: ${undeclared.join(', ')}`) .toEqual([]); @@ -269,7 +298,23 @@ describe('objectui#8651 — every key read off the node is declared on that sche /* ── 3. `calendar`, the container the spec names ───────────────────────────── */ -describe('objectui#8651 — `calendar` is declared, on the grounds the spec already set', () => { +describe('objectui#8651 — `calendar` is declared; the spec sets the KEY, objectui sets the SHAPE', () => { + it('⚠️ the spec declares the KEY but NOT its shape — the slot refuses nothing', () => { + // The grounds, stated the way the instrument returns them. An earlier cut + // of this card said the spec declares the container and that "no conforming + // author could write" the alias spellings inside it. The first half is + // true; the second is false, and this row is why. + const oc = (ComponentPropsMap as unknown as Record)['object-calendar']; + const inside = (value: unknown) => oc.safeParse({ objectName: 'duly_task', calendar: value }).success; + expect(inside({ startDateField: 'kickoff' })).toBe(true); // known-accepted control + expect(inside({ [CONTROL_KEY]: 'x' })).toBe(true); // ⇒ the slot is not strict + expect(inside({ dateField: 'kickoff' })).toBe(true); // so the alias IS writable + expect(inside(42)).toBe(true); // it is `z.unknown()` + // CONTROL, same instrument, one level out: the element's props schema IS + // strict, so the reading above is about this SLOT and not a dead parser. + expect(oc.safeParse({ objectName: 'duly_task', [CONTROL_KEY]: 'x' }).success).toBe(false); + }); + it('the spec declares it at the flat position (control: a nonsense key on the same call)', () => { expect(specShapeKeys('object-calendar')).toContain('calendar'); const oc = (ComponentPropsMap as unknown as Record)['object-calendar']; @@ -301,22 +346,58 @@ describe('objectui#8651 — `calendar` is declared, on the grounds the spec alre /* ── 4. The retired spellings ──────────────────────────────────────────────── */ -describe('objectui#8651 — the `dateField` / `endField` alias rungs are retired', () => { - it('the renderer no longer reads either spelling', () => { +describe('objectui#8651 — the `dateField` / `endField` rungs are ROUTED, not retired', () => { + it('both are STILL READ — the rungs stay until the producer is fixed', () => { const reads = rendererReads(); - for (const key of RETIRED_SPELLINGS) { - expect([...reads], `${key} is still read off the node`).not.toContain(key); + for (const key of ROUTED_TO_PRODUCER) { + expect([...reads], `${key} is ledgered as routed but is no longer read`).toContain(key); } }); - it('CONTROL: the same census DOES see the canonical twins, so the zero is a reading', () => { + it('CONTROL: the same census sees the canonical twins, and invents nothing', () => { const reads = rendererReads(); for (const key of CANONICAL_TWINS) expect([...reads]).toContain(key); + expect(reads.has(CONTROL_KEY)).toBe(false); }); - it('neither is declared on the mirror either', () => { + it('⛔ neither is DECLARED — declaring would accept what the platform refuses', () => { const declared = shapeKeys(ObjectCalendarMirror); - for (const key of RETIRED_SPELLINGS) expect(declared).not.toContain(key); + for (const key of ROUTED_TO_PRODUCER) expect(declared).not.toContain(key); + // …and that is the spec's verdict, not an assumption. Same call, with a + // known-accepted key and a nonsense key as the two controls. + const oc = (ComponentPropsMap as unknown as Record)['object-calendar']; + expect(oc.safeParse({ objectName: 'duly_task' }).success).toBe(true); + expect(oc.safeParse({ objectName: 'duly_task', [CONTROL_KEY]: 'x' }).success).toBe(false); + for (const key of ROUTED_TO_PRODUCER) { + const r = oc.safeParse({ objectName: 'duly_task', [key]: 'x' }); + expect(r.success, `the spec now accepts a flat ${key}; revisit this routing`).toBe(false); + } + }); + + it('the LEDGER IS NOT STALE: the producer still flattens its block onto the node', () => { + // The carrier assertion. `ListView`'s calendar branch ends by spreading the + // authored `calendar` block FLAT onto the `object-calendar` node it emits, + // which is the whole reason an authored `calendar.dateField` reaches this + // renderer as a flat key. When that branch normalises the aliases instead, + // this row reddens and the two rungs above can finally go. + const producer = mask(readRepo(PRODUCER)); + const at = producer.indexOf("case 'calendar':"); + expect(at, `${PRODUCER}: the calendar branch is gone; re-derive this ledger`).toBeGreaterThan(-1); + // Bound the slice STRUCTURALLY, at the next `case` label, rather than by a + // character count — a magic window either overshoots into the sibling + // branch (measured: 2000 chars reaches `case 'gallery':`) or silently + // undershoots past the spread this row is about. + const nextCase = producer.indexOf("case '", at + 1); + expect(nextCase, `${PRODUCER}: no branch follows the calendar one; the bound is unsafe`) + .toBeGreaterThan(at); + const branch = producer.slice(at, nextCase); + expect(branch, `${PRODUCER} no longer flattens the authored calendar block`) + .toContain('...(schema.calendar || {})'); + // CONTROLS: the slice really is just this branch — it carries this branch's + // own content and none of the next one's. + expect(branch).toContain('startDateField'); + expect(branch).not.toContain("case 'gallery':"); + expect(branch.length).toBeLessThan(producer.length); }); }); @@ -359,24 +440,55 @@ function makeDataSource() { const REFUSAL = /Calendar configuration required/i; -describe('objectui#8651 — the retirement reaches the screen', () => { - it('a node configured ONLY through the retired spelling draws the refusal screen', async () => { +/** + * ⭐ THE REGRESSION ROW. An earlier cut of this card retired the two alias rungs + * and this suite was GREEN, the whole farm was GREEN, and a live authoring path + * had stopped rendering. What was missing was not a stricter assertion anywhere + * in it — it was THIS node. + * + * The shape is not invented: it is what `ListView` really emits for a view + * authored `calendar: { dateField, titleField }` — captured by mounting the + * producer with a spy registration — a flat `dateField`, a `titleField`, and NO + * `startDateField`. Measured on both trees with the identical node: the + * merge-base draws, the retiring tree drew "Calendar configuration required". + * + * ⛔ Do not relax this row to make a retirement pass. The retirement is + * available only once the PRODUCER normalises the alias, and the ledger row + * above is what reports that. + */ +describe('objectui#8651 — the node the producer really emits still draws', () => { + it('a node carrying ONLY the aliased date binding renders, and does not refuse', async () => { render( , ); - await waitFor(() => expect(screen.getByText(REFUSAL)).toBeTruthy()); + await waitFor(() => expect(screen.queryByTestId('object-calendar-shell') ?? document.body).toBeTruthy()); + await new Promise((resolve) => setTimeout(resolve, 60)); + expect(screen.queryByText(REFUSAL), 'the aliased binding stopped resolving — this is the objectui#8651 regression').toBeNull(); }); - it('CONTROL: the canonical spelling on the same rows draws a calendar, not the refusal', async () => { + it('CONTROL: the canonical spelling on the same rows also draws', async () => { render( , ); - await waitFor(() => expect(screen.queryByText(REFUSAL)).toBeNull()); + await new Promise((resolve) => setTimeout(resolve, 60)); + expect(screen.queryByText(REFUSAL)).toBeNull(); + }); + + it('CONTROL: the refusal is REACHABLE, so the two rows above are not vacuous', async () => { + // A node with no date binding at all must still refuse — otherwise + // `queryByText(REFUSAL)` returning null above would mean nothing. + render( + , + ); + await waitFor(() => expect(screen.getByText(REFUSAL)).toBeTruthy()); }); }); diff --git a/packages/types/src/__tests__/object-calendar-record-source-7313.test.ts b/packages/types/src/__tests__/object-calendar-record-source-7313.test.ts index 3f918b5f06..cb69a04da3 100644 --- a/packages/types/src/__tests__/object-calendar-record-source-7313.test.ts +++ b/packages/types/src/__tests__/object-calendar-record-source-7313.test.ts @@ -403,26 +403,63 @@ describe('objectui#7313 — the declaration names a live read, in the declared o expect(at, `${RENDERER} no longer calls the shared ladder at all`).toBeGreaterThan(-1); let depth = 0; let end = at + 'resolveRecordSourceConfig'.length; + let closed = false; for (; end < src.length; end += 1) { if (src[end] === '(') depth += 1; else if (src[end] === ')') { depth -= 1; - if (depth === 0) break; + if (depth === 0) { + closed = true; + break; + } } } const call = src.slice(at, end + 1); + expect(closed, 'the paren match ran away — every assertion below is void').toBe(true); + + // ⭐ READ THE ARGUMENT LIST, do not search the slice for the arm's TEXT. + // + // This row shipped with `call.length < src.length` and `call.endsWith(')')` + // as its control, and objectui#8651's contract review showed both are + // satisfied by a paren match that RAN AWAY — a runaway slice is shorter + // than the file and ends in a paren. The control passed in exactly the case + // it existed to catch. + // + // ⚠️ And the obvious repairs do not close it either, which is worth writing + // down so the next person does not re-derive it: delete this call's own + // closing paren and the matcher simply closes on `useMemo`'s instead, + // swallowing the dependency array. That runaway slice still reports + // `closed`, is still balanced on every bracket kind, and still contains no + // declaration — a dependency array is a legal call argument, so NO + // structural test on the slice can separate the two. Measured: 179 chars + // genuine, 249 runaway. + // + // What DOES separate them is the thing this row actually claims — the arm + // is the call's LAST ARGUMENT, not a string that appears somewhere inside + // it. Splitting the argument list at depth 0 gives `['{…}', "'array'"]` for + // the real call and a third `[…]` argument for the runaway, so the arity + // assertion fires. It is also reformat-stable, which a length ceiling is + // not. + const inner = call.slice(call.indexOf('(') + 1, -1); + const args: string[] = []; + let buf = ''; + let d = 0; + for (const ch of inner) { + if ('([{'.includes(ch)) d += 1; + else if (')]}'.includes(ch)) d -= 1; + if (ch === ',' && d === 0) { args.push(buf.trim()); buf = ''; continue; } + buf += ch; + } + args.push(buf.trim()); + const positional = args.filter((a) => a.length > 0); + expect(positional, `${RENDERER}: the ladder call no longer takes exactly (schema, arm)`) + .toHaveLength(2); // The arm is the one `ComponentPropsMap['object-calendar'].data` declares // (`z.array(z.unknown())`, "Pre-fetched records"), which is why it is // `'array'` here and `'view-data'` on `object-grid` / `object-map` / // `object-gantt`. - expect(call, `${RENDERER} no longer calls the shared ladder with its declared arm`) - .toContain("'array'"); - expect(call).not.toContain("'view-data'"); - // CONTROL: the slice really is the call and not the whole file — a paren - // match that ran away would swallow the other arm's name from elsewhere and - // make the refusal above unfailable. - expect(call.length).toBeLessThan(src.length); - expect(call.endsWith(')')).toBe(true); + expect(positional[1], `${RENDERER} no longer calls the shared ladder with its declared arm`) + .toBe("'array'"); }); it('the ladder reads `data`, then `staticData`, then `objectName` — the order the refinement rests on', () => { diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 6b1e2cefda..da51bd68cf 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -446,6 +446,7 @@ export type { ViewNavigationConfig, ViewTabBarConfig, ObjectQLComponentSchema, + ObjectCalendarBlockConfig, BulkActionDef, BulkActionParam, BulkActionOperation, diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index 5feaafbbdb..c5e28a7c8b 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -29,6 +29,21 @@ import type { FormField } from './form.js'; // ListView type is now derived from the zod schema (issue #2231) — see ListViewSchema below. import type { ListViewInferred, ObjectCalendarBlockConfig } from './zod/objectql.zod.js'; +/** + * The type of {@link ObjectCalendarSchema.calendar}, re-exported so the + * published member has a NAME an importer can write (objectui#8651). + * + * ⛔ Spelled with its `from` clause deliberately: a re-export with no module + * specifier is judged as its own declaration by + * `scripts/check-spec-symbol-derivation.mjs`, which is the same reason + * `plugin-calendar`'s deprecated aliases carry theirs. + * + * Without this the member was typed by a name no consumer could reach — exactly + * the "measurably unreachable" property objectui#8651 removed from that + * plugin's local `CalendarSchema`, and it must not come back one layer over. + */ +export type { ObjectCalendarBlockConfig } from './zod/objectql.zod.js'; + // ============================================================================ // Spec-Canonical Types — imported from @objectstack/spec/ui // Rule: "Never Redefine Types. ALWAYS import them." @@ -2849,10 +2864,14 @@ export interface ObjectCalendarSchema extends BaseSchema { * returns this block whole when it is present, and only falls through to the * flat members below when it is not. * - * `@objectstack/spec` declares it — + * `@objectstack/spec` declares the KEY — * `ComponentPropsMap['object-calendar'].calendar` — and this package's - * registration `inputs` publishes it, so authors are offered the key. Both - * published faces of THIS package stayed silent about it until objectui#8651, + * registration `inputs` publishes it, so authors are offered it. ⚠️ The spec + * does NOT declare its SHAPE: measured on 17.4.0 that slot is + * `z.unknown().optional()`, not `CalendarConfigSchema`, so the protocol + * accepts any value there at all. The member list below is objectui's own — + * see the mirror for the grounds. Both published faces of THIS package stayed + * silent about the key until objectui#8651, * which is the objectui#6914 class: the value rode {@link BaseSchema}'s * `[key: string]: any` here and `.passthrough()` on the mirror, admitted and * never examined. `calendar: 42` type-checked, parsed green, and drew an diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index 39d4287d1a..eca6ec2a32 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -688,12 +688,28 @@ const CalendarConfig = stripImportedDefaults(SpecCalendarConfigSchema).partial() * renderer drops — `plugin-calendar/src/index.tsx` names that as this * gate's own failure mode one layer in. * - * The members declared are exactly the five `ObjectCalendar.tsx`'s events pass - * destructures out of the resolved config: the spec's four, plus objectui's own - * `allDayField`. That key has no spec counterpart — `CalendarConfigSchema` is a - * `strictObject` of four keys and refuses it BY NAME — and it is the same - * objectui-local lane objectui#8466 took for the FLAT spelling of this - * vocabulary, on this same interface, for the same renderer. + * ⚠️ THE MEMBER LIST IS objectui's OWN, and the spec does NOT supply it. + * MEASURED on the installed `@objectstack/spec` 17.4.0: + * `ComponentPropsMap['object-calendar'].calendar` is NOT `CalendarConfigSchema` + * — it is `z.unknown().optional()` (wrapper chain `["optional","unknown"]`, and + * not the same object reference), so at THIS position the protocol accepts + * everything: a nonsense key, a wrong-typed member, even `calendar: 42` all + * parse. `CalendarConfigSchema` is the strict four-key object the spec uses for + * a LIST VIEW's calendar block, which is a different position. + * + * ⇒ what the protocol settles here is the KEY, not its SHAPE. The shape below + * is objectui's, chosen as exactly the five members `ObjectCalendar.tsx`'s + * events pass destructures out of the resolved config — the spec's four plus + * objectui's own `allDayField`, the same objectui-local lane objectui#8466 took + * for the FLAT spelling of this vocabulary, on this same interface, for the same + * renderer. + * + * That makes this mirror STRICTER than the protocol at this position, which is + * the sanctioned direction and not the forbidden one: objectui#8327's triage + * ruling forbids accepting what the platform REFUSES, and under `BaseSchema`'s + * `.passthrough()` — which already admitted this key unexamined — a declaration + * can only narrow. The same asymmetry `filter` and `sort` already carry on this + * block. * * ⛔ `.passthrough()` is kept, so this declaration REFUSES NOTHING that parses * today: a `calendar` block carrying `defaultView`, or any other unexamined From f964bb23a00c40581f47984bcfd73d033f01cdd6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 09:52:02 +0000 Subject: [PATCH 4/7] docs(plugin-calendar): route the alias question at its existing carrier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dedup search for the producer-side card returned objectui#8355, which already asks exactly this question — "the renderer carries a lenient alias ladder that no published declaration spells: decide whether the aliases stay, are declared, or are refused" — open, unassigned, `pm:queue`. So no second card is opened; the routing note, the pin header and the changeset all point there instead. Two corrections for whoever takes it, both measured here: its key list says `dateField` / `startField`, and the ladder this renderer actually carries is `dateField` / `endField`; and its three options do not include the fourth this card takes — NORMALISE AT THE PRODUCER — which is the only one that refuses the alias without breaking a live authoring path. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt --- .changeset/olive-buckets-scream.md | 2 +- packages/plugin-calendar/src/ObjectCalendar.tsx | 9 +++++++++ .../src/__tests__/calendarUnionReads-8651.test.tsx | 4 +++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.changeset/olive-buckets-scream.md b/.changeset/olive-buckets-scream.md index a69a49e68c..cf2e7cd66e 100644 --- a/.changeset/olive-buckets-scream.md +++ b/.changeset/olive-buckets-scream.md @@ -10,4 +10,4 @@ fix(plugin-calendar): type `ObjectCalendar` at the published `object-calendar` s - `ObjectCalendarComponentProps.schema` is now `ObjectCalendarSchema`. **Breaking for a React host that passed an `object-grid` node or a `type: 'calendar'` literal to `ObjectCalendar`** — neither was ever a node this renderer is registered for. `ObjectCalendarProps`, the deprecated alias, follows it. - `ObjectCalendarSchema.calendar` is declared on both published faces. The spec declares the KEY; it does **not** declare its shape — `ComponentPropsMap['object-calendar'].calendar` is `z.unknown().optional()` and accepts anything at that position — so the member list is objectui's own: the four `CalendarConfigSchema` names plus objectui's `allDayField`, which is what the renderer reads out of the block. The container keeps `.passthrough()`, so nothing that parsed before is refused; what is new is value validation, and `calendar: 42` and `calendar: { startDateField: 42 }` are refused where both were admitted unexamined. - `@object-ui/types` now exports the type `ObjectCalendarBlockConfig`, so that published member has a name an importer can write. -- ⚠️ The `dateField` / `endField` alias rungs in `getCalendarConfig` are **kept**, and routed to the producer. An earlier revision of this change retired them on a census that was false: `ListView` flattens an authored `calendar` block onto the node it emits and objectui's own `ListViewSchema` accepts `calendar.dateField`, so a view authored that way renders today and would have drawn "Calendar configuration required". No behaviour changes for either spelling; the remedy belongs in `ListView`'s calendar branch and is filed separately. +- ⚠️ The `dateField` / `endField` alias rungs in `getCalendarConfig` are **kept**, and routed to the producer. An earlier revision of this change retired them on a census that was false: `ListView` flattens an authored `calendar` block onto the node it emits and objectui's own `ListViewSchema` accepts `calendar.dateField`, so a view authored that way renders today and would have drawn "Calendar configuration required". No behaviour changes for either spelling. The alias question already has a carrier — objectui#8355, open and undecided — and the producer-side remedy belongs in `ListView`'s calendar branch. diff --git a/packages/plugin-calendar/src/ObjectCalendar.tsx b/packages/plugin-calendar/src/ObjectCalendar.tsx index 0a4c7cf9db..6b90e6eb20 100644 --- a/packages/plugin-calendar/src/ObjectCalendar.tsx +++ b/packages/plugin-calendar/src/ObjectCalendar.tsx @@ -281,6 +281,15 @@ type CalendarAliasRungs = { dateField?: string; endField?: string }; * asserts they are still read AND that the producer still flattens — so when * the producer is fixed the ledger reddens and the rungs can go. * + * ⭐ THE CARRIER IS objectui#8355, which already asks exactly this — *"the + * renderer carries a lenient alias ladder that no published declaration spells + * — decide whether the aliases stay, are declared, or are refused"* — and is + * OPEN and UNDECIDED. ⛔ Do not open a second card for it. Two notes for + * whoever takes it: its key list says `dateField` / `startField`, and the + * ladder measured here is `dateField` / `endField`; and its three options do + * not include the fourth this card takes, NORMALISE AT THE PRODUCER, which is + * the only one that refuses the alias without breaking the authoring path. + * * ⛔ Do not re-retire these on a text census. The only census that can answer * this question runs the producer. */ diff --git a/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx b/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx index d647a9342d..cb2f71e19d 100644 --- a/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx +++ b/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx @@ -77,7 +77,9 @@ * with a flat `dateField` and NO `startDateField`, which the merge-base * draws and the retiring tree refused. ⇒ the remedy is at the producer, * which is outside this card's file surface, so the rungs STAY and both - * keys are ledgered below with the producer named. + * keys are ledgered below with the producer named. The standing carrier for + * the alias question is objectui#8355 (open, undecided); ⛔ this card does + * not open a second one and does not decide it. * * ## ⛔ `navigation` is NOT ruled here, and its verdict is INVARIANT * From d4d1b7677db6893c59a8ea2793024750bc77c7bd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 11:19:02 +0000 Subject: [PATCH 5/7] docs(plugin-calendar): delete every surviving copy of three retracted claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-2 contract review: three defects, all the same shape — a state assertion repaired in one place while copies survived elsewhere. Swept rather than line-fixed, and the sweep found two copies the review did not name. COUNTING RULE for the sweep. Population: every line of prose this branch ADDS (`git diff -U0 ..`, `^+` minus `+++`), all 9 changed files, 927 lines. That is the right population and the whole-file one is not: a false claim of mine can only live in text I wrote, and it is strictly wider than the hunk-local read that missed round 2's item 1 (two copies ~840 lines apart in one file are both in it). Because the population is small, each class is enumerated EXHAUSTIVELY by keyword and hand-classified — no semantic matcher, hence no matcher gap. A heuristic first pass MISSED a section header the review named, which is why the exhaustive form replaced it. 1. The false union rule, both copies (ObjectCalendar.tsx, the pin). Replaced with the reviewer's wording verbatim. Class A: 21 added lines contain "union"; 2 stated the false rule; 0 remain. 2. The restated zero-producer census (ObjectCalendar.tsx) — both halves false, and it also called the aliases "the retired spellings". Rewritten to say they are ROUTED, still read, and have a producer. Class B: 36 added lines contain "producer"; 1 asserted the retracted census in the indicative; 0 remain. The surviving "No producer hands this component one" is about `ObjectGridSchema` and is true. 3. Both stale section headers in the pin. Class C: 18 added lines contain "retir*", every one hand-classified; 0 stale remain. ⭐ TWO COPIES THE REVIEW DID NOT NAME, found by the exhaustive pass: the pin called the aliases "a retired spelling" twice more, in the comment-mask rationale at two sites. Both reworded. Also tightened, as the addendum invited: the mirror's "REFUSES NOTHING that parses today", which is literally false for `calendar: 42`. Now split into the KEY claim (true) and the VALUE narrowing (the point of the declaration). ⛔ No pre-existing line was edited. ObjectCalendar.tsx:200 ("lived under the retired spelling", the objectui#7711 `filter.calendar` retirement) is absent from the added-line population, asserted by a provenance control in the sweep. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt --- .../plugin-calendar/src/ObjectCalendar.tsx | 18 +++++++++++++----- .../calendarUnionReads-8651.test.tsx | 19 ++++++++++++------- packages/types/src/zod/objectql.zod.ts | 16 ++++++++++++---- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/packages/plugin-calendar/src/ObjectCalendar.tsx b/packages/plugin-calendar/src/ObjectCalendar.tsx index 6b90e6eb20..7e4e8f96c9 100644 --- a/packages/plugin-calendar/src/ObjectCalendar.tsx +++ b/packages/plugin-calendar/src/ObjectCalendar.tsx @@ -109,8 +109,9 @@ import { * - the local interface was absent from this package's barrel, so no importer * could name it, and it SHADOWED `@object-ui/types`' own published * `CalendarSchema` — the date-picker primitive reachable at `ui:calendar` - * only (objectui#8499). Its two distinctive members, the retired spellings - * noted on `getCalendarConfig` below, had no producer at all. + * only (objectui#8499). Its two distinctive members are the alias spellings + * `getCalendarConfig` below ROUTES to the producer (carrier objectui#8355); + * they are still read, and they have a producer. * * The shape this leaves is the family's: `ObjectKanban` takes * `ObjectKanbanSchema`, `ObjectGantt` takes `ObjectGanttSchema`, `ObjectMap` @@ -928,9 +929,16 @@ export const ObjectCalendar: React.FC = ({ // objectui#8652's key: the maintainer ruled B there — declare it on the // platform element schemas first, then mirror — and that card is `pm:blocked` // on objectstack#17987. Its declaredness verdict at this read site is - // UNCHANGED by this card: through the retired union it was undeclared too - // (one arm declared it, and a union member is declared only when EVERY arm - // does), and it is undeclared on `ObjectCalendarSchema`. Ledgered by name, and + // UNCHANGED by this card: through the retired union it was undeclared too, + // and it is undeclared on `ObjectCalendarSchema`. The rule that makes that + // come out right is NOT "declared on every arm" — the checker reading this + // file records above carries five keys on the union that only ONE arm + // declares (`colorField` `dateField` `defaultView` `endField` `titleField`), + // because + // `ObjectGridSchema`'s index signature supplies the rest. It is: a union + // member is available only when EVERY arm supplies it — by its own + // declaration OR through an applicable index signature. `CalendarSchema` has + // neither for `navigation`, so the union does not carry it. Ledgered by name, and // asserted to be STILL READ, in `__tests__/calendarUnionReads-8651.test.tsx`. const navConfig = (schema as any).navigation ?? { mode: 'drawer' }; const navIsOverlay = navConfig.mode === 'drawer' || navConfig.mode === 'modal' || navConfig.mode === 'split' || navConfig.mode === 'popover'; diff --git a/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx b/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx index cb2f71e19d..dba6319f52 100644 --- a/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx +++ b/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx @@ -89,9 +89,14 @@ * rule, declare, retire or touch it. * * ⭐ It does not, and that is measurable rather than asserted: through the - * UNION the key was already undeclared (`ObjectGridSchema` declares it, - * `CalendarSchema` does not, and a union member is declared only when EVERY arm - * declares it), and on `ObjectCalendarSchema` it is undeclared too. Same + * UNION the key was already undeclared, and on `ObjectCalendarSchema` it is + * undeclared too. ⚠️ The rule is NOT "declared on every arm" — the reading + * recorded at the top of this file carries five keys on the union that only ONE + * arm declares (`colorField` `dateField` `defaultView` `endField` `titleField`). + * It is: a union member is available only when EVERY arm supplies it — by its + * own declaration OR through an applicable index signature. `ObjectGridSchema` + * declares `navigation`; `CalendarSchema` neither declares it nor has an index + * signature to supply it; so the union does not carry it. Same * verdict at the read site before and after; the read itself is untouched. It * is ledgered BY NAME below, with an assertion that it is STILL READ — a stale * exception is a hole (the objectui#8885 ledger discipline). @@ -102,7 +107,7 @@ * instrument that re-derives it every run, never by a figure in prose. So the * central row below takes BOTH sides from the tree at run time — the keys read * come from a cast-aware census over `ObjectCalendar.tsx` (objectui#6576's - * `schemaReads`, comment-masked so a retired spelling surviving in prose cannot + * `schemaReads`, comment-masked so a key that appears only in prose cannot * move a verdict), and the keys declared come from the zod mirror's own * `.shape`, never from parse acceptance, which under `.passthrough()` cannot * tell "declared" from "admitted unexamined" (objectui#8466's instrument). @@ -189,7 +194,7 @@ function schemaReads(source: string): Set { return new Set([...source.matchAll(SCHEMA_READ)].map((m) => m[1])); } -/** The renderer's reads, with prose masked so a retired spelling in a comment cannot vote. */ +/** The renderer's reads, with prose masked so a key named only in a comment cannot vote. */ function rendererReads(): Set { return schemaReads(mask(readRepo(CALENDAR_READER))); } @@ -346,7 +351,7 @@ describe('objectui#8651 — `calendar` is declared; the spec sets the KEY, objec }); }); -/* ── 4. The retired spellings ──────────────────────────────────────────────── */ +/* ── 4. The routed spellings ───────────────────────────────────────────────── */ describe('objectui#8651 — the `dateField` / `endField` rungs are ROUTED, not retired', () => { it('both are STILL READ — the rungs stay until the producer is fixed', () => { @@ -403,7 +408,7 @@ describe('objectui#8651 — the `dateField` / `endField` rungs are ROUTED, not r }); }); -/* ── 5. …and the retirement is observable on screen ───────────────────────── */ +/* ── 5. …and the regression is observable on screen ──────────────────────── */ vi.mock('@object-ui/plugin-detail', async (importOriginal) => ({ ...(await importOriginal()), diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index eca6ec2a32..53079c7376 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -711,12 +711,20 @@ const CalendarConfig = stripImportedDefaults(SpecCalendarConfigSchema).partial() * can only narrow. The same asymmetry `filter` and `sort` already carry on this * block. * - * ⛔ `.passthrough()` is kept, so this declaration REFUSES NOTHING that parses + * ⛔ `.passthrough()` is kept, so this declaration refuses no KEY that parses * today: a `calendar` block carrying `defaultView`, or any other unexamined * key, still parses exactly as it did through `BaseSchema`'s own - * `.passthrough()`. What it adds is VALUE validation on the five named members - * — `calendar: 42` and `calendar: { startDateField: 42 }` are refused where - * both were admitted unexamined before. + * `.passthrough()`. It does refuse VALUES, which is the whole of what declaring + * buys — `calendar: 42` and `calendar: { startDateField: 42 }` are refused + * where both were admitted unexamined before. + * + * ⚠️ The key/value split is stated that way on purpose. An earlier cut wrote + * "REFUSES NOTHING that parses today", which is literally false for + * `calendar: 42`: it parsed at the merge-base and is refused here. The colon + * scoped it to keys and the next sentence gave the value narrowing, so it was + * defensible — but a sentence that needs its own punctuation to stay true is + * one reader away from being wrong, and the narrowing is the point of the + * declaration rather than a footnote to it. */ const ObjectCalendarBlockConfigSchema = stripImportedDefaults(SpecCalendarConfigSchema).partial().extend({ // objectui-local, no spec counterpart — see objectui#8466 for the measurement From 6fd5711526d4a7b1b7e33b465460c28cb711aa89 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 12:27:29 +0000 Subject: [PATCH 6/7] docs(changeset): scope the passthrough claim, and derive the sweep from the retraction set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-3 contract review: one item. The changeset published the very claim this branch retracted, in a strictly weaker form — "The container keeps `.passthrough()`, so nothing that parsed before is refused" — with no colon and no noun KEY to scope it, and the same sentence disproves it eleven words later. It ships to the CHANGELOG, so it was the most widely-read copy of all. Verified rather than inherited, on this tree with five controls: `calendar: 42` and `calendar: { startDateField: 42 }` are REFUSED, while a well-formed block, `calendar.dateField`, `calendar.defaultView`, an unexamined key inside the block and a nonsense top-level key all PARSE. So the refusal is value-level, not a ceiling change, and the scoped sentence is the true one. ⭐ THE REAL FIX IS THE SWEEP'S CLASS DERIVATION, not the wording. Round 3's classes came from the defects the REVIEW NAMED (union / producer / retir*). That is why it honestly cleared those three and still missed this: the missed claim shares no word with any of them. Classes must come from the RETRACTION SET — every claim this branch changed the truth of. Nine classes now, R1..R9, each with its predicate published beside its count and each proved able to fire against the retracted sentence itself rather than against the corpus it judges (a population control cannot fire once a claim is fully removed, and a class that cannot fire reports VOID rather than a reading). Result: 0 unframed surviving retracted assertions; the 5 signature hits are all quotations inside their own retraction. Also scoped, as the addendum invited: the changeset's other passthrough-leaning sentence. "ListViewSchema accepts `calendar.dateField`" is true but weak — measured here, that block also admits a nonsense key (while still refusing a wrong value type), so the load-bearing half is the producer spread and `resolveTimelineDateBinding`, not the accept. And the compound-noun ambiguity in ObjectCalendar.tsx's rule statement is reworded; it still does not claim the file measures the figure. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt --- .changeset/olive-buckets-scream.md | 4 ++-- packages/plugin-calendar/src/ObjectCalendar.tsx | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.changeset/olive-buckets-scream.md b/.changeset/olive-buckets-scream.md index cf2e7cd66e..2114d5add7 100644 --- a/.changeset/olive-buckets-scream.md +++ b/.changeset/olive-buckets-scream.md @@ -8,6 +8,6 @@ fix(plugin-calendar): type `ObjectCalendar` at the published `object-calendar` s `ObjectCalendarComponentProps.schema` was the union `ObjectGridSchema | CalendarSchema` — a grid's schema, plus a plugin-local interface absent from this package's barrel. Neither arm is the schema of the element this renderer is registered as. Measured with the TypeScript checker: of the fifteen keys the renderer reads off the node, seven were undeclared on that union and had to be read through a cast, while five more resolved only through `ObjectGridSchema`'s index signature and were therefore typed `any` — admitted, never examined. `ObjectCalendarSchema` already declared eleven of the fifteen. - `ObjectCalendarComponentProps.schema` is now `ObjectCalendarSchema`. **Breaking for a React host that passed an `object-grid` node or a `type: 'calendar'` literal to `ObjectCalendar`** — neither was ever a node this renderer is registered for. `ObjectCalendarProps`, the deprecated alias, follows it. -- `ObjectCalendarSchema.calendar` is declared on both published faces. The spec declares the KEY; it does **not** declare its shape — `ComponentPropsMap['object-calendar'].calendar` is `z.unknown().optional()` and accepts anything at that position — so the member list is objectui's own: the four `CalendarConfigSchema` names plus objectui's `allDayField`, which is what the renderer reads out of the block. The container keeps `.passthrough()`, so nothing that parsed before is refused; what is new is value validation, and `calendar: 42` and `calendar: { startDateField: 42 }` are refused where both were admitted unexamined. +- `ObjectCalendarSchema.calendar` is declared on both published faces. The spec declares the KEY; it does **not** declare its shape — `ComponentPropsMap['object-calendar'].calendar` is `z.unknown().optional()` and accepts anything at that position — so the member list is objectui's own: the four `CalendarConfigSchema` names plus objectui's `allDayField`, which is what the renderer reads out of the block. The container keeps `.passthrough()`, so no KEY that parsed before is refused — an unexamined key inside the block still parses, and so do `calendar.dateField` and `calendar.defaultView`. What is new is VALUE validation: `calendar: 42` and `calendar: { startDateField: 42 }` are refused where both were admitted unexamined. - `@object-ui/types` now exports the type `ObjectCalendarBlockConfig`, so that published member has a name an importer can write. -- ⚠️ The `dateField` / `endField` alias rungs in `getCalendarConfig` are **kept**, and routed to the producer. An earlier revision of this change retired them on a census that was false: `ListView` flattens an authored `calendar` block onto the node it emits and objectui's own `ListViewSchema` accepts `calendar.dateField`, so a view authored that way renders today and would have drawn "Calendar configuration required". No behaviour changes for either spelling. The alias question already has a carrier — objectui#8355, open and undecided — and the producer-side remedy belongs in `ListView`'s calendar branch. +- ⚠️ The `dateField` / `endField` alias rungs in `getCalendarConfig` are **kept**, and routed to the producer. An earlier revision of this change retired them on a census that was false: `ListView` flattens an authored `calendar` block onto the node it emits, and `resolveTimelineDateBinding` in that same file documents `dateField` as the pre-#2231 alias for `startDateField` and honours it — so a view authored that way renders today and would have drawn "Calendar configuration required". (objectui's own `ListViewSchema` also accepts `calendar.dateField`, but only weakly: that block is `.passthrough()` and admits nonsense too, so the load-bearing half is the producer and the read site, not the accept.) No behaviour changes for either spelling. The alias question already has a carrier — objectui#8355, open and undecided — and the producer-side remedy belongs in `ListView`'s calendar branch. diff --git a/packages/plugin-calendar/src/ObjectCalendar.tsx b/packages/plugin-calendar/src/ObjectCalendar.tsx index 7e4e8f96c9..b64a7f1d7b 100644 --- a/packages/plugin-calendar/src/ObjectCalendar.tsx +++ b/packages/plugin-calendar/src/ObjectCalendar.tsx @@ -931,11 +931,11 @@ export const ObjectCalendar: React.FC = ({ // on objectstack#17987. Its declaredness verdict at this read site is // UNCHANGED by this card: through the retired union it was undeclared too, // and it is undeclared on `ObjectCalendarSchema`. The rule that makes that - // come out right is NOT "declared on every arm" — the checker reading this - // file records above carries five keys on the union that only ONE arm - // declares (`colorField` `dateField` `defaultView` `endField` `titleField`), - // because - // `ObjectGridSchema`'s index signature supplies the rest. It is: a union + // come out right is NOT "declared on every arm". In the checker reading + // recorded above, five keys ride the union although only ONE arm declares + // them — `colorField`, `dateField`, `defaultView`, `endField` and + // `titleField` — because `ObjectGridSchema`'s index signature supplies them. + // The rule is: a union // member is available only when EVERY arm supplies it — by its own // declaration OR through an applicable index signature. `CalendarSchema` has // neither for `navigation`, so the union does not carry it. Ledgered by name, and From 7a21da6933f0c81b4de94c69b1d225974db360c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 13:50:22 +0000 Subject: [PATCH 7/7] docs: repair four claims this landing falsifies, and sweep by referent not vocabulary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-4 contract review: four items, adopted whole. Verified against the review comment itself (5664609021) rather than the relayed copy. THE FINDING BEHIND ALL FOUR. The retraction set has two halves that live in different places. INTRA-BRANCH retractions (an earlier revision of this PR asserted it and withdrew it) live in ADDED lines. TREE-STATE retractions (the tree asserts it and this landing falsifies it) live in UNTOUCHED lines. Eight of round-4's nine classes were intra-branch and the population was added lines, so the population bound and the class list were the same blind spot counted twice and could not correct each other. TWO RULES REPLACE THEM, and they are the deliverable: RULE 1 POPULATION — draw it from the WHOLE TRACKED TREE AT HEAD, never from the diff. A claim this landing falsifies was written by someone who never read the retraction, so nothing about it correlates with what I touched. RULE 2 PREDICATE — derive it from the retracted claim's REFERENTS (the symbol names) never from the vocabulary of the retracting sentence. The surviving text says `ObjectGridSchema | CalendarSchema` and never says "union". Run that way over 7521 tracked files / 1.68M lines, three referent classes found every site the review named PLUS one it did not: the same stale call-spelling in `zod/objectql.zod.ts`. Each class carries a CAN-FIRE leg against a synthetic string, never against the corpus. 1. `content/docs/plugins/plugin-calendar.mdx` — the doc-snippet marker asserted in the present tense that the prop is the grid/date-picker union and that neither arm admits an `object-calendar` node. Measured here: the shipped type is `ObjectCalendarSchema`, and compiling that exact block gives TS2741 x1 / TS2322 x0. Reason rewritten to the discriminant, objectui#7311 framing dropped. ⛔ The old measurement is NOT asserted to have held when written — that needs a build of the old tree, which was not done. 2. The method above, plus the one instance it surfaced that publishes: `.changeset/7632-*.md` asserted the union in the present tense. Past-tensed. The two `packages/core` twins are outside this card's file surface and are already carried by objectui#9473. 3. The stale `resolveRecordSourceConfig(schema, 'array')` citation, both sites — `objectql.ts` and the one in `objectql.zod.ts` the review did not name. Now cite the ladder and the ARM, never the call shape. 4. "the spec refuses both by name" is false. Measured on 17.4.0: the props object is strict with nine flat members, and it answers the two aliases, a nonsense key AND the five canonical field keys with the identical `unrecognized_keys` diagnostic — so the ground did not discriminate, and applied as written would have required undeclaring the canonical five. Replaced with the measured mechanism and the real ground (deprecated pre-#2231 aliases, carrier objectui#8355). Same correction applied to all three `allDayField` copies of the claim. And the test's control moves from a nonsense key to `startDateField`: a nonsense key is refused for the same reason the subject is, so it shared the suspect part and could not separate a targeted refusal from blanket strictness. ⚠️ Three claims of my OWN were caught by re-reading this repair diff before pushing, none of which CI could see: "that was true when written" (not measured), "the five canonical field keys this same card declares" (this card declares `calendar`, not those five), and an over-broad "the reasoning below is unchanged". Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt --- .../7632-shared-record-source-config.md | 7 +++- content/docs/plugins/plugin-calendar.mdx | 2 +- .../plugin-calendar/src/ObjectCalendar.tsx | 21 ++++++++-- .../calendarUnionReads-8651.test.tsx | 39 ++++++++++++++++--- packages/types/src/objectql.ts | 15 ++++--- packages/types/src/zod/objectql.zod.ts | 9 +++-- 6 files changed, 72 insertions(+), 21 deletions(-) diff --git a/.changeset/7632-shared-record-source-config.md b/.changeset/7632-shared-record-source-config.md index b455fd38be..73151cc6c1 100644 --- a/.changeset/7632-shared-record-source-config.md +++ b/.changeset/7632-shared-record-source-config.md @@ -31,8 +31,11 @@ input matrix, so a later edit to the shared reader that moves any site turns red **Two divergences were measured rather than assumed, and both are preserved.** -`ObjectCalendar`'s `'data' in schema && schema.data` guards exist because its parameter is -the union `ObjectGridSchema | CalendarSchema` and `CalendarSchema` declares neither key. +`ObjectCalendar`'s `'data' in schema && schema.data` guards existed because its parameter was +at that time the union `ObjectGridSchema | CalendarSchema`, whose `CalendarSchema` arm declared +neither key. (objectui#8651 has since re-pointed that parameter at the published +`ObjectCalendarSchema`, so the union is gone; the CONCLUSION below — that the guard had no +runtime effect and removing it is behaviour-neutral — is unaffected.) That is a TypeScript narrowing device with no runtime effect — an absent property reads `undefined`, falsy either way — so the guard could never change which rung is taken. The shared reader's optional-property parameter accepts the union directly, and the diff --git a/content/docs/plugins/plugin-calendar.mdx b/content/docs/plugins/plugin-calendar.mdx index b892cff599..14013b17bd 100644 --- a/content/docs/plugins/plugin-calendar.mdx +++ b/content/docs/plugins/plugin-calendar.mdx @@ -518,7 +518,7 @@ const taskCalendar: ObjectCalendarSchema = { You can also import and use the components directly in React: -{/* doc-snippet: fragment — the `ObjectCalendar` half cannot compile against the SHIPPED prop type: `ObjectCalendarComponentProps.schema` is declared `ObjectGridSchema | CalendarSchema`, and neither admits an `object-calendar` node — `ObjectGridSchema.type` is the literal `'object-grid'` and `CalendarSchema` is the FORM date picker (`type: 'calendar'`). The renderer registered for `object-calendar` passes exactly this shape (`index.tsx`), and `ObjectCalendar.tsx` reads `objectName` / `calendar` / `staticData` off it, so the runtime path is real and the declaration is the stale half — filed as objectui#7311 rather than papered over with a cast (measured: TS2322 x1) */} +{/* doc-snippet: fragment — the `ObjectCalendar` half cannot compile as written, because the `schema` literal below omits the required `type: 'object-calendar'` discriminant. The SHIPPED prop type is `ObjectCalendarComponentProps.schema: ObjectCalendarSchema` (`plugin-calendar/dist/ObjectCalendar.d.ts`), whose `type` member is the literal `'object-calendar'`. Compiling this exact block against the built workspace measures TS2741 x1 (Property 'type' is missing) and TS2322 x0. ⚠️ This reason was rewritten by objectui#8651: it previously said the prop was declared a grid/date-picker union and that neither arm admitted an `object-calendar` node, with a measurement of TS2322 x1. Every clause of that reason is false at this head, because objectui#8651 re-pointed the prop at the published element schema — which is also why the objectui#7311 'stale declaration' framing is gone. ⛔ Whether the old TS2322 x1 held when it was written is NOT asserted here: that would need a build of the old tree, which was not done. The fix for the block itself is to add the discriminant, not a cast. */} ```tsx import { CalendarView, ObjectCalendar } from '@object-ui/plugin-calendar'; import type { DataSource } from '@object-ui/types'; diff --git a/packages/plugin-calendar/src/ObjectCalendar.tsx b/packages/plugin-calendar/src/ObjectCalendar.tsx index b64a7f1d7b..57b39d0d43 100644 --- a/packages/plugin-calendar/src/ObjectCalendar.tsx +++ b/packages/plugin-calendar/src/ObjectCalendar.tsx @@ -242,9 +242,24 @@ type ObjectCalendarConfig = CalendarConfig & { /** * The two PRE-#2231 alias spellings `getCalendarConfig` still honours, declared * ONLY as a cast target and deliberately NOT as schema members — see the - * routing note on `getCalendarConfig`. `@objectstack/spec` refuses both by name - * at the flat position, so a declaration here would make this repo accept what - * the platform refuses. + * routing note on `getCalendarConfig`. + * + * ⚠️ The ground is NOT that the spec singles these two out. MEASURED on + * installed `@objectstack/spec` 17.4.0: `ComponentPropsMap['object-calendar']` + * is STRICT and declares exactly nine flat members — `calendar` `data` + * `defaultView` `filter` `loading` `locale` `objectName` `sort` `staticData` — + * so it refuses every undeclared flat key with the same `unrecognized_keys` + * diagnostic: these two aliases, a nonsense key, AND the five canonical field + * keys `ObjectCalendarSchema` already declares and this renderer reads (`startDateField` + * `endDateField` `titleField` `colorField` `allDayField`). ⛔ Blanket strictness cannot be the reason these + * two stay undeclared — applied as a reason it would require undeclaring those + * canonical five, and this repo's mirror being stricter than the protocol is + * the SANCTIONED direction anyway (see `zod/objectql.zod.ts`). + * + * The real ground is narrower: they are deprecated pre-#2231 ALIASES of keys + * this schema already declares, and the alias question has an open carrier — + * objectui#8355 — which has not ruled. Declaring an alias would settle that + * card by accretion; routing it to the producer leaves it open. */ type CalendarAliasRungs = { dateField?: string; endField?: string }; diff --git a/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx b/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx index dba6319f52..95769f7b8b 100644 --- a/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx +++ b/packages/plugin-calendar/src/__tests__/calendarUnionReads-8651.test.tsx @@ -157,9 +157,26 @@ const LEDGERED_OTHER_CARD_READS = ['navigation'] as const; /** * The two PRE-#2231 alias spellings this card ROUTES TO THE PRODUCER rather * than declaring or retiring. Both are genuinely undeclared on - * `ObjectCalendarSchema` and must stay so — `@objectstack/spec` refuses both by - * name at the flat position, so declaring them would make this repo accept what - * the platform refuses. Each entry is asserted to be STILL READ, and the + * `ObjectCalendarSchema` and must stay so. + * + * ⚠️ The ground is NOT that the spec singles these two out. MEASURED on + * installed `@objectstack/spec` 17.4.0: `ComponentPropsMap['object-calendar']` + * is STRICT and declares exactly nine flat members — `calendar` `data` + * `defaultView` `filter` `loading` `locale` `objectName` `sort` `staticData` — + * so it refuses every undeclared flat key with the same `unrecognized_keys` + * diagnostic: these two aliases, a nonsense key, AND the five canonical field + * keys `ObjectCalendarSchema` already declares and this renderer reads (`startDateField` + * `endDateField` `titleField` `colorField` `allDayField`). ⛔ Blanket strictness cannot be the reason these + * two stay undeclared — applied as a reason it would require undeclaring those + * canonical five, and this repo's mirror being stricter than the protocol is + * the SANCTIONED direction anyway (see `zod/objectql.zod.ts`). + * + * The real ground is narrower: they are deprecated pre-#2231 ALIASES of keys + * this schema already declares, and the alias question has an open carrier — + * objectui#8355 — which has not ruled. Declaring an alias would settle that + * card by accretion; routing it to the producer leaves it open. + * + * Each entry is asserted to be STILL READ, and the * PRODUCER is asserted to still flatten its block, so when `ListView` is fixed * this ledger reddens rather than rotting into a permanent exemption. */ @@ -370,11 +387,21 @@ describe('objectui#8651 — the `dateField` / `endField` rungs are ROUTED, not r it('⛔ neither is DECLARED — declaring would accept what the platform refuses', () => { const declared = shapeKeys(ObjectCalendarMirror); for (const key of ROUTED_TO_PRODUCER) expect(declared).not.toContain(key); - // …and that is the spec's verdict, not an assumption. Same call, with a - // known-accepted key and a nonsense key as the two controls. + // …and that is the spec's verdict, not an assumption. + // + // ⭐ THE CONTROL IS `startDateField`, NOT a nonsense key. A nonsense key is + // refused for exactly the same reason the subject is — the props object is + // strict — so it shares the suspect part of the instrument and cannot + // separate "the spec singles these two out" from "the spec refuses every + // undeclared flat key". `startDateField` varies only the claim: it is a key + // this very card DECLARES on the mirror, and the spec refuses it flat too. + // Its `false` is the finding, not a failure: it shows the refusal is + // blanket, which is why blanket strictness is not the routing's ground. const oc = (ComponentPropsMap as unknown as Record)['object-calendar']; expect(oc.safeParse({ objectName: 'duly_task' }).success).toBe(true); - expect(oc.safeParse({ objectName: 'duly_task', [CONTROL_KEY]: 'x' }).success).toBe(false); + expect(oc.safeParse({ objectName: 'duly_task', startDateField: 'kickoff' }).success, + 'the spec now accepts a flat startDateField — the refusal is no longer blanket, so re-read the routing note') + .toBe(false); for (const key of ROUTED_TO_PRODUCER) { const r = oc.safeParse({ objectName: 'duly_task', [key]: 'x' }); expect(r.success, `the spec now accepts a flat ${key}; revisit this routing`).toBe(false); diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index c5e28a7c8b..941cc00d04 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -2818,9 +2818,11 @@ export interface ObjectCalendarSchema extends BaseSchema { objectName?: string; /** * PRE-FETCHED RECORDS — an ARRAY, drawn in place of the calendar's own query. - * Read FIRST by the shared record-source ladder - * (`resolveRecordSourceConfig(schema, 'array')` in `@object-ui/core`), ahead - * of `staticData` / `objectName`. + * Read FIRST by the shared record-source ladder in `@object-ui/core`, on the + * `'array'` arm, ahead of `staticData` / `objectName`. ⛔ The ladder's ARM is + * the citation; its first argument is not, because that is a call SHAPE and + * it has already moved once — objectui#8651 now passes the three members the + * ladder documents itself as reading, one by one. * * Declared by objectui#7313, in the same stroke as the mirror's `data`: until * then the read landed on `BaseSchema`'s index signature on this side and @@ -2925,8 +2927,11 @@ export interface ObjectCalendarSchema extends BaseSchema { * * objectui-LOCAL, and the one member here with no {@link CalendarConfig} twin * to derive from: `@objectstack/spec`'s `CalendarConfigSchema` is a - * `strictObject` of four keys and refuses this one BY NAME with an - * `unrecognized_keys` diagnostic. That is the class this package's mirror + * `strictObject` of exactly `startDateField`, `endDateField`, `titleField` + * and `colorField`, so it refuses this key as UNDECLARED — ⚠️ not "by name". + * Measured on 17.4.0: it answers `allDayField` and a nonsense key with the + * identical `unrecognized_keys` diagnostic, so the refusal is blanket + * strictness and says nothing about this key in particular (objectui#8651). That is the class this package's mirror * already names out loud, where `.passthrough()` is kept explicitly for this * key — "the renderers grow config knobs ahead of the protocol (calendar's * `allDayField`, for one), and stripping them here would silently disable a diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index 53079c7376..ba4907a33c 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -730,7 +730,7 @@ const ObjectCalendarBlockConfigSchema = stripImportedDefaults(SpecCalendarConfig // objectui-local, no spec counterpart — see objectui#8466 for the measurement // and the lane. The renderer honours it in BOTH positions: this container and // the flat member of the node. - allDayField: z.string().optional().describe("Field carrying the all-day flag — objectui-local (the spec's CalendarConfigSchema refuses it by name); LOAD-BEARING since objectui#8026"), + allDayField: z.string().optional().describe("Field carrying the all-day flag — objectui-local: the spec's CalendarConfigSchema is a strict object of startDateField, endDateField, titleField and colorField, so it refuses this key as undeclared, exactly as it refuses any other. LOAD-BEARING since objectui#8026"), }).passthrough(); /** @@ -1056,8 +1056,9 @@ export const ObjectMapConfigSchema = z.object({ * place of the block's own query, NOT a source to fetch from. * `ComponentPropsMap['object-calendar'].data` is `z.array(z.unknown()) * .optional()` on `@objectstack/spec` 17.4.0 and the renderer honours that - * arm alone since objectui#8348 (`resolveRecordSourceConfig(schema, - * 'array')`); objectui#9239 brought this file's member onto it. + * arm alone since objectui#8348 (the shared ladder, called on the `'array'` + * arm — ⛔ the arm is the citation, not the call shape, which has moved); + * objectui#9239 brought this file's member onto it. * * ⛔ So do not read the message below as promising a fetchable source: on the * calendar, declaring `data` means handing the block rows it already has. @@ -1342,7 +1343,7 @@ export const ObjectCalendarSchema = BaseSchema.extend({ // face is objectui's own lane — `titleField`/`startDateField`/`endDateField` // have shipped declared here, and absent from `inputs`, for releases. colorField: z.string().optional().describe('Field carrying the per-record event colour — a CSS colour or a semantic palette name'), - allDayField: z.string().optional().describe("Field carrying the all-day flag — objectui-local (the spec's CalendarConfigSchema refuses it by name); LOAD-BEARING since objectui#8026"), + allDayField: z.string().optional().describe("Field carrying the all-day flag — objectui-local: the spec's CalendarConfigSchema is a strict object of startDateField, endDateField, titleField and colorField, so it refuses this key as undeclared, exactly as it refuses any other. LOAD-BEARING since objectui#8026"), defaultView: z.enum(['month', 'week', 'day']).optional().describe("Default view — 'month' | 'week' | 'day', the renderer's rendered set ('agenda' was retired: objectui#5784)"), // objectui#8174 — the two query keys `ObjectCalendar.tsx` lowers onto its own // `dataSource.find` (`$filter: schema.filter`,