From a709aab8feb327d8f07b5af4fed5529f3be19db8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 08:16:50 +0000 Subject: [PATCH 1/2] docs(types,core): name resolveRecordSourceConfig, not getDataConfig, for the gantt and calendar record-source ladder The gantt and calendar zod describes and TS docs attributed the three-rung record-source ladder to a `getDataConfig` their renderers no longer hold (removed by objectui#7632; the calendar never had one). Name the shared `resolveRecordSourceConfig` instead; the map text stays, since `ObjectMap.tsx` keeps a local `getDataConfig` wrapper. The ladder order is unchanged. `record-source.ts` now says its quotation is the map faces' text. Adds a pin that gantt/calendar faces never name `getDataConfig`, with the map faces as control. Co-Authored-By: Claude --- packages/core/src/utils/record-source.ts | 20 ++++--- ...ctql-record-source-refinement-6939.test.ts | 55 +++++++++++++++++-- packages/types/src/objectql.ts | 21 +++---- packages/types/src/zod/objectql.zod.ts | 24 ++++---- 4 files changed, 87 insertions(+), 33 deletions(-) diff --git a/packages/core/src/utils/record-source.ts b/packages/core/src/utils/record-source.ts index 1ef24ff894..477bcc09eb 100644 --- a/packages/core/src/utils/record-source.ts +++ b/packages/core/src/utils/record-source.ts @@ -24,9 +24,11 @@ import type { ViewData } from '@object-ui/types'; * more than one binding?** — the published three-rung record-source ladder * (`data`, then `staticData`, then `objectName`), declared on both faces of * the contract (`ObjectMapSchema.objectName` / `ObjectGanttSchema.objectName` - * in `@object-ui/types`, and the `.describe` on their zod twins: - * *"objectName — the THIRD record source `getDataConfig` resolves, after - * `data` and `staticData`"*), ruled objectui#6939 (2026-09-02) and pinned by + * in `@object-ui/types`, and the `.describe` on their zod twins — the map + * twin's reads *"objectName — the THIRD record source `getDataConfig` + * resolves, after `data` and `staticData`"*, naming `ObjectMap.tsx`'s local + * wrapper; the gantt twin names `resolveRecordSourceConfig` itself), ruled + * objectui#6939 (2026-09-02) and pinned by * `objectql-record-source-refinement-6939.test.ts`. **That is this * function.** * 2. **How does `objectName` get POPULATED when it is absent?** — the @@ -47,8 +49,9 @@ import type { ViewData } from '@object-ui/types'; * ladder reduces to two rungs — the resolved config's object when it names one, * else the schema's own `objectName`, which is what a `value`/`api`-backed block * still needs for metadata reads, i18n field labels and permission verdicts. - * Callers pass the ALREADY-RESOLVED config (their `getDataConfig(schema)` - * output), so rung ordering is settled before this function is reached. + * Callers pass the ALREADY-RESOLVED config (their + * {@link resolveRecordSourceConfig} output, direct or through a local wrapper), + * so rung ordering is settled before this function is reached. * * ## No lenient rung was added (AGENTS.md #0.1) * @@ -62,7 +65,7 @@ import type { ViewData } from '@object-ui/types'; * * @param schema - The block's schema; only `objectName` is read. * @param dataConfig - The RESOLVED data config — the caller's own - * `getDataConfig(schema)` output, `null` when nothing is bound. + * {@link resolveRecordSourceConfig} output, `null` when nothing is bound. * @returns The bound object's name, or `undefined` when neither the resolved * config nor the schema names one. * @@ -185,7 +188,10 @@ function authoredDataIsOnTheDeclaredArm(authored: unknown, arm: RecordSourceData * * `data`, then `staticData`, then `objectName` — declared on both faces of the * published contract and pinned by - * `objectql-record-source-refinement-6939.test.ts`: + * `objectql-record-source-refinement-6939.test.ts`. Quoted here from the + * `ObjectMapSchema` faces, whose `getDataConfig` is `ObjectMap.tsx`'s local + * wrapper around this function; the gantt and calendar faces name + * `resolveRecordSourceConfig` instead (objectui#9618): * * 1. **`data`** — *"Data source configuration. Read FIRST by `getDataConfig`"*, * honoured ONLY on the arm `dataArm` names. Returned verbatim, so a config diff --git a/packages/types/src/__tests__/objectql-record-source-refinement-6939.test.ts b/packages/types/src/__tests__/objectql-record-source-refinement-6939.test.ts index 464a6a5a75..8f3402128f 100644 --- a/packages/types/src/__tests__/objectql-record-source-refinement-6939.test.ts +++ b/packages/types/src/__tests__/objectql-record-source-refinement-6939.test.ts @@ -18,10 +18,12 @@ * ## The defect * * Both mirrors REQUIRED `objectName`. Both renderers resolve their records from - * one of THREE keys, in this order — `getDataConfig` in - * `plugin-map/src/ObjectMap.tsx` (`schema.data`, `schema.staticData`, - * `schema.objectName`) and `plugin-gantt/src/ObjectGantt.tsx` (the same three, - * the same order). A document authored on `staticData` alone draws correctly + * one of THREE keys, in this order (`schema.data`, `schema.staticData`, + * `schema.objectName`) — today through the shared `resolveRecordSourceConfig` + * in `@object-ui/core` (objectui#7632), which `plugin-map/src/ObjectMap.tsx` + * reaches via its local `getDataConfig` wrapper and + * `plugin-gantt/src/ObjectGantt.tsx` calls directly. A document authored on + * `staticData` alone draws correctly * and was refused by `safeValidateSchema`: six catalog entries, three per * component, every one of them `staticData`-only. * @@ -43,7 +45,7 @@ import { describe, it, expect } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { ObjectGanttSchema, ObjectMapSchema, safeValidateSchema } from '../zod/index.zod'; +import { ObjectCalendarSchema, ObjectGanttSchema, ObjectMapSchema, safeValidateSchema } from '../zod/index.zod'; const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..'); @@ -149,3 +151,46 @@ describe('objectui#6939 — the refinement sits on declared keys, and the object expect(ObjectMapSchema.safeParse({ type: 'object-map', objectName: 'stores', data: 'nope' }).success).toBe(false); }); }); + +describe('objectui#9618 — the record-source text names the function each renderer actually has', () => { + // `getDataConfig` left `ObjectGantt.tsx` (and never lived in + // `ObjectCalendar.tsx`) when objectui#7632 moved the ladder into + // `resolveRecordSourceConfig`; only `ObjectMap.tsx` keeps a local wrapper by + // that name. So the gantt and calendar faces must not name it, and the map + // face — whose text is TRUE — is the control that the descriptions and the + // source slice are actually being read. + type Described = { shape: Record }; + const describes = (member: unknown): Record => + Object.fromEntries( + Object.entries((member as Described).shape).map(([k, v]) => [k, v.description ?? '']), + ); + + /** The `export interface NAME` block of the TS face, up to the next export. */ + function tsFace(name: string): string { + const src = fs.readFileSync(path.join(REPO_ROOT, 'packages/types/src/objectql.ts'), 'utf8'); + const at = src.indexOf(`export interface ${name} `); + expect(at, `${name} interface not found in objectql.ts`).toBeGreaterThan(-1); + const end = src.indexOf('\nexport ', at + 1); + return src.slice(at, end === -1 ? undefined : end); + } + + it.each([ + ['ObjectGanttSchema', ObjectGanttSchema], + ['ObjectCalendarSchema', ObjectCalendarSchema], + ] as const)('%s: no zod describe and no TS doc names `getDataConfig`', (name, member) => { + const d = describes(member); + expect(Object.entries(d).filter(([, text]) => text.includes('getDataConfig'))).toEqual([]); + expect(d.objectName).toContain('resolveRecordSourceConfig'); + expect(d.staticData).toContain('resolveRecordSourceConfig'); + const face = tsFace(name); + expect(face).not.toContain('getDataConfig'); + expect(face).toContain('resolveRecordSourceConfig'); + }); + + it('⛔ CONTROL: the map faces still name its real local `getDataConfig`', () => { + expect(describes(ObjectMapSchema).objectName).toContain('getDataConfig'); + expect(tsFace('ObjectMapSchema')).toContain('getDataConfig'); + const renderer = fs.readFileSync(path.join(REPO_ROOT, 'packages/plugin-map/src/ObjectMap.tsx'), 'utf8'); + expect(renderer).toContain('function getDataConfig('); + }); +}); diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index 5d9bb2bb33..6de175d70c 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -2928,9 +2928,9 @@ export interface ObjectTreeSchema extends BaseSchema { export interface ObjectGanttSchema extends BaseSchema { type: 'object-gantt'; /** - * ObjectQL object name — the THIRD record source `getDataConfig` resolves, - * after {@link ObjectGanttSchema.data} and {@link ObjectGanttSchema.staticData} - * (`plugin-gantt/src/ObjectGantt.tsx`). + * ObjectQL object name — the THIRD record source `resolveRecordSourceConfig` + * (`@object-ui/core`) resolves, after {@link ObjectGanttSchema.data} and + * {@link ObjectGanttSchema.staticData} (called by `plugin-gantt/src/ObjectGantt.tsx`). * * Optional since objectui#6939: a gantt authored on inline rows never reads * this key, and requiring it refused three catalog entries that draw @@ -2941,8 +2941,9 @@ export interface ObjectGanttSchema extends BaseSchema { */ objectName?: string; /** - * Data source configuration. Read FIRST by `getDataConfig` — `if - * (schema.data) return schema.data;` — ahead of `staticData` / `objectName`. + * Data source configuration. Read FIRST by `resolveRecordSourceConfig` + * (`@object-ui/core`), which returns it verbatim when it is on the block's + * declared arm — ahead of `staticData` / `objectName`. * * Declared by objectui#6939, in the same stroke as the mirror's `data`: until * then the read landed on `BaseSchema`'s index signature on this side and @@ -3232,7 +3233,7 @@ export interface ObjectGanttSchema extends BaseSchema { // correct and is why they now have to be declared HERE. `plugin-gantt`'s // registry mapping (`OBJECT_GANTT_DATA_SOURCE` in `index.tsx`) names `filter` // and `sort` as the two keys the element data-source binding maps onto. - /** Inline records, wrapped into a `{ provider: 'value' }` config by `getDataConfig`. */ + /** Inline records, wrapped into a `{ provider: 'value' }` config by `resolveRecordSourceConfig`. */ staticData?: any[]; /** Query filter (JSON Rules format), forwarded verbatim as `$filter`. */ filter?: any[]; @@ -3260,9 +3261,9 @@ export interface ObjectGanttSchema extends BaseSchema { export interface ObjectCalendarSchema extends BaseSchema { type: 'object-calendar'; /** - * ObjectQL object name — the THIRD record source `getDataConfig` resolves, - * after {@link ObjectCalendarSchema.data} and {@link ObjectCalendarSchema.staticData} - * (`plugin-calendar/src/ObjectCalendar.tsx`). + * ObjectQL object name — the THIRD record source `resolveRecordSourceConfig` + * (`@object-ui/core`) resolves, after {@link ObjectCalendarSchema.data} and + * {@link ObjectCalendarSchema.staticData} (called by `plugin-calendar/src/ObjectCalendar.tsx`). * * Optional since objectui#7313 (the objectui#6939 shape): a calendar authored * on inline rows never reads this key, and requiring it refused the two @@ -3315,7 +3316,7 @@ export interface ObjectCalendarSchema extends BaseSchema { * — `ViewData` on both, deliberately kept. */ data?: SpecObjectCalendarProps['data']; - /** Inline records, wrapped into a `{ provider: 'value' }` config by `getDataConfig`. */ + /** Inline records, wrapped into a `{ provider: 'value' }` config by `resolveRecordSourceConfig`. */ staticData?: any[]; /** * The configuration container, and the FIRST thing this element's renderer diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index 68f174ff89..eb4c0e6bb6 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -1705,16 +1705,17 @@ export const ObjectMapConfigSchema = z.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 + * config) or `objectName` (the bound object) — the shared ladder + * `resolveRecordSourceConfig` in `@object-ui/core`, which + * `plugin-map/src/ObjectMap.tsx` reaches through its local `getDataConfig` + * wrapper and `plugin-gantt/src/ObjectGantt.tsx` calls directly: `data`, then + * `staticData`, then `objectName`, else `null`. Both mirrors * used to REQUIRE `objectName` alone, so a document authored on `staticData` * (6 of the 20 catalog entries objectui#6939 measured) drew correctly and was * refused by `safeValidateSchema` — `declared !== enforced`, with the corpus * on the right side. `objectName` is optional on both members now, and this * refinement carries the requirement the renderers actually have: with none of - * the three present `getDataConfig` returns `null` and nothing is drawn. + * the three present the ladder returns `null` and nothing is drawn. * * Presence is `!== undefined` — the ruling's wording ("at least one of `data`, * `staticData`, `objectName` is present"), NOT the renderers' truthiness: an @@ -1825,7 +1826,8 @@ export const ObjectTreeSchema = BaseSchema.extend({ * ObjectGantt Schema * * `objectName` is OPTIONAL and the member ends in `requireRecordSource` - * (objectui#6939): `getDataConfig` (`plugin-gantt/src/ObjectGantt.tsx`) reads + * (objectui#6939): the shared record-source ladder `resolveRecordSourceConfig` + * (`@object-ui/core`, called by `plugin-gantt/src/ObjectGantt.tsx`) reads * `data`, then `staticData`, then `objectName`, so a gantt authored on inline * rows never reads the object name — three catalog entries drew correctly and * were refused here. `data` is declared for the first time in the same stroke: @@ -1837,8 +1839,8 @@ export const ObjectTreeSchema = BaseSchema.extend({ */ export const ObjectGanttSchema = BaseSchema.extend({ type: z.literal('object-gantt'), - 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#6939)'), - data: ViewDataSchema.optional().describe('Data source configuration — read FIRST by getDataConfig; undeclared on either face until objectui#6939'), + objectName: z.string().optional().describe('ObjectQL object name — the THIRD record source resolveRecordSourceConfig resolves, after data and staticData; one of the three must be present (objectui#6939)'), + data: ViewDataSchema.optional().describe('Data source configuration — read FIRST by resolveRecordSourceConfig; undeclared on either face until objectui#6939'), startDateField: z.string().optional().describe('Start date field'), endDateField: z.string().optional().describe('End date field'), titleField: z.string().optional().describe('Title field'), @@ -1957,7 +1959,7 @@ export const ObjectGanttSchema = BaseSchema.extend({ // The query/data keys the fetch path reads. They were declared on // `ObjectGridSchema` — what `ObjectGanttProps.schema` used to be typed as before // objectui#5903 retyped it to `ObjectGanttSchema` — so they need declaring here. - staticData: z.array(z.any()).optional().describe('Inline records, wrapped into a { provider: value } data config — read SECOND by getDataConfig'), + staticData: z.array(z.any()).optional().describe('Inline records, wrapped into a { provider: value } data config — read SECOND by resolveRecordSourceConfig'), filter: z.array(z.any()).optional().describe('Query filter, forwarded verbatim as $filter'), sort: z.array(SortConfigSchema).optional().describe('Sort configuration, forwarded as $orderby (array only; the legacy string clause is retired — objectui#8221)'), // objectui#10250 — the full-text pair the record query carries, declared in @@ -1986,7 +1988,7 @@ 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)'), + objectName: z.string().optional().describe('ObjectQL object name — the THIRD record source resolveRecordSourceConfig resolves, after data and staticData; one of the three must be present (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 @@ -2006,7 +2008,7 @@ export const ObjectCalendarSchema = BaseSchema.extend({ // 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'), + staticData: z.array(z.any()).optional().describe('Inline records, wrapped into a { provider: value } data config — read SECOND by resolveRecordSourceConfig'), // 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 From ba0545c0d60781d446699ce0528352abd56d08a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 08:20:38 +0000 Subject: [PATCH 2/2] chore(changeset): declare the objectui#9618 record-source text fix Co-Authored-By: Claude --- .../9618-record-source-ladder-attribution.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .changeset/9618-record-source-ladder-attribution.md diff --git a/.changeset/9618-record-source-ladder-attribution.md b/.changeset/9618-record-source-ladder-attribution.md new file mode 100644 index 0000000000..32e44a88a1 --- /dev/null +++ b/.changeset/9618-record-source-ladder-attribution.md @@ -0,0 +1,17 @@ +--- +'@object-ui/types': patch +'@object-ui/core': patch +--- + +The `ObjectGanttSchema` and `ObjectCalendarSchema` record-source text now names +the function their renderers actually call (objectui#9618). + +Their zod `.describe` strings and TS docs said the `data` → `staticData` → +`objectName` ladder is resolved by `getDataConfig`. Neither renderer has had a +function by that name since the ladder moved into `@object-ui/core`'s shared +`resolveRecordSourceConfig` (objectui#7632), so the text now names that +function. The ladder order is unchanged, and so is every accepted document. +The `ObjectMapSchema` text still says `getDataConfig`, which is true: +`ObjectMap.tsx` keeps a local wrapper by that name that delegates to the shared +ladder. The `resolveRecordSourceConfig` docblock in `@object-ui/core` now says +that the text it quotes is the map faces' text.