From bac7f878968e9bb9f5fbe69f9918cc2d04f214f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 01:31:53 +0000 Subject: [PATCH 1/2] docs(types,plugin-calendar): teach the object-calendar field-name keys inside the calendar block, and name the flat spelling as the runtime handoff (objectui#8831) The spec refuses startDateField / endDateField / titleField / colorField / allDayField written flat on an object-calendar node and prescribes the calendar block; 17.5.0 declares all five there. The README's two object-calendar examples now write the block, the calendar-view sentence is scoped to calendar-view (it has no calendar block), and the types' flat member descriptions call the flat spelling the read-only handoff. No accept set moves: the flat members stay declared because the renderer reads them. The 8466 README pin is inverted to the new teaching, and the 8466 changeset gets a dated note. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT Co-authored-by: Claude --- .../8466-calendar-color-allday-fields.md | 2 + .../8831-calendar-flat-teaching-face.md | 13 ++ packages/plugin-calendar/README.md | 77 +++++---- .../calendar-flat-color-allday-8466.test.ts | 161 ++++++++++++++---- packages/types/src/objectql.ts | 84 ++++++--- packages/types/src/zod/objectql.zod.ts | 72 +++++--- 6 files changed, 304 insertions(+), 105 deletions(-) create mode 100644 .changeset/8831-calendar-flat-teaching-face.md diff --git a/.changeset/8466-calendar-color-allday-fields.md b/.changeset/8466-calendar-color-allday-fields.md index fa689ee0dc..accb45bb26 100644 --- a/.changeset/8466-calendar-color-allday-fields.md +++ b/.changeset/8466-calendar-color-allday-fields.md @@ -64,3 +64,5 @@ reason. The `BaseSchema` index-signature ceiling measured by objectstack-ai/objectui#7927 is unchanged — a MISSPELLED key is still admitted on both faces, and the accompanying pin asserts that rather than claiming otherwise. + +⚠️ **Dated note, 2026-10-01 — the README no longer teaches the flat spelling on `object-calendar` (objectui#8831).** The first two paragraphs above say the package README teaches all five flat keys. That was true when this entry was written, and the same release changes it. `@objectstack/spec` refuses the five flat on an `object-calendar` node and prescribes the `calendar` block, so the README now writes them inside `calendar` and calls the flat spelling the runtime handoff `ObjectView` and `ListView` emit. It keeps the five-key sentence for `calendar-view` only. The declarations this entry adds are unchanged: they record that the renderer reads the flat spelling, not that an author should write it. Separately, `@objectstack/spec` 17.5.0 (objectui#11073) declares `allDayField` on `CalendarConfigSchema`, so `calendar.allDayField` now has the spec twin that this entry says it lacks. diff --git a/.changeset/8831-calendar-flat-teaching-face.md b/.changeset/8831-calendar-flat-teaching-face.md new file mode 100644 index 0000000000..4d0a7132bb --- /dev/null +++ b/.changeset/8831-calendar-flat-teaching-face.md @@ -0,0 +1,13 @@ +--- +'@object-ui/types': patch +'@object-ui/plugin-calendar': patch +--- + +`object-calendar` is taught with its `calendar` block, not with flat field-name keys (objectstack-ai/objectui#8831). + +`@objectstack/spec` refuses `startDateField`, `endDateField`, `titleField`, `colorField` and `allDayField` written flat on an `object-calendar` node, and its diagnostic prescribes `calendar: { startDateField, endDateField, titleField, colorField, allDayField }`. Since 17.5.0 the spec's `CalendarConfigSchema` declares all five, `allDayField` included. objectui's published faces still taught the flat spelling as something to write, so an author who followed them was refused at publish. + +- `@object-ui/plugin-calendar`: both `object-calendar` examples in the README write the five keys inside `calendar`, and the README says what the flat spelling is: the runtime handoff `ObjectView` and `ListView` emit, which `getCalendarConfig` reads only when the node has no `calendar` block. The five-key sentence for `calendar-view` stays and now says it is about that element: `CalendarViewSchema` has no `calendar` block, so the flat keys are its only spelling. +- `@object-ui/types`: the `.describe()` text and the TypeScript docblocks of `ObjectCalendarSchema`'s five flat members call them the read-only handoff and point at `calendar.KEY`. The `calendar` member's text calls the block the authored spelling. The `allDayField` member of the `calendar` block no longer calls the key objectui-local, because 17.5.0 declares it. + +Descriptions and documentation only. No accept set moves: the flat members stay declared on both faces, with the same types, because the renderer still reads them. diff --git a/packages/plugin-calendar/README.md b/packages/plugin-calendar/README.md index 03fb4e7ad8..7e8309ad64 100644 --- a/packages/plugin-calendar/README.md +++ b/packages/plugin-calendar/README.md @@ -259,46 +259,59 @@ const schema = { ``` The records above already use the default field names (`title`, `start`, `end`, -`color`), so no field-name keys are needed; point `titleField` / -`startDateField` / `endDateField` / `allDayField` / `colorField` at your own -fields when they differ. +`color`), so no field-name keys are needed; on a `calendar-view` node, point +`titleField` / `startDateField` / `endDateField` / `allDayField` / `colorField` +at your own fields when they differ. They sit flat on this node because +`CalendarViewSchema` has no `calendar` block: here the flat keys are the only +spelling. An `object-calendar` takes the same five inside its `calendar` block +instead (next section). ### With ObjectQL Integration -Every key below is one `ObjectCalendarSchema` declares. Spelling the start-date -key anything else is not a partial failure: `getCalendarConfig` gates the whole -configuration on it, so a calendar whose title and end keys are spelled correctly -still renders the "Calendar configuration required" refusal screen and never -reads them. - -`startDateField` is the only key that gate asks for — `titleField` is optional, -and an event with no explicit title resolves one through the ADR-0079 record -display-name chain. The refusal screen says so, and it also names where the key -belongs: the view's `calendar` block. That matters on an **interface page**, -whose `interfaceConfig` has no calendar slot of its own — the only lever there -is `sourceView`, pointed at a view that declares the block (objectui#8170). +Every key below is one `ObjectCalendarSchema` declares. The five field-name keys +go inside the `calendar` block: `getCalendarConfig` reads that block first and +returns it whole. A misspelt start-date key inside it is therefore not a partial +failure either: no record has a date to be placed by, so every one is counted +as unscheduled under the calendar instead of drawn. + +`startDateField` is the only one of the five the calendar needs — `titleField` +is optional, and an event with no explicit title resolves one through the +ADR-0079 record display-name chain. A calendar authored with no `calendar` +block renders the "Calendar configuration required" refusal screen, which says +so and also names where the key belongs: the view's `calendar` block. That +matters on an **interface page**, whose `interfaceConfig` has no calendar slot +of its own — the only lever there is `sourceView`, pointed at a view that +declares the block (objectui#8170). + +The same five keys written flat on the node are **not** a second way to author +this. That flat spelling is the runtime handoff — `ObjectView` and `ListView` +emit it on the node they build, and `getCalendarConfig` falls back to it only +when the node has no `calendar` block — so `ObjectCalendarSchema` declares it +because the renderer reads it. `@objectstack/spec` refuses it at authoring with +`unrecognized_keys` and names the `calendar` block in the same diagnostic (one +key per concept, its Prime Directive #12). ```typescript import type { ObjectCalendarSchema } from '@object-ui/types'; // What this annotation buys, and what it does not - measured, objectui#7925. // It type-checks the VALUES of the declared keys: `defaultView: 'agenda'` and -// `titleField: 42` are both compile errors, and `check:doc-snippets` re-runs -// that check on every commit. Since objectui#8466 that cover reaches all five -// flat field-name keys - `allDayField` and `colorField` were reachable only -// through `BaseSchema`'s index signature until then, so this block is also -// what proves they are declared. It does NOT check key NAMES - this interface -// extends `BaseSchema`, whose `[key: string]: any` admits any spelling, so a +// `calendar: { titleField: 42 }` are both compile errors, and +// `check:doc-snippets` re-runs that check on every commit. It does NOT check +// key NAMES - this interface extends `BaseSchema`, whose `[key: string]: any` +// admits any spelling, and the `calendar` block is open the same way, so a // misspelt key still compiles clean. Read the block as type-checked values, // never as a guarded key set. const schema: ObjectCalendarSchema = { type: 'object-calendar', objectName: 'events', - titleField: 'name', - startDateField: 'startDate', - endDateField: 'endDate', - allDayField: 'isAllDay', - colorField: 'statusColor', + calendar: { + titleField: 'name', + startDateField: 'startDate', + endDateField: 'endDate', + allDayField: 'isAllDay', + colorField: 'statusColor' + }, defaultView: 'month', navigation: { mode: 'modal' } // what an event click opens - see below }; @@ -348,8 +361,8 @@ Authored JSON reacts to clicks through the node's action channel instead When using with ObjectStack, the calendar can automatically fetch and display events. The adapter is **not** a schema key: `ObjectCalendarRenderer` reads it from the renderer context that `SchemaRendererProvider` supplies. The schema -names the object and its fields with the same flat keys as above - there is no -`fields` container. +names the object with `objectName` and its fields inside the same `calendar` +block as above - there is no `fields` container. ```typescript import { createObjectStackAdapter } from '@object-ui/data-objectstack'; @@ -365,9 +378,11 @@ const dataSource = createObjectStackAdapter({ const schema: ObjectCalendarSchema = { type: 'object-calendar', objectName: 'calendar_events', - titleField: 'title', - startDateField: 'start_time', - endDateField: 'end_time', + calendar: { + titleField: 'title', + startDateField: 'start_time', + endDateField: 'end_time' + }, defaultView: 'month' }; ``` diff --git a/packages/types/src/__tests__/calendar-flat-color-allday-8466.test.ts b/packages/types/src/__tests__/calendar-flat-color-allday-8466.test.ts index b635ecfa69..01722bae1a 100644 --- a/packages/types/src/__tests__/calendar-flat-color-allday-8466.test.ts +++ b/packages/types/src/__tests__/calendar-flat-color-allday-8466.test.ts @@ -19,6 +19,16 @@ * admitted, never examined by either published face. A misspelling therefore * left the calendar silently colourless while every published gate passed. * + * ⚠️ Dated note, 2026-10-01 — objectui#8831. The README sentence quoted above + * is no longer how `object-calendar` is taught. `ComponentPropsMap['object-calendar']` + * refuses all five flat keys and its diagnostic prescribes the `calendar` + * block, so the README now authors the five INSIDE that block and names the + * flat spelling as the runtime handoff `ObjectView`/`ListView` emit (it keeps + * the five-key sentence for `calendar-view` only, where flat is the one + * spelling). The declarations this file pins did NOT move: they record that the + * renderer READS the flat spelling, not that an author should write it. The + * README row below pins the new teaching. + * * ## Why BOTH keys, and the measurement that decided the second one * * The two keys look asymmetric and are not. `colorField` IS a spec key — but @@ -85,8 +95,9 @@ * improvement it existed to enable. * * The rule they now follow: read the FACT off disk — this key is read off the - * node; this key is in that memo's dependency list; these five are taught in one - * block — never the shape the fact happens to be written in. Casts, whitespace, + * node; this key is in that memo's dependency list; these five are authored + * inside the `calendar` block and never flat (objectui#8831) — never the shape + * the fact happens to be written in. Casts, whitespace, * dep ordering and fill width are all formatting, and each helper below is built * so none of them can move a verdict. Each carries a control known to fire in * the same region, because a re-anchor that can no longer go red has not fixed @@ -98,11 +109,16 @@ import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import { CalendarConfigSchema, ComponentPropsMap } from '@objectstack/spec/ui'; +// @ts-expect-error — plain-JS shared helper, intentionally untyped (`allowJs: false`) +import { maskComments } from '../../../../scripts/js-comment-mask.mjs'; import { ObjectCalendarSchema, safeValidateSchema } from '../zod/index.zod'; import type { ObjectCalendarSchema as TsObjectCalendarSchema } from '../objectql'; import type { CalendarViewSchema as TsCalendarViewSchema } from '../complex'; +/** 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, '..', '..', '..', '..'); @@ -110,7 +126,10 @@ const CALENDAR_READER = 'packages/plugin-calendar/src/ObjectCalendar.tsx'; const CALENDAR_REGISTRATION = 'packages/plugin-calendar/src/index.tsx'; const CALENDAR_README = 'packages/plugin-calendar/README.md'; -/** The five flat field-name keys `getCalendarConfig` reads and the README teaches. */ +/** + * The five field-name keys `getCalendarConfig` reads off the node FLAT (the + * runtime handoff), and that the README authors inside the `calendar` block. + */ const FLAT_KEYS = ['titleField', 'startDateField', 'endDateField', 'allDayField', 'colorField'] as const; /** The three that were already declared — the precedent the two new ones join. */ const ALREADY_DECLARED = ['titleField', 'startDateField', 'endDateField'] as const; @@ -175,7 +194,7 @@ const siblingPins: [ // sibling does not declare either, so the five above are readings. export type _SiblingControlFallsThrough = Expect>; -// The TS face ACCEPTS the documented shape… +// The TS face ACCEPTS the flat handoff shape… const calendarLiteral: TsObjectCalendarSchema = { ...CALENDAR_NODE, colorField: 'status_colour', @@ -257,17 +276,83 @@ function configMemoDeps(source: string): Set { return schemaReads(source.slice(from, i - 1)); } +const OPENERS = '{[('; +const CLOSERS = '}])'; + +/** The index of the bracket that closes the one opened at `open`. Throws rather than guessing. */ +function matchingClose(code: string, open: number): number { + let depth = 0; + for (let i = open; i < code.length; i += 1) { + if (OPENERS.includes(code[i])) depth += 1; + else if (CLOSERS.includes(code[i])) { + depth -= 1; + if (depth === 0) return i; + } + } + throw new Error(`unterminated literal from offset ${open}`); +} + /** - * The markdown blocks that teach every one of `keys`, each block's internal - * whitespace collapsed first. A blank line between blocks is STRUCTURE; where - * the lines break inside one is formatting — which is why pinning the literal - * wrap `'at your own\nfields when they differ.'` reddened on a re-wrap. + * Every node a markdown document AUTHORS with `type: ''` in a TypeScript + * fence, as the text of the object literal that carries that `type` key + * (objectui#8831). Brace-matched, never line- or indent-based, so a re-wrap or + * a re-indent cannot move a verdict; comments are masked first, so a key named + * in a comment is not read as one written. */ -function blocksTeachingAll(markdown: string, keys: readonly string[]): string[] { - return markdown - .split(/\n\s*\n/) - .map((block) => block.replace(/\s+/g, ' ').trim()) - .filter((block) => keys.every((key) => block.includes(`\`${key}\``))); +function authoredNodes(markdown: string, type: string): string[] { + const nodes: string[] = []; + const typeKey = new RegExp(`\\btype\\s*:\\s*['"]${type}['"]`, 'g'); + for (const fence of markdown.matchAll(/^```(?:ts|tsx|typescript)\n([\s\S]*?)^```/gm)) { + const code = mask(fence[1]); + for (const m of code.matchAll(typeKey)) { + // Walk BACK to the bracket that opens the literal holding this key. + let depth = 0; + let open = -1; + for (let i = (m.index ?? 0) - 1; i >= 0 && open < 0; i -= 1) { + if (CLOSERS.includes(code[i])) depth += 1; + else if (OPENERS.includes(code[i])) { + if (depth === 0) open = i; + else depth -= 1; + } + } + if (open < 0 || code[open] !== '{') { + throw new Error(`a \`type: '${type}'\` key outside any object literal`); + } + nodes.push(code.slice(open, matchingClose(code, open) + 1)); + } + } + return nodes; +} + +/** An object literal's text with every NESTED bracket region blanked, so only its own top level is left. */ +function topLevelText(literal: string): string { + // Indexed by UTF-16 unit, not by code point, so offsets into the result are + // offsets into `literal` — `memberLiteral` below depends on that. + let depth = 0; + let out = ''; + for (let i = 0; i < literal.length; i += 1) { + const c = literal[i]; + if (OPENERS.includes(c)) depth += 1; + out += depth === 1 ? c : ' '; + if (CLOSERS.includes(c)) depth -= 1; + } + return out; +} + +/** The keys written at the top level of an object literal — a nested block's keys are not among them. */ +function topLevelKeys(literal: string): string[] { + return [...topLevelText(literal).matchAll(/[{,]\s*([A-Za-z_$][\w$]*)\s*:/g)].map((m) => m[1]); +} + +/** The object literal written as the value of top-level `key`, or `undefined` when there is none. */ +function memberLiteral(literal: string, key: string): string | undefined { + const m = new RegExp(`[{,]\\s*${key}\\s*:`).exec(topLevelText(literal)); + if (!m) return undefined; + // Skip whitespace in the ORIGINAL text: in the top-level text the nested + // opener is itself blanked to a space, so it cannot be looked for there. + let at = m.index + m[0].length; + while (/\s/.test(literal[at] ?? '')) at += 1; + return literal[at] === '{' ? literal.slice(at, matchingClose(literal, at) + 1) : undefined; } function shapeKeys(schema: unknown): string[] { @@ -359,23 +444,39 @@ describe('objectui#8466 — the renderer reads these keys, which is what the dec expect(reads.has(CONTROL_KEY)).toBe(false); }); - it('the README still teaches all five together, which is what makes them authorable', () => { - // The card's second half: the published prose. If this teaching is ever - // rewritten, the declaration set it justifies has to be revisited. + it('the README authors the five INSIDE the `calendar` block on `object-calendar`, never flat (objectui#8831)', () => { + // This row used to pin the opposite — that the README taught the five flat, + // "which is what makes them authorable". objectui#8831 ruled that spelling + // the runtime handoff: `ComponentPropsMap['object-calendar']` refuses it and + // its diagnostic prescribes the `calendar` block. So the README teaches the + // block, and the declarations this file pins stay because the renderer + // READS the flat spelling, not because an author should write it. // - // NOT the sentence's LINE WRAP: this used to pin the literal - // `'at your own\nfields when they differ.'`, so re-wrapping a prose - // paragraph in another package reddened a types test with no behaviour - // moving (objectui#8832). One markdown block is structure; where the lines - // break inside it is formatting. + // Read off the authored nodes by brace matching, never by line or indent, + // so a re-wrap or re-indent cannot move the verdict (objectui#8832). const readme = readRepo(CALENDAR_README); - for (const key of FLAT_KEYS) expect(readme).toContain(`\`${key}\``); - const teaching = blocksTeachingAll(readme, FLAT_KEYS); - expect(teaching.length, 'the five flat keys are no longer taught in one block').toBeGreaterThan(0); - // Control, fired in the same region: the same search over the same README - // returns nothing once a key the README does not teach joins the set, so the - // reading above is a reading and not "every block matches". - expect(blocksTeachingAll(readme, [...FLAT_KEYS, CONTROL_KEY])).toHaveLength(0); + const nodes = authoredNodes(readme, 'object-calendar'); + expect(nodes.length, 'the README authors no `object-calendar` node, so the rows below read nothing').toBeGreaterThan(0); + for (const node of nodes) { + const top = topLevelKeys(node); + // Firing control at the SAME depth: the slice is the whole node. + expect(top).toContain(READ_CONTROL_KEY); + for (const key of FLAT_KEYS) { + expect(top, `a README \`object-calendar\` node writes \`${key}\` flat`).not.toContain(key); + } + } + // The teaching moved; it did not vanish. One node writes all five in its + // block — `allDayField` included, which the spec declares there since 17.5.0. + const blocks = nodes.map((node) => memberLiteral(node, 'calendar')).filter((b): b is string => b !== undefined); + expect( + blocks.some((block) => FLAT_KEYS.every((key) => topLevelKeys(block).includes(key))), + 'no README `calendar` block carries all five field-name keys', + ).toBe(true); + // Control, same instrument, opposite verdict: a `calendar-view` node has no + // `calendar` block, so the README writes the keys flat there, and the + // extractor sees a flat key when one is written. + const views = authoredNodes(readme, 'calendar-view'); + expect(views.some((view) => topLevelKeys(view).includes('titleField'))).toBe(true); }); }); @@ -503,7 +604,7 @@ describe('objectui#8466 — the mirror declares what the interface declares', () expect(keys).not.toContain(MISSPELLING); }); - it('accepts the documented shape, and the values SURVIVE the parse', () => { + it('accepts the flat handoff shape `ObjectView`/`ListView` emit, and the values SURVIVE the parse', () => { const node = { ...CALENDAR_NODE, colorField: 'status_colour', allDayField: 'is_all_day' }; const r = ObjectCalendarSchema.safeParse(node); expect(r.success, JSON.stringify(r.error?.issues)).toBe(true); @@ -555,7 +656,7 @@ describe('objectui#8466 — the mirror declares what the interface declares', () /* ── Keep the type-level consts referenced (they are the pins) ─────────────── */ -describe('objectui#8466 — the TS face accepts the documented node', () => { +describe('objectui#8466 — the TS face accepts the flat handoff node', () => { it('the accepted literal carries the values it was authored with', () => { expect(calendarLiteral.colorField).toBe('status_colour'); expect(calendarLiteral.allDayField).toBe('is_all_day'); diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index e4a65b1f2e..900e3bff12 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -3892,11 +3892,19 @@ 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. * + * ⭐ This is where the five field-name keys are AUTHORED (objectui#8831): + * `calendar: { startDateField, endDateField, titleField, colorField, allDayField }`. + * `ComponentPropsMap['object-calendar']` refuses the same five written flat on + * the node, and its diagnostic prescribes exactly this block (one key per + * concept, the spec's Prime Directive #12). The flat members below are the + * runtime handoff, declared because the renderer reads them, not a second + * spelling to write. + * * `@objectstack/spec` declares the KEY — * `ComponentPropsMap['object-calendar'].calendar` — and this package's * 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 + * does NOT declare its SHAPE: measured on 17.4.0, and again on 17.5.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, @@ -3908,9 +3916,9 @@ export interface ObjectCalendarSchema extends BaseSchema { * 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. + * `ObjectCalendar`'s events pass destructures out of the resolved config. + * Through `@objectstack/spec` 17.4.0 that was the spec's four plus objectui's + * own `allDayField`; since 17.5.0 `CalendarConfigSchema` declares all five. * * ⛔ `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 @@ -3919,9 +3927,25 @@ export interface ObjectCalendarSchema extends BaseSchema { * carrying it still parses — it is simply not advertised. */ calendar?: ObjectCalendarBlockConfig; - /** Field for event start */ + /** + * Field for event start — the FLAT spelling, READ BUT NOT AUTHORED + * (objectui#8831). Write `calendar.startDateField` instead. + * + * This is the runtime handoff: `ObjectView` and `ListView` emit the five + * field-name keys flat on the `object-calendar` node they build, and + * `getCalendarConfig` reads them only when the node carries no + * {@link ObjectCalendarSchema.calendar} block. `@objectstack/spec` refuses + * the same five at this element with `unrecognized_keys`, and its diagnostic + * names the {@link ObjectCalendarSchema.calendar} block as the place to write + * them (one key per concept, its Prime Directive #12). The five stay declared + * here because the renderer reads them; a declaration records that read, it + * is not a second authoring spelling. + */ startDateField?: string; - /** Field for event end */ + /** + * Field for event end — the FLAT spelling, read but not authored. Write + * `calendar.endDateField`; see {@link ObjectCalendarSchema.startDateField}. + */ endDateField?: string; /** * ⛔ RETIRED (objectui#8355, director ruling of 2026-09-16) — `dateField` was @@ -3950,11 +3974,19 @@ export interface ObjectCalendarSchema extends BaseSchema { * calendar drew, and only the end binding went missing. */ endField?: never; - /** Field for event title */ + /** + * Field for event title — the FLAT spelling, read but not authored. Write + * `calendar.titleField`; see {@link ObjectCalendarSchema.startDateField}. + */ titleField?: string; /** - * Record field carrying the event's colour — any CSS colour or a semantic - * palette name, typically a server-computed status colour. Resolved PER + * Record field carrying the event's colour — the FLAT spelling, read but not + * authored: write `calendar.colorField`, and see + * {@link ObjectCalendarSchema.startDateField} for why the flat member stays + * declared. + * + * The value is any CSS colour or a semantic palette name, typically a + * server-computed status colour. Resolved PER * RECORD by `plugin-calendar/src/ObjectCalendar.tsx`, which falls back to the * record's own `color` value and then to the platform default, so an authored * value that never arrives is invisible rather than loud. @@ -3972,23 +4004,25 @@ export interface ObjectCalendarSchema extends BaseSchema { */ colorField?: SpecCalendarConfig['colorField']; /** - * Record field carrying the all-day flag. LOAD-BEARING since objectui#8026: + * Record field carrying the all-day flag — the FLAT spelling, read but not + * authored: write `calendar.allDayField`, and see + * {@link ObjectCalendarSchema.startDateField} for why the flat member stays + * declared. + * + * LOAD-BEARING since objectui#8026: * the events pass in `plugin-calendar/src/ObjectCalendar.tsx` reads it and a - * change to the authored key genuinely changes what is drawn. It is also in + * change to the key genuinely changes what is drawn. It is also in * that component's `getCalendarConfig` memo dependency list, which is what * makes the change reach the screen. * - * objectui-LOCAL, and the one member here with no {@link CalendarConfig} twin - * to derive from: `@objectstack/spec`'s `CalendarConfigSchema` is a - * `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 - * shipped capability" (`zod/objectql.zod.ts`). + * Through `@objectstack/spec` 17.4.0 this key was objectui-LOCAL in both + * positions: `CalendarConfigSchema` was a `strictObject` of exactly + * `startDateField`, `endDateField`, `titleField` and `colorField`, and + * answered `allDayField` and a nonsense key with the identical + * `unrecognized_keys` diagnostic (objectui#8651). Since 17.5.0 that schema + * declares `allDayField` too, so `calendar.allDayField` is a spec key like its + * four neighbours (objectui#11073). This flat member is still typed `string` + * rather than derived from {@link CalendarConfig}; the two types are equal. * * ⛔ Declaring it widens NO accept set, which is why Commandment #0.1 is not * engaged. Measured on spec 17.3.0: `ComponentPropsMap['object-calendar']` @@ -3996,8 +4030,8 @@ export interface ObjectCalendarSchema extends BaseSchema { * {@link ObjectCalendarSchema.titleField}, * {@link ObjectCalendarSchema.startDateField} and * {@link ObjectCalendarSchema.endDateField} above, which have shipped - * DECLARED for releases. The flat face is objectui's own lane, taken whole; - * this key is its fifth member, not a new dialect. And under `BaseSchema`'s + * DECLARED for releases. This key is the fifth member of that flat handoff, + * not a new dialect. And under `BaseSchema`'s * index signature the value was already `any`, so declaring only NARROWS. * * ⭐ Nor is it a new precedent: {@link CalendarViewSchema} — a sibling diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index fa554ca4d6..882064e05b 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -1535,10 +1535,15 @@ const CalendarConfig = stripImportedDefaults(SpecCalendarConfigSchema).partial() * declaration rather than a footnote to it. */ 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 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"), + // Through `@objectstack/spec` 17.4.0 this member was objectui-local (see + // objectui#8466 for that measurement). 17.5.0 declares `allDayField` on + // `CalendarConfigSchema` itself as `z.string().optional()`, so this extension + // now restates the spec's member with the SAME accept set: removing it would + // change nothing a parse decides (objectui#8831 reports that and leaves the + // removal to its own change). The renderer honours the key in BOTH + // positions: this container, which is where it is authored, and the flat + // member of the node, which is the runtime handoff. + allDayField: z.string().optional().describe('Field carrying the all-day flag. Declared by the spec\'s CalendarConfigSchema since 17.5.0, with the same accept set as this member. LOAD-BEARING since objectui#8026'), // ⭐ objectui#8355 — the same two spellings the view-level block above refuses, // and deliberately NOT the same string: `getCalendarConfig` reads this // container FIRST and returns it WHOLE, so a retired spelling here never @@ -2397,6 +2402,23 @@ const OBJECT_CALENDAR_NEITHER_CHANNEL = neitherContentChannelGuidance( 'the records of `objectName` as events from `startDateField` to `endDateField`', ); +/** + * objectui#8831 — ONE description for the five FLAT field-name members of + * `ObjectCalendarSchema`, so the five cannot teach five different things. + * + * The flat spelling is read, not authored. `@objectstack/spec` refuses all five + * at this element and its diagnostic names the canonical form, + * `calendar: { startDateField, endDateField, titleField, colorField, allDayField }` + * (one key per concept, its Prime Directive #12). The members stay declared + * because the renderer reads them: `ObjectView` and `ListView` emit this + * spelling on the node they build, and `getCalendarConfig` falls back to it + * when the node has no `calendar` block. ⛔ Declared is not a licence to author + * it; the description says where the key is written instead. + */ +function objectCalendarFlatField(what: string, key: string): string { + return `${what} — FLAT spelling, read but not authored: the runtime handoff ObjectView/ListView emit, read by getCalendarConfig only when the node has no calendar block. Author calendar.${key} instead; the spec refuses the flat key on object-calendar (Prime Directive #12)`; +} + /** * ObjectCalendar Schema * @@ -2458,9 +2480,16 @@ export const ObjectCalendarSchema = BaseSchema.extend({ // 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'), + // + // objectui#8831 — this container is the AUTHORED spelling of the five + // field-name keys. `ComponentPropsMap['object-calendar']` refuses them FLAT + // and its own diagnostic prescribes + // `calendar: { startDateField, endDateField, titleField, colorField, allDayField }`, + // so this description names the container as the place to write them, and + // the five flat members below describe themselves as the runtime handoff. + calendar: ObjectCalendarBlockConfigSchema.optional().describe('Calendar configuration container, and the AUTHORED spelling of the five field-name keys: startDateField, endDateField, titleField, colorField, allDayField. Read FIRST by getCalendarConfig, ahead of the flat members, which are the runtime handoff and not a second authorable spelling'), + startDateField: z.string().optional().describe(objectCalendarFlatField('Start date field', 'startDateField')), + endDateField: z.string().optional().describe(objectCalendarFlatField('End date field', 'endDateField')), // ⭐ objectui#8355 — the FLAT spelling the retired ladder actually read, and // the one position where an unrefused alias is worst: `BaseSchema` ends // `.passthrough()`, so the key was KEPT, carried into the renderer, and — with @@ -2469,14 +2498,16 @@ export const ObjectCalendarSchema = BaseSchema.extend({ // `?: never` twin and `tsc` refuses the key at the authoring site too. dateField: CalendarNodeDateAliasRefusals.dateField, endField: CalendarNodeDateAliasRefusals.endField, - titleField: z.string().optional().describe('Title field'), + titleField: z.string().optional().describe(objectCalendarFlatField('Title field', 'titleField')), // objectui#8466 — the last two members of the FLAT field-name face, which - // `ObjectCalendar.tsx`'s `getCalendarConfig` reads bare off the node and - // which `plugin-calendar/README.md` teaches as authorable. Neither published - // face of this package named them: they rode `BaseSchema`'s `[key: string]: - // any` on the TS side and its `.passthrough()` here — admitted, never - // examined, so a misspelling left the calendar silently colourless while - // every published gate passed. + // `ObjectCalendar.tsx`'s `getCalendarConfig` reads bare off the node. When + // that card landed, `plugin-calendar/README.md` taught them as authorable; + // since objectui#8831 it teaches the `calendar` container instead, and the + // flat members stay declared because the renderer still reads them. Neither + // published face of this package named them: they rode `BaseSchema`'s + // `[key: string]: any` on the TS side and its `.passthrough()` here — + // admitted, never examined, so a misspelling left the calendar silently + // colourless while every published gate passed. // // 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 @@ -2486,11 +2517,14 @@ export const ObjectCalendarSchema = BaseSchema.extend({ // that asymmetry is deliberate: `ComponentPropsMap['object-calendar']` // refuses all five flat keys with `unrecognized_keys`, so declaring them // THERE would redden the FORWARD direction of - // `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`. The flat - // 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 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"), + // `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`. + // `titleField`/`startDateField`/`endDateField` have shipped declared here, and + // absent from `inputs`, for releases. What the five declarations record is a + // READ, not an authoring lane (objectui#8831): `ObjectView`/`ListView` emit + // this spelling on the node they build, and `getCalendarConfig` falls back to + // it only when the node carries no `calendar` block. + colorField: z.string().optional().describe(objectCalendarFlatField('Field carrying the per-record event colour — a CSS colour or a semantic palette name', 'colorField')), + allDayField: z.string().optional().describe(objectCalendarFlatField('Field carrying the all-day flag, LOAD-BEARING since objectui#8026', 'allDayField')), defaultView: z.enum(['month', 'week', 'day']).optional().describe("Default view — 'month' | 'week' | 'day', the renderer's rendered set ('agenda' was retired)"), // objectui#8174 — the two query keys `ObjectCalendar.tsx` lowers onto its own // `dataSource.find` (`$filter: schema.filter`, From a86a6afdbdee780ec7c8c4b6be8e46ec8ee710fa Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 02:00:08 +0000 Subject: [PATCH 2/2] docs(plugin-calendar): the calendar container is declared on both faces, and allDayField is a spec key since 17.5.0 (objectui#8831) The plugin-calendar docs page said neither published face of ObjectCalendarSchema declares the calendar container; both have since objectui#8651. The paragraph now states what the two faces and the spec check inside the block, measured on the installed 17.5.0. Three pending changesets (8026, 8830, olive-buckets-scream) still described allDayField as objectui-local or refused by the spec, which objectui#11073's bump made false; olive-buckets-scream also said the dateField / endField rungs are kept, which objectui#8355 made false. Each gets a dated, append-only correction with its frontmatter unchanged. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT Co-authored-by: Claude --- .../8026-objectcalendar-alldayfield-honoured.md | 2 ++ .changeset/8830-calendar-doc-key-set.md | 2 ++ .changeset/olive-buckets-scream.md | 5 +++++ content/docs/plugins/plugin-calendar.mdx | 16 +++++++++++----- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.changeset/8026-objectcalendar-alldayfield-honoured.md b/.changeset/8026-objectcalendar-alldayfield-honoured.md index 9cf1ffce95..c716072a35 100644 --- a/.changeset/8026-objectcalendar-alldayfield-honoured.md +++ b/.changeset/8026-objectcalendar-alldayfield-honoured.md @@ -39,3 +39,5 @@ the `CalendarConfig` mirror in `@object-ui/types`, whose comment names it: "the renderers grow config knobs ahead of the protocol (calendar's `allDayField`, for one), and stripping them here would silently disable a shipped capability." Nothing in this change widens any accept set. + +**Correction, 2026-10-01 (objectui#8831).** The "Not spec surface" paragraph above is false against `@objectstack/spec` 17.5.0, which this same release installs. That release's `CalendarConfigSchema` declares five keys, `allDayField` among them, and accepts it. It still refuses `defaultView`. The key no longer rides the `.passthrough()` of the `CalendarConfig` mirror in `@object-ui/types`: that mirror derives from the spec schema, so `allDayField` is now a declared member of it. objectui#11073's bump to 17.5.0 made the paragraph false, not this entry and not objectui#8831. The paragraphs about the renderer still hold: `object-calendar` takes no default field name for `allDayField`, and a declared one is the whole answer. diff --git a/.changeset/8830-calendar-doc-key-set.md b/.changeset/8830-calendar-doc-key-set.md index 5f4cbb2746..04aad1016c 100644 --- a/.changeset/8830-calendar-doc-key-set.md +++ b/.changeset/8830-calendar-doc-key-set.md @@ -10,3 +10,5 @@ load-bearing since objectui#8026 — and neither objectui authoring face rejects fifth key in the `calendar` block. The page now separates the spec face from the renderer face, and `packages/types/src/__tests__/calendar-doc-key-set-8830.test.ts` derives both sets on every run so the enumeration cannot drift again. + +**Correction, 2026-10-01 (objectui#8831).** "the four plus objectui's own `allDayField`" is false against `@objectstack/spec` 17.5.0, which this same release installs. That release's `CalendarConfigSchema` declares all five, so `allDayField` is a spec key like the other four. objectui#11073's bump made the phrase false, not this entry and not objectui#8831. The rest of this entry holds: the renderer still reads five keys, neither objectui face rejects an extra key in the `calendar` block, and `calendar-doc-key-set-8830.test.ts` still derives both sets on every run. diff --git a/.changeset/olive-buckets-scream.md b/.changeset/olive-buckets-scream.md index 2114d5add7..36182a866d 100644 --- a/.changeset/olive-buckets-scream.md +++ b/.changeset/olive-buckets-scream.md @@ -11,3 +11,8 @@ fix(plugin-calendar): type `ObjectCalendar` at the published `object-calendar` s - `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 `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. + +**Correction, 2026-10-01 (objectui#8831).** Two statements in this entry are false against the tree this release ships, for two different reasons. + +- "the four `CalendarConfigSchema` names plus objectui's `allDayField`": `@objectstack/spec` 17.5.0, which this same release installs, declares all five on `CalendarConfigSchema`, so the container's members are the spec's five. objectui#11073's bump made this false, not this entry and not objectui#8831. What the same bullet says about the spec at this position still holds on 17.5.0: `ComponentPropsMap['object-calendar'].calendar` declares the key but not its shape, and accepts `calendar: 42`. +- "and so do `calendar.dateField`", and the ⚠️ bullet's "The `dateField` / `endField` alias rungs in `getCalendarConfig` are **kept**": objectui#8355 retired both rungs in this same release. `getCalendarConfig` no longer reads either spelling, and the container refuses `calendar.dateField` and `calendar.endField` by name (see objectui#8355's own entry). objectui#8355 made these false, not objectui#11073 and not objectui#8831. An unexamined key and `calendar.defaultView` still parse inside the block, and the value validation this entry adds (`calendar: 42` and `calendar: { startDateField: 42 }` are refused) still holds. diff --git a/content/docs/plugins/plugin-calendar.mdx b/content/docs/plugins/plugin-calendar.mdx index e3351dee43..3f8a3b6ab5 100644 --- a/content/docs/plugins/plugin-calendar.mdx +++ b/content/docs/plugins/plugin-calendar.mdx @@ -307,11 +307,17 @@ before the spec declared it; the spec declares it since `@objectstack/spec` never declares it draws exactly as before, with the all-day flag inferred from the absence of an end date. -Nor is a sixth key rejected on the way in. Neither published face of -`ObjectCalendarSchema` declares the `calendar` container, so whatever you write -inside it reaches the renderer unexamined; on the list-view path the container -*is* declared — as objectui's own mirror of the spec schema, which keeps -`.passthrough()` for exactly this. Annotating the block `CalendarConfig` is what +Nor is a sixth key rejected on the way in. Both published faces of +`ObjectCalendarSchema` declare the `calendar` container (objectui#8651), with +the five keys above as its members. Their values are checked, so +`calendar: { startDateField: 42 }` is refused, and the retired `dateField` / +`endField` are refused by name (objectui#8355). The container keeps +`.passthrough()`, though, so any other key you write inside it still parses and +reaches the renderer unexamined. `@objectstack/spec` does not judge it there +either: on an `object-calendar` node it declares the `calendar` key but not that +key's shape. On the list-view path the container is objectui's own mirror of +`CalendarConfigSchema`, kept `.passthrough()` in the same way, while the spec's +own list-view block is strict. Annotating the block `CalendarConfig` is what narrows it to the spec's five. ## Configuration