From 4f2d1f4a67ad97ef3d84bdc0b493aabdc14be0fc Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 13 Sep 2026 04:25:38 +0000 Subject: [PATCH] fix(types): narrow ObjectCalendarSchema.data to the protocol's array arm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ComponentPropsMap['object-calendar'].data` on `@objectstack/spec` declares `z.array(z.unknown()).optional()` — "Pre-fetched records — skips the internal fetch". Both published faces of `@object-ui/types` declared `ViewData` on the same key: a `{ provider, items }` config object, which that row refuses by kind. After objectui#8348 put the renderer on the protocol's side (decision batch #83, maintainer verbatim 8348 uses the protocol as the authority), this mirror was the lone published face still teaching the config-object spelling, so an author who validated against `@object-ui/types` got a green verdict for metadata the renderer ignores, `os validate` refuses and the save gate rejects. - TS face: `data?: ViewData` -> `data?: SpecObjectCalendarProps['data']`, DERIVED from the protocol's own row so the key cannot drift from it again. - Zod mirror: `ViewDataSchema.optional()` -> `z.array(z.unknown()).optional()`. - Requiredness unchanged on both faces, so the zod-mirror-parity ratchet is unmoved (verified with `tsc -p tsconfig.test.json`, which is the only instrument that sees that ledger's type-level reconciliation). - The `data` equality with `TsObjectGanttSchema['data']` is INVERTED rather than deleted, and both sides are named; the `staticData` and `objectName` equalities were re-checked individually and still hold. - `requireRecordSource`'s docblock says what `data` means per member: the ladder is shared, the arm is not. `object-map` / `object-gantt` keep the `ViewData` arm — neither has a `ComponentPropsMap` row, so the row governing them is this package's own. `@objectstack/spec` is untouched. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt --- .../9239-object-calendar-data-array-arm.md | 25 +++ content/docs/plugins/plugin-calendar.mdx | 23 +- ...object-calendar-record-source-7313.test.ts | 203 ++++++++++++++++-- packages/types/src/objectql.ts | 47 +++- packages/types/src/zod/objectql.zod.ts | 49 ++++- 5 files changed, 308 insertions(+), 39 deletions(-) create mode 100644 .changeset/9239-object-calendar-data-array-arm.md diff --git a/.changeset/9239-object-calendar-data-array-arm.md b/.changeset/9239-object-calendar-data-array-arm.md new file mode 100644 index 0000000000..5c98e4c99d --- /dev/null +++ b/.changeset/9239-object-calendar-data-array-arm.md @@ -0,0 +1,25 @@ +--- +'@object-ui/types': minor +--- + +**BREAKING — `ObjectCalendarSchema.data` narrows from the `ViewData` provider block to the protocol's ARRAY of pre-fetched records, on both published faces.** + +`ComponentPropsMap['object-calendar'].data` on `@objectstack/spec` declares `z.array(z.unknown()).optional()` — *"Pre-fetched records — skips the internal fetch"*. Both published faces of this package declared `ViewData` on the same key instead: a `{ provider, items }` config object, which that row refuses BY KIND. One key, two published shapes that refuse each other. + +After objectui#8348 put the renderer on the protocol's side (maintainer ruling, decision batch #83, 2026-09-08, verbatim 「8348 以协议为准」), this package's mirror was the LONE published face still teaching the config-object spelling. An author who validated metadata against `@object-ui/types` got a green verdict for a document the renderer ignores, `os validate` refuses and the save gate rejects — `declared !== enforced` with the declaration on the wrong side, the shape AGENTS.md #0.1 exists to prevent. objectui#9239 brings the declaration onto the contract. + +**What changes for authors** + +- `ObjectCalendarSchema` (TypeScript): `data?: ViewData` becomes `data?: unknown[]`, DERIVED from the protocol's own row rather than re-spelled, so the key cannot drift from it a second time. A calendar literal carrying `data: { provider: 'value', items: [...] }` is now a compile error at `data`; an array of records compiles. +- `ObjectCalendarSchema` (Zod mirror, reached by `safeValidateSchema` and so by the CLI's `validate` / `check`): `ViewDataSchema.optional()` becomes `z.array(z.unknown()).optional()`. The config object is now refused AT the key; an array is accepted. +- Requiredness is unchanged — optional on both faces, as before — so the `zod-mirror-parity` ratchet is unmoved. + +⛔ **The accept set genuinely shrinks. That is the point**, and it is a narrowing onto a contract `@objectstack/spec` already publishes, not a new dialect: every document this declaration now refuses was already refused by the protocol, by `os validate`, by the save gate and by the renderer. Nothing that renders today stops rendering because of this change — objectui#8348 is where the runtime behaviour moved. + +**What does NOT change** + +- `staticData` and `objectName` are untouched on this block, and so is the three-rung record-source ladder: `requireRecordSource` asks only whether a rung is PRESENT, whatever the value's kind. +- ⛔ `ObjectMapSchema.data` and `ObjectGanttSchema.data` stay `ViewData`. Neither block has a `ComponentPropsMap` row, so the published row that governs them is this package's own — they are not following, and the type-level equality that used to bind the calendar's `data` to the gantt's is now pinned as a DIFFERENCE rather than deleted. +- ⛔ `@objectstack/spec` itself is not touched. + +Refs: objectui#9239 · objectui#8348 (the ruling and the renderer half) · objectui#7313 (which declared this key, in the provider-block arm) · objectui#4631 diff --git a/content/docs/plugins/plugin-calendar.mdx b/content/docs/plugins/plugin-calendar.mdx index fbff304912..0a6e918e37 100644 --- a/content/docs/plugins/plugin-calendar.mdx +++ b/content/docs/plugins/plugin-calendar.mdx @@ -242,10 +242,12 @@ const schema: ObjectCalendarSchema = { type: 'object-calendar', objectName?: string, // ObjectQL object name staticData?: Array, // Static data array - data?: Array, // Pre-fetched records, drawn in place of the + data?: Array, // Pre-fetched records, drawn in place of the // calendar's own query. An ARRAY — this block's // published row refuses the `{ provider, items }` - // config object (objectui#8348) + // config object (objectui#8348). `@object-ui/types` + // declares it as that array too since + // objectui#9239, derived from the protocol's row calendar?: CalendarConfig, // Calendar-specific configuration filter?: ViewFilterRule[], // Query filter, lowered to $filter sort?: SortConfig[], // Query sort, lowered to $orderby @@ -356,10 +358,19 @@ here. `object-calendar`'s published `data` row is an **array of pre-fetched records**, so the config object is refused by kind: `os validate` and the save gate reject it, and as of that card the renderer does too. -That is a behaviour change for stored documents. A calendar written that way no -longer draws those rows; the record-source ladder falls past `data` to -`staticData`, then to `objectName`, so it queries its object instead — or draws -nothing when it names neither. +Since **objectui#9239** the `@object-ui/types` declaration refuses it as well — +both the `ObjectCalendarSchema` interface and the zod mirror `safeValidateSchema` +runs. Until that card this package was the one published face that still +accepted the config object, so a document validated *here* could pass while every +other surface rejected it; the annotated examples on this page are checked +against the narrowed declaration. + +That is a behaviour change for stored documents, and a **breaking narrowing** of +the authoring type. A calendar written that way no longer draws those rows; the +record-source ladder falls past `data` to `staticData`, then to `objectName`, so +it queries its object instead — or draws nothing when it names neither. In +TypeScript the same document is now a compile error at `data` rather than a +silent pass. Read from the database with the **Object Provider** above (`objectName`), pass events you already hold with the **Value Provider** (`staticData`), or hand the 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 fdacc613d9..6843829bd8 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 @@ -45,6 +45,11 @@ import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; +// objectui#9239 — the PROTOCOL's own row, read directly so the pins below +// 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'; + import { ObjectCalendarSchema, ObjectGanttSchema, safeValidateSchema } from '../zod/index.zod'; import { BaseSchema } from '../zod/base.zod'; import type { @@ -80,14 +85,22 @@ export type _CalendarObjectNameIsOptionalKey = Expect< IsOptionalKey< TsObjectCalendarSchema, 'objectName' > >; /** - * `data` is DECLARED, optional, and `ViewData` — not `any`. Deleting the member - * does NOT fall through to the index signature: it lands on the INHERITED - * `BaseSchema.data?: any` (a declared member wins over an index signature), and - * `Equal< any, ViewData | undefined >` is false -> red. That is the whole - * reason the member is declared here rather than left to the base. + * `data` is DECLARED, optional, and the ARRAY of PRE-FETCHED RECORDS the + * protocol's row declares — not `any`, and ⛔ no longer `ViewData` + * (objectui#9239). Deleting the member does NOT fall through to the index + * signature: it lands on the INHERITED `BaseSchema.data?: any` (a declared + * member wins over an index signature), and `Equal< any, … >` is false -> red. + * That is the whole reason the member is declared here rather than left to the + * base. + * + * The shape is written out rather than re-derived from the spec, deliberately: + * the declaration DERIVES `ComponentPropsMap['object-calendar'].data` + * (`z.array(z.unknown()).optional()` on `@objectstack/spec` 17.4.0), so a + * protocol move would silently re-shape this repository's published type. This + * row is what makes such a move LOUD instead. */ -export type _CalendarDataIsOptionalViewData = - Expect< Equal< TsObjectCalendarSchema['data'], ViewData | undefined > >; +export type _CalendarDataIsOptionalRecordArray = + Expect< Equal< TsObjectCalendarSchema['data'], unknown[] | undefined > >; export type _CalendarDataIsOptionalKey = Expect< IsOptionalKey< TsObjectCalendarSchema, 'data' > >; @@ -97,14 +110,36 @@ export type _CalendarStaticDataIsOptionalAnyArray = export type _CalendarStaticDataIsOptionalKey = Expect< IsOptionalKey< TsObjectCalendarSchema, 'staticData' > >; -/** One concept, one type: the calendar's three keys are the gantt's three keys. */ -export type _CalendarDataMatchesGantt = - Expect< Equal< TsObjectCalendarSchema['data'], TsObjectGanttSchema['data'] > >; +/** + * One concept, one type — for TWO of the three keys now. Each was RE-CHECKED + * individually when `data` left this set (objectui#9239), ⛔ not deleted by + * association with it: `staticData` is `any[] | undefined` on both members and + * `objectName` is `string | undefined` on both, so both rows still hold and + * still bite. + */ export type _CalendarStaticDataMatchesGantt = Expect< Equal< TsObjectCalendarSchema['staticData'], TsObjectGanttSchema['staticData'] > >; export type _CalendarObjectNameMatchesGantt = Expect< Equal< TsObjectCalendarSchema['objectName'], TsObjectGanttSchema['objectName'] > >; +/** + * ⭐ …and `data` is pinned as DIFFERENT, which is objectui#9239's whole subject. + * A bare deletion of the old equality would have left the divergence unwitnessed + * — nothing would notice the two keys silently converging again — so the row is + * INVERTED rather than removed, and BOTH sides are named below so the inversion + * cannot be satisfied by the wrong member moving. + * + * `object-calendar` carries the protocol's ARRAY arm + * (`ComponentPropsMap['object-calendar'].data`); `object-gantt` has no + * `ComponentPropsMap` row at all, so the published row that governs it is this + * package's own `ViewDataSchema.optional()` and it STAYS. ⛔ Do not "fix" this + * pin by moving the gantt. + */ +export type _CalendarDataDiffersFromGantt = + Expect< Equal< Equal< TsObjectCalendarSchema['data'], TsObjectGanttSchema['data'] >, false > >; +export type _GanttDataIsStillOptionalViewData = + Expect< Equal< TsObjectGanttSchema['data'], ViewData | undefined > >; + /** * The document the plugin page teaches under "With Static Data". It did not * compile before this card — `objectName` was a required member, and an index @@ -116,10 +151,14 @@ export const STATIC_DATA_DOCUMENT: TsObjectCalendarSchema = { staticData: [{ id: 1, title: 'Team Meeting', startDate: '2024-01-15T10:00:00' }], }; -/** …and the `data`-authored one, typed against the declared `ViewData`. */ +/** + * …and the `data`-authored one, typed against the declared ARRAY of pre-fetched + * records (objectui#9239). The provider-block spelling this literal used to + * carry is now a compile error here, which is the declaration half of the fix. + */ export const DATA_DOCUMENT: TsObjectCalendarSchema = { type: 'object-calendar', - data: { provider: 'value', items: [{ id: 1, title: 'Team Meeting' }] }, + data: [{ id: 1, title: 'Team Meeting' }], }; /** @@ -134,8 +173,11 @@ export const DATA_DOCUMENT: TsObjectCalendarSchema = { * `tsconfig.test.json` rather than quietly meaning nothing. * * ⚠️ It is NOT the same fix as this file's own subject. `object-calendar` - * joined the `object-map` / `object-gantt` ladder — `data` (a `ViewData` - * provider block) → `staticData` → `objectName`, `requireRecordSource`. The + * joined the `object-map` / `object-gantt` ladder — `data` → `staticData` → + * `objectName`, `requireRecordSource`. (What `data` ADMITS diverged later: + * objectui#9239 put the calendar's rung on the protocol's ARRAY arm while the + * map's and the gantt's stay `ViewData` provider blocks. The LADDER is what is + * shared, not the arm.) The * kanban board walks its own: pre-fetched `data` prop → `bind` → an inline ROW * ARRAY on `data` → `objectName`, with no `staticData` rung, and objectui#7651 * (ruled B, closed `not_planned`) refuses giving it the shared one. Its @@ -156,13 +198,32 @@ export const KANBAN_NO_LONGER_REQUIRES_OBJECT_NAME: TsObjectKanbanSchema = { /* ── Runtime pins ─────────────────────────────────────────────────────────── */ /** - * The four documents the card's verdict table is written over. `data` uses - * the value provider — the config `staticData` is folded into, so the two - * accepted-without-`objectName` rows exercise different keys but one route. + * ⭐ The `data` rung's VALUE, per member — the one thing the two ladders stopped + * sharing (objectui#9239). The LADDER is still shared (`requireRecordSource` + * asks only whether a rung is present, `!== undefined`, whatever its kind); what + * each member's published `data` row ADMITS is not: + * + * - `object-calendar` — `ComponentPropsMap['object-calendar'].data` is + * `z.array(z.unknown()).optional()`, an ARRAY of pre-fetched records. + * - `object-gantt` — no `ComponentPropsMap` row exists, so its published row is + * this package's own `ViewDataSchema.optional()`: a PROVIDER BLOCK. + */ +const DATA_ARM = { + 'object-calendar': [{ id: 1, title: 'Team Meeting' }], + 'object-gantt': { provider: 'value', items: [{ id: 1, title: 'Team Meeting' }] }, +} as const; +type LadderMember = keyof typeof DATA_ARM; + +/** + * The four documents the card's verdict table is written over, on the CALENDAR's + * arm. Before objectui#9239 `dataOnly` was the value-provider config — the one + * `staticData` is folded into — so the two accepted-without-`objectName` rows + * exercised different keys but one route. They still exercise different keys; + * the route is now literally different too, which is the point of that card. */ const DOCUMENTS = { staticOnly: { staticData: [{ id: 1, title: 'Team Meeting', startDate: '2024-01-15T10:00:00' }] }, - dataOnly: { data: { provider: 'value', items: [{ id: 1, title: 'Team Meeting' }] } }, + dataOnly: { data: DATA_ARM['object-calendar'] }, none: {}, objectOnly: { objectName: 'events' }, } as const; @@ -176,6 +237,17 @@ function withType(type: string, name: DocumentName): Record { return { type, ...DOCUMENTS[name] }; } +/** + * The same four documents with the `data` rung on the arm THAT member's own + * published row declares (objectui#9239). Used by the gantt parity block below, + * which measures the LADDER — had it kept feeding one arm to both members, the + * gantt would refuse `dataOnly` by KIND and the comparison would read as a + * ladder divergence that does not exist. + */ +function withArm(type: LadderMember, name: DocumentName): Record { + return name === 'dataOnly' ? { type, data: DATA_ARM[type] } : withType(type, name); +} + /** Report the issues rather than `false`, so a red run says what broke. */ function reasons(schema: unknown): string[] { const r = safeValidateSchema(schema); @@ -229,8 +301,8 @@ describe('objectui#7313 — the four documents, through the member and the publi }); describe('objectui#7313 — parity with `ObjectGanttSchema`, verdict for verdict', () => { - const verdicts = (member: { safeParse: (v: unknown) => { success: boolean } }, type: string) => - DOCUMENT_NAMES.map((name) => member.safeParse(withType(type, name)).success); + const verdicts = (member: { safeParse: (v: unknown) => { success: boolean } }, type: LadderMember) => + DOCUMENT_NAMES.map((name) => member.safeParse(withArm(type, name)).success); it('the two members agree on all four documents, and the vector is not vacuous', () => { const calendar = verdicts(ObjectCalendarSchema, 'object-calendar'); @@ -269,7 +341,16 @@ describe('objectui#7313 — `data` and `staticData` are DECLARED, not passthroug const r = ObjectCalendarSchema.safeParse({ type: 'object-calendar', objectName: 'events', data: 'nope' }); expect(r.success).toBe(false); if (!r.success) expect(r.error.issues.map((i) => i.path[0])).toContain('data'); - expect(ObjectCalendarSchema.safeParse({ type: 'object-calendar', objectName: 'events', data: { provider: 'object', object: 'events' } }).success).toBe(true); + // ⭐ objectui#9239 — the PROVIDER BLOCK is a wrong-typed `data` now too. This + // line asserted `success: true` until that card, and it was the mirror's half + // of the divergence: the protocol refused this document by kind while this + // published face called it valid. `objectName` is supplied, so the refusal + // below can only be the KEY's — the refinement is satisfied either way. + const block = ObjectCalendarSchema.safeParse({ type: 'object-calendar', objectName: 'events', data: { provider: 'object', object: 'events' } }); + expect(block.success).toBe(false); + if (!block.success) expect(block.error.issues.map((i) => i.path[0])).toContain('data'); + // …and the ARRAY of pre-fetched records is what the key admits instead. + expect(ObjectCalendarSchema.safeParse({ type: 'object-calendar', objectName: 'events', data: DATA_ARM['object-calendar'] }).success).toBe(true); }); it('a wrong-typed `staticData` is refused AT the key', () => { @@ -336,3 +417,83 @@ describe('objectui#7313 — the declaration names a live read, in the declared o expect(page.match(/^const \w+ = \{\n\s+type: 'object-calendar'/gm)).toBeNull(); }); }); + +/* ── objectui#9239 — the `data` ARM, and the face that had it wrong ────────── */ + +/** + * objectui#9239 — both published faces of this package declared the `{ provider, + * items }` PROVIDER BLOCK under `ObjectCalendarSchema.data` while + * `ComponentPropsMap['object-calendar'].data` on `@objectstack/spec` declared + * `z.array(z.unknown()).optional()`. One key, two published shapes that refuse + * each other BY KIND — and after objectui#8348 put the renderer on the + * protocol's side, this mirror was the LONE published face still teaching a + * spelling the renderer, `os validate` and the save gate all refuse. + * + * The rows below measure the protocol directly rather than restating it: the + * mirror is asked for a verdict on the same two documents the protocol's own row + * is asked for, so a future protocol move breaks this file instead of quietly + * re-opening the divergence. That is the objectui#4631 class ("three declared + * surfaces that disagree") closed on this key from the runtime side; the type + * side is closed by the declaration DERIVING the protocol's row. + */ +describe('objectui#9239 — `data` is the protocol\'s ARRAY arm, on both faces', () => { + const PROTOCOL_ROW = ComponentPropsMap['object-calendar']; + const BLOCK = DATA_ARM['object-gantt']; + const ARRAY = DATA_ARM['object-calendar']; + + it('the protocol refuses the provider block and accepts the array — measured, not assumed', () => { + const refused = PROTOCOL_ROW.safeParse({ objectName: 'events', data: BLOCK }); + expect(refused.success).toBe(false); + if (!refused.success) { + expect(refused.error.issues.map((i) => i.path[0])).toContain('data'); + expect(refused.error.issues.some((i) => (i as { expected?: string }).expected === 'array')).toBe(true); + } + expect(PROTOCOL_ROW.safeParse({ objectName: 'events', data: ARRAY }).success).toBe(true); + }); + + it('⭐ the mirror now returns the protocol\'s verdict on both documents', () => { + // THE ablation row. Put `ObjectCalendarSchema.data` back on + // `ViewDataSchema.optional()` and the first expectation flips to `true`, + // reddening here — the divergence cannot come back silently. + expect(ObjectCalendarSchema.safeParse({ type: 'object-calendar', objectName: 'events', data: BLOCK }).success).toBe(false); + expect(ObjectCalendarSchema.safeParse({ type: 'object-calendar', objectName: 'events', data: ARRAY }).success).toBe(true); + + // Verdict-for-verdict against the protocol's own row, so neither side can + // drift alone. Non-vacuity is the `.size` below: a pair that agreed by + // accepting everything would "agree" too. + const documents = [BLOCK, ARRAY, 'nope', []]; + const mirror = documents.map((d) => ObjectCalendarSchema.safeParse({ type: 'object-calendar', objectName: 'events', data: d }).success); + const protocol = documents.map((d) => PROTOCOL_ROW.safeParse({ objectName: 'events', data: d }).success); + expect(mirror).toEqual(protocol); + expect(new Set(mirror).size).toBe(2); + expect(mirror).toEqual([false, true, false, true]); + }); + + it('the two members diverge on the ARM only — each refuses the other\'s `data` document', () => { + // `data` is present in both, so `requireRecordSource` is satisfied and the + // only thing that can refuse is the KEY. Path `[]` would be the refinement. + const calendarOnGanttArm = ObjectCalendarSchema.safeParse({ type: 'object-calendar', data: BLOCK }); + expect(calendarOnGanttArm.success).toBe(false); + if (!calendarOnGanttArm.success) expect(calendarOnGanttArm.error.issues.map((i) => i.path[0])).toContain('data'); + + const ganttOnCalendarArm = ObjectGanttSchema.safeParse({ type: 'object-gantt', data: ARRAY }); + expect(ganttOnCalendarArm.success).toBe(false); + if (!ganttOnCalendarArm.success) expect(ganttOnCalendarArm.error.issues.map((i) => i.path[0])).toContain('data'); + + // ⛔ `object-gantt` was NOT moved: its own arm still validates. + expect(ObjectGanttSchema.safeParse({ type: 'object-gantt', data: BLOCK }).success).toBe(true); + }); + + it('the LADDER is untouched: the refinement still counts presence, whatever the arm', () => { + // An `object-calendar` whose `data` is on the WRONG arm is refused at the + // key, NOT sent down to `staticData` — the refinement never fires, because + // `data !== undefined`. Exactly one issue, and it is the key's. + const r = ObjectCalendarSchema.safeParse({ type: 'object-calendar', data: BLOCK }); + expect(r.success).toBe(false); + if (!r.success) { + expect(r.error.issues).toHaveLength(1); + expect(r.error.issues[0].path).toEqual(['data']); + expect(r.error.issues[0].message).not.toBe(REFUSAL_MESSAGE); + } + }); +}); diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index 08cd9286cd..57a6623ce9 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -104,6 +104,12 @@ import type { ChartAggregate, GanttConfig as SpecGanttConfig, CalendarConfig as SpecCalendarConfig, + // objectui#9239 — `ComponentPropsMap['object-calendar']`'s author state, so + // `ObjectCalendarSchema.data` below DERIVES the protocol's `data` row rather + // than re-spelling it. Aliased because the bare name is the protocol's, and a + // local symbol under a `@objectstack/spec` export's name reads to the next + // agent as the spec's own definition (`pnpm check:spec-symbols`). + ObjectCalendarProps as SpecObjectCalendarProps, ChartDrillDown, I18nLabel, DashboardWidget as SpecDashboardWidget, @@ -2770,16 +2776,45 @@ export interface ObjectCalendarSchema extends BaseSchema { */ objectName?: string; /** - * Data source configuration. Read FIRST by `getDataConfig` — `if - * (schema.data) return schema.data;` — ahead of `staticData` / `objectName`. + * 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`. * * 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 * on `.passthrough()` on the mirror's, so the record source the resolver - * prefers was the one neither face named. Same type as - * {@link ObjectMapSchema.data}. - */ - data?: ViewData; + * prefers was the one neither face named. + * + * ⛔ NOT `ViewData`, and NOT the same type as {@link ObjectMapSchema.data} — + * that is what objectui#9239 changed here, and it is a BREAKING NARROWING of + * a published authoring type. Until it, both published faces of this package + * declared the `{ provider, items }` PROVIDER BLOCK on this key while the + * protocol declared an array, so an author validating against + * `@object-ui/types` got a green verdict for metadata `os validate`, the save + * gate and (since objectui#8348) the renderer all refuse — `declared !== + * enforced` with the declaration on the wrong side, the shape AGENTS.md #0.1 + * exists to prevent. Maintainer ruling, decision batch #83 (2026-09-08), + * verbatim: 「8348 以协议为准」. + * + * DERIVED from the protocol's own row rather than re-spelled, so this key + * cannot drift from it a second time: `ComponentPropsMap['object-calendar']` + * (the spec's own `ObjectCalendarPropsSchema`) declares + * `z.array(z.unknown()).optional()`, described *"Pre-fetched records — skips + * the internal fetch"*. + * MEASURED on the installed artifact at `@objectstack/spec` 17.4.0 — the + * version this repository's `pnpm-lock.yaml` resolves — through the published + * `@objectstack/spec/ui` entry point: the provider block returns + * `success=false` with `expected: 'array'` at `path: ['data']`, the array + * returns `success=true`. The same reading is written down, per block, in + * `packages/core/src/utils/record-source.ts`. + * + * ⛔ The sibling blocks are NOT following: `object-map` and `object-gantt` + * have no `ComponentPropsMap` row at all, so the published row that governs + * them is this package's own `ObjectMapSchema.data` / `ObjectGanttSchema.data` + * — `ViewData` on both, deliberately kept. + */ + data?: SpecObjectCalendarProps['data']; /** Inline records, wrapped into a `{ provider: 'value' }` config by `getDataConfig`. */ staticData?: any[]; /** Field for event start */ diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index 58d4837f8f..88299976d4 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -940,12 +940,12 @@ export const ObjectMapConfigSchema = z.object({ }); /** - * objectui#6939 — the record-source refinement `ObjectMapSchema` and - * `ObjectGanttSchema` below share. + * objectui#6939 — the record-source refinement `ObjectMapSchema`, + * `ObjectGanttSchema` and `ObjectCalendarSchema` below share. * - * Both renderers resolve their records from ONE of three keys, in this order: - * `data` (a spec `ViewData` config), `staticData` (inline rows, wrapped into a - * `{ provider: 'value' }` config) or `objectName` (the bound object) — + * Those renderers resolve their records from ONE of three keys, in this order: + * `data`, `staticData` (inline rows, wrapped into a `{ provider: 'value' }` + * config) or `objectName` (the bound object) — * `getDataConfig` in `plugin-map/src/ObjectMap.tsx` and * `plugin-gantt/src/ObjectGantt.tsx`, each `if (schema.data) … if * (schema.staticData) … if (schema.objectName) … return null`. Both mirrors @@ -972,6 +972,25 @@ export const ObjectMapConfigSchema = z.object({ * Deliberately a `function`, not an `export const`: the parity census in * `__tests__/zod-mirror-parity.test.ts` reads `^export const` out of this * directory and would demand a registered TS counterpart for it. + * + * ⭐ WHAT `data` MEANS IS NOT SHARED, only its PRESENCE is (objectui#9239). + * This refinement asks one question — is any rung declared? — and `!== + * undefined` answers it whatever the value's kind, so the three members reach + * it from two different arms: + * + * - `object-map` / `object-gantt` — `data` is a spec `ViewData` PROVIDER BLOCK + * (`{ provider, … }`), the source the block will FETCH FROM. Neither has a + * `ComponentPropsMap` row, so the published row that governs them is this + * file's own `ViewDataSchema.optional()`. + * - `object-calendar` — `data` is an ARRAY of PRE-FETCHED RECORDS, drawn in + * 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. + * + * ⛔ 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. */ const RECORD_SOURCE_KEYS = ['data', 'staticData', 'objectName'] as const; function requireRecordSource(type: 'object-map' | 'object-gantt' | 'object-calendar') { @@ -1193,7 +1212,25 @@ export const ObjectGanttSchema = BaseSchema.extend({ export const ObjectCalendarSchema = BaseSchema.extend({ type: z.literal('object-calendar'), objectName: z.string().optional().describe('ObjectQL object name — the THIRD record source getDataConfig resolves, after data and staticData; one of the three must be present (objectui#7313)'), - data: ViewDataSchema.optional().describe('Data source configuration — read FIRST by getDataConfig; undeclared on either face until objectui#7313'), + // objectui#9239 — the ARRAY arm, mirroring `ComponentPropsMap['object-calendar'].data` + // on `@objectstack/spec` (`z.array(z.unknown()).optional()`, "Pre-fetched + // records — skips the internal fetch"). ⛔ NOT `ViewDataSchema`: this member + // declared the provider BLOCK until that card while the protocol declared an + // array, so `safeValidateSchema` returned a green verdict for a document the + // protocol, `os validate`, the save gate and (since objectui#8348) the + // renderer all refuse. Maintainer ruling, decision batch #83 (2026-09-08), + // verbatim 「8348 以协议为准」, and the standing lane arbiter 「以 objectstack + // 协议为准,文档应该以实际实现为准。协议不正确的应该先修改协议」. + // + // ⛔ `ObjectMapSchema.data` / `ObjectGanttSchema.data` above are NOT following: + // neither block has a `ComponentPropsMap` row, so the published row governing + // them is this file's own `ViewDataSchema.optional()` and it stays. + // + // Mirrored at the SAME requiredness as `../objectql.ts` (both optional) so the + // zod-mirror-parity ratchet stays at zero drift for this pair, and at the same + // TYPE: the TS face derives `SpecObjectCalendarProps['data']`, whose input is + // `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'), startDateField: z.string().optional().describe('Start date field'), endDateField: z.string().optional().describe('End date field'),