Skip to content

Commit 6414dfd

Browse files
claude[bot]claude
andauthored
refactor(types)!: retire the two report data-source keys on both faces (#7487)
`ReportComponentSchema.dataSource` and `ReportBuilderSchema.dataSources` were annotated with `DataSource`, the runtime adapter interface, which no JSON document can author — and no read site ever consumed either key. Both become `?: never` on the TypeScript face and `retirementTombstone(...)` on the zod mirror, so an authored value is refused by name instead of being accepted by `z.any()` and read by nobody. The docs page stops teaching the keys and records the retirement in prose. The replacement binding key the ruling names (`data?: ViewData`) is NOT declared: `data` is already the report row array on that interface, with a live read in `LegacyReportRenderer`. Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6a99bb2 commit 6414dfd

5 files changed

Lines changed: 238 additions & 24 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
'@object-ui/types': minor
3+
---
4+
5+
The two report data-source keys are retired on both faces (objectui#6121,
6+
maintainer ruling of 2026-08-30, decision batch #8 — option A's retirement half;
7+
ADR-0049 enforce-or-remove).
8+
9+
**The accept set of a published validator moves** (`@object-ui/types/zod`):
10+
11+
- `ReportComponentSchema.dataSource` was `z.any().optional()`, so any JSON value
12+
parsed green and was then read by nobody.
13+
- `ReportBuilderSchema.dataSources` was `z.array(z.any()).optional()`, on a node
14+
type no renderer is registered for at all.
15+
16+
Both now carry `retirementTombstone(...)`: an authored value is refused at the
17+
key's own path with `code: 'invalid_type'` and a message that names the key, says
18+
why it is retired and points at the spelling that runs. Nothing that used to be
19+
refused parses green.
20+
21+
**The TypeScript face** — both keys become `?: never` rather than being deleted,
22+
so an author who still writes one gets a `tsc` error at the authoring site
23+
instead of a silently stripped key. They were annotated `DataSource` /
24+
`DataSource[]`, the runtime ADAPTER interface (`find(resource, params)`), which
25+
no JSON document can author; that mis-annotation is the defect objectui#6121 was
26+
filed for, since every example on `content/docs/core/report-schema.mdx` authored
27+
a config object against it.
28+
29+
**Why this is a retirement and not a rename.** No read site consumed either key:
30+
`@object-ui/plugin-report`'s `ReportRenderer` takes its adapter as a React prop
31+
or from `SchemaRendererContext`, never off `schema.dataSource`, and the live
32+
9.0 path binds a semantic-layer `dataset` (ADR-0021). Authored occurrences
33+
measured zero in this repo and in the sibling `objectstack` checkout, whose
34+
report metadata binds `dataset` throughout — the ruling's own deprecation-window
35+
exit criterion. A stored document that still carries the key now fails loudly at
36+
`safeParse` instead of being accepted and ignored; drop the key, and bind the
37+
report through `dataset`.
38+
39+
The replacement binding key the ruling names (`data?: ViewData`) is deliberately
40+
NOT declared here, and is escalated on objectui#6121: `data` is already a live
41+
key on `ReportComponentSchema` — the report ROW array, read by
42+
`LegacyReportRenderer` as `data.length` / `data.map` — so declaring the binding
43+
under that name would put two authoring contracts on one key inside one
44+
renderer.
45+
46+
Pinned in `packages/types/src/__tests__/report-schema-authoring-face.test.ts`:
47+
the `never` twins, the named refusals with their issue envelope, the `.describe()`
48+
metadata channel, and controls that a report without the key still parses.

‎content/docs/core/report-schema.mdx‎

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,18 @@ complete list.
8585
| `type` | `'report'` | Component type identifier (required) |
8686
| `title` | `string` | Report title |
8787
| `description` | `string` | Report description |
88-
| `dataSource` | `DataSource` | Data source configuration |
88+
89+
> **Retired (objectui#6121):** `ReportComponentSchema.dataSource` and
90+
> `ReportBuilderSchema.dataSources` used to be documented and declared here.
91+
> Both were annotated with `DataSource`, the runtime **adapter** interface
92+
> (`find(resource, params)`), which no JSON document can author — and no
93+
> renderer ever read either key off a schema: the report renderers take their
94+
> adapter as a React prop or from the renderer context. Both keys are now
95+
> `never` on the TypeScript face and are refused **by name** by the published
96+
> validator, so an authored value fails loudly instead of being accepted and
97+
> ignored. A report binds its data through the semantic-layer `dataset` form
98+
> (ADR-0021); a legacy presentation report receives already-fetched rows under
99+
> `data`.
89100
90101
### Report Fields
91102

@@ -227,15 +238,6 @@ const comprehensiveReport: ReportComponentSchema = {
227238
title: 'Quarterly Sales Analysis',
228239
description: 'Comprehensive sales performance analysis by region and product',
229240
230-
// Data source
231-
dataSource: {
232-
provider: 'api',
233-
read: {
234-
url: '/api/sales',
235-
method: 'GET'
236-
}
237-
},
238-
239241
// Report fields
240242
fields: [
241243
{
@@ -406,11 +408,6 @@ const builder: ReportBuilderSchema = {
406408
title: 'Untitled Report'
407409
},
408410
409-
dataSources: [
410-
{ provider: 'api', read: { url: '/api/sales' } },
411-
{ provider: 'api', read: { url: '/api/customers' } }
412-
],
413-
414411
availableFields: [
415412
{ name: 'revenue', label: 'Revenue', type: 'number' },
416413
{ name: 'units', label: 'Units Sold', type: 'number' }

‎packages/types/src/__tests__/report-schema-authoring-face.test.ts‎

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,33 @@
6363
* The `expect(…)` lines below are RUNTIME and are judged by vitest. Every
6464
* relaxation therefore carries at least one assertion of each kind, so neither
6565
* instrument going missing can make this file vacuous on its own.
66+
*
67+
* ## 3. The RETIREMENT this card's own ruling ordered (maintainer, 2026-08-30)
68+
*
69+
* The third section pins the opposite direction: `ReportComponentSchema.dataSource`
70+
* and `ReportBuilderSchema.dataSources` are RETIRED. Both were annotated with the
71+
* runtime `DataSource` ADAPTER — a shape no JSON document can author — and no read
72+
* site ever consumed either key. A retirement needs its own pin for the mirror
73+
* reason a widening does: `?: never` compiles for every existing caller (nobody
74+
* wrote the key), so nothing would fail if a later edit restored the adapter
75+
* annotation or relaxed the mirror back to `z.any()`. Both halves are pinned —
76+
* the `never` TypeScript twin AND the mirror's named refusal — because either one
77+
* alone leaves `declared !== enforced`, which is the defect ADR-0049 names.
6678
*/
6779

6880
import { describe, it, expect } from 'vitest';
6981
import type {
82+
ReportBuilderSchema,
7083
ReportComponentSchema,
7184
ReportExportConfig,
7285
ReportExportFormat,
7386
} from '../reports.js';
7487
import type { ChartDataSeries } from '../data-display.js';
7588
import { ChartDataSeriesSchema } from '../zod/data-display.zod.js';
76-
import { ReportComponentSchema as ReportComponentZodSchema } from '../zod/reports.zod.js';
89+
import {
90+
ReportBuilderSchema as ReportBuilderZodSchema,
91+
ReportComponentSchema as ReportComponentZodSchema,
92+
} from '../zod/reports.zod.js';
7793

7894
/** `true` only when the two types are mutually assignable AND identical. */
7995
type Eq<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
@@ -162,3 +178,98 @@ describe('objectui#6121 — ChartDataSeries declares the per-series family overr
162178
expect(ChartDataSeriesSchema.parse(plain).type).toBeUndefined();
163179
});
164180
});
181+
182+
describe('objectui#6121 — the two report data-source keys are retired on both faces', () => {
183+
// 3a. THE TYPE PIN. `?: never` resolves the member type to `undefined`, so
184+
// this line fails if either key is restored to `DataSource` / `DataSource[]`
185+
// — or to any other value type, including the `ViewData` binding whose key
186+
// name is still an open question on this card.
187+
type RetiredDataSource = ReportComponentSchema['dataSource'];
188+
type RetiredDataSources = ReportBuilderSchema['dataSources'];
189+
type _DataSourceStaysRetired = Assert<Eq<RetiredDataSource, undefined>>;
190+
type _DataSourcesStayRetired = Assert<Eq<RetiredDataSources, undefined>>;
191+
192+
it('refuses an authored `dataSource` on both faces, by name', () => {
193+
const authored = {
194+
type: 'report' as const,
195+
title: 'Quarterly Sales Analysis',
196+
// The exact face `content/docs/core/report-schema.mdx` used to teach.
197+
dataSource: { provider: 'api', read: { url: '/api/sales', method: 'GET' } },
198+
};
199+
200+
// @ts-expect-error `dataSource` is retired — `?: never` admits no value
201+
const typed: ReportComponentSchema = authored;
202+
expect(typed).toBeTruthy();
203+
204+
const result = ReportComponentZodSchema.safeParse(authored);
205+
expect(result.success).toBe(false);
206+
// The ENVELOPE, not the fact that something failed: one issue, at this
207+
// key's own path, reported as `invalid_type` (what `z.never()` emits) —
208+
// and carrying the tombstone's guidance rather than zod's generic text,
209+
// which is the half `retirementTombstone` exists for.
210+
const issues = result.success ? [] : result.error.issues;
211+
expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'dataSource']]);
212+
expect(issues[0]?.message).toContain('RETIRED (objectui#6121, ADR-0049)');
213+
});
214+
215+
it('refuses an authored `dataSources` on the builder, by name', () => {
216+
const authored = {
217+
type: 'report-builder' as const,
218+
dataSources: [{ provider: 'api', read: { url: '/api/sales' } }],
219+
};
220+
221+
// @ts-expect-error `dataSources` is retired — `?: never` admits no value
222+
const typed: ReportBuilderSchema = authored;
223+
expect(typed).toBeTruthy();
224+
225+
const result = ReportBuilderZodSchema.safeParse(authored);
226+
expect(result.success).toBe(false);
227+
const issues = result.success ? [] : result.error.issues;
228+
expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'dataSources']]);
229+
expect(issues[0]?.message).toContain('RETIRED (objectui#6121, ADR-0049)');
230+
});
231+
232+
// 3b. CONTROLS, in the same run. Two zeros above need two things that fire:
233+
// without these, a mirror that refused EVERYTHING would read as a pass, and
234+
// so would a `.safeParse` that had stopped being called at all.
235+
it('the same report without the retired key still parses, and the row array is untouched', () => {
236+
const report: ReportComponentSchema = {
237+
type: 'report',
238+
title: 'Quarterly Sales Analysis',
239+
// `data` is the report ROW array — a live key with a live read
240+
// (`LegacyReportRenderer` reads `data.length` / `data.map`). It is NOT
241+
// the retired binding, and this control is what keeps the retirement
242+
// above from reading as "reports refuse data".
243+
data: [{ region: 'EMEA', revenue: 1 }],
244+
};
245+
const result = ReportComponentZodSchema.safeParse(report);
246+
expect(result.success).toBe(true);
247+
expect(result.success && result.data.data).toHaveLength(1);
248+
249+
const builder: ReportBuilderSchema = { type: 'report-builder', showPreview: true };
250+
expect(ReportBuilderZodSchema.safeParse(builder).success).toBe(true);
251+
});
252+
253+
// 3c. The guidance reaches the OTHER author-facing channel too — the
254+
// `.describe()` metadata that feeds generated JSON Schema and the docs
255+
// surface. One string, two channels, so they cannot drift apart.
256+
it('publishes the retirement guidance as schema metadata', () => {
257+
const shapeOf = (schema: { shape: Record<string, { description?: string }> }) => schema.shape;
258+
const componentDescribe = shapeOf(
259+
ReportComponentZodSchema as unknown as { shape: Record<string, { description?: string }> },
260+
).dataSource?.description;
261+
const builderDescribe = shapeOf(
262+
ReportBuilderZodSchema as unknown as { shape: Record<string, { description?: string }> },
263+
).dataSources?.description;
264+
265+
expect(componentDescribe).toContain('RETIRED (objectui#6121, ADR-0049)');
266+
expect(builderDescribe).toContain('RETIRED (objectui#6121, ADR-0049)');
267+
// Control for the reader itself: a NON-retired member's description is
268+
// still its own noun, so the two hits above are not "every key says
269+
// RETIRED".
270+
expect(
271+
shapeOf(ReportComponentZodSchema as unknown as { shape: Record<string, { description?: string }> })
272+
.title?.description,
273+
).toBe('Report title');
274+
});
275+
});

‎packages/types/src/reports.ts‎

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import type { z } from 'zod';
3434
import type { ReportType as SpecReportType } from '@objectstack/spec/ui';
3535
import type { BaseSchema, SchemaNode } from './base.js';
3636
import type { ChartSchema } from './data-display.js';
37-
import type { DataSource } from './data.js';
3837

3938
/**
4039
* Report Export Format
@@ -375,9 +374,37 @@ export interface ReportComponentSchema extends BaseSchema {
375374
reportType?: ReportType;
376375

377376
/**
378-
* Data source configuration
377+
* Data source configuration — RETIRED (objectui#6121, maintainer ruling of
378+
* 2026-08-30, decision batch #8; ADR-0049 enforce-or-remove).
379+
*
380+
* The key was annotated `DataSource`, the RUNTIME ADAPTER interface declared
381+
* in `./data.ts` (`find(resource, params)`, `searchAll?()`, and friends). No
382+
* JSON document can author that shape, and nothing ever read the key off a
383+
* report schema: `@object-ui/plugin-report`'s `ReportRenderer` takes its
384+
* adapter as a React prop or off `SchemaRendererContext`, never off
385+
* `schema.dataSource`, and the live 9.0 path binds a semantic-layer
386+
* `dataset` instead (ADR-0021). Measured zero authored occurrences in this
387+
* repo and in the sibling `objectstack` checkout, whose authored reports all
388+
* bind `dataset` — that measurement is the ruling's own deprecation-window
389+
* exit criterion.
390+
*
391+
* `?: never` rather than deleted, so an author who still writes the key gets
392+
* a `tsc` error at the authoring site and a NAMED refusal from the zod twin
393+
* (`retirementTombstone` in `./zod/reports.zod.ts`) instead of a silently
394+
* stripped key — the disposition objectui#7344 landed for `onSave` /
395+
* `onCancel` on {@link ReportBuilderSchema}.
396+
*
397+
* ⚠️ The REPLACEMENT binding key the ruling names — `data?: ViewData` — is
398+
* deliberately NOT declared here. `data` is already taken on this interface
399+
* by the report ROW array below, which `LegacyReportRenderer` reads
400+
* (`data.length`, `data.map`, and as the chart's rows); declaring the
401+
* binding under that same name would put two authoring contracts on one key
402+
* inside one renderer, which is the objectstack#5576 collision this card's
403+
* own ruling rejected option D for. Escalated on objectui#6121.
404+
*
405+
* @deprecated Retired — no read site ever consumed this key.
379406
*/
380-
dataSource?: DataSource;
407+
dataSource?: never;
381408

382409
/**
383410
* Report fields
@@ -494,9 +521,21 @@ export interface ReportBuilderSchema extends BaseSchema {
494521
report?: ReportComponentSchema;
495522

496523
/**
497-
* Available data sources
524+
* Available data sources — RETIRED (objectui#6121, maintainer ruling of
525+
* 2026-08-30, decision batch #8; ADR-0049 enforce-or-remove).
526+
*
527+
* Same reading as {@link ReportComponentSchema.dataSource}, one degree
528+
* further from a reader: no renderer is registered for `report-builder` at
529+
* all — measured, zero `ComponentRegistry.register('report-builder', …)`
530+
* sites, with the bare `'report'` registration in `@object-ui/plugin-report`
531+
* as the positive control that makes that zero a reading. It is the same
532+
* measurement that retired `onSave` / `onCancel` below (objectui#7344), and
533+
* the declared element type was an array of the runtime `DataSource`
534+
* ADAPTER, which JSON cannot author.
535+
*
536+
* @deprecated Retired — no read site ever consumed this key.
498537
*/
499-
dataSources?: DataSource[];
538+
dataSources?: never;
500539

501540
/**
502541
* Available fields

‎packages/types/src/zod/reports.zod.ts‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import { z } from 'zod';
2020
import { BaseSchema, SchemaNodeSchema } from './base.zod.js';
2121
import { ChartSchema } from './data-display.zod.js';
22-
import { handlerKeyRefusal } from './tombstone.zod.js';
22+
import { handlerKeyRefusal, retirementTombstone } from './tombstone.zod.js';
2323

2424
/**
2525
* Report Export Format Schema
@@ -143,7 +143,17 @@ export const ReportComponentSchema = BaseSchema.extend({
143143
type: z.literal('report'),
144144
title: z.string().optional().describe('Report title'),
145145
description: z.string().optional().describe('Report description'),
146-
dataSource: z.any().optional().describe('Data source configuration'),
146+
// RETIRED (objectui#6121, maintainer ruling of 2026-08-30, decision batch #8;
147+
// ADR-0049 enforce-or-remove). The TS twin is `dataSource?: never`; the key
148+
// stays DECLARED so an authored value is refused BY NAME instead of being
149+
// waved through by `z.any()` and then read by nobody.
150+
dataSource: retirementTombstone(
151+
'Data source configuration — RETIRED (objectui#6121, ADR-0049). The key was declared as the ' +
152+
'runtime `DataSource` ADAPTER (`find(resource, params)`), which JSON has no value for, and ' +
153+
'no renderer ever read it off a report schema: the report renderers take their adapter as a ' +
154+
'React prop or from `SchemaRendererContext`. Bind a report through the semantic-layer ' +
155+
'`dataset` form (ADR-0021); a legacy presentation report receives its rows under `data`.',
156+
),
147157
fields: z.array(ReportFieldSchema).optional().describe('Report fields'),
148158
filters: z.array(ReportFilterSchema).optional().describe('Report filters'),
149159
groupBy: z.array(ReportGroupBySchema).optional().describe('Group by configuration'),
@@ -165,7 +175,16 @@ export const ReportComponentSchema = BaseSchema.extend({
165175
export const ReportBuilderSchema = BaseSchema.extend({
166176
type: z.literal('report-builder'),
167177
report: ReportComponentSchema.optional().describe('Initial report configuration'),
168-
dataSources: z.array(z.any()).optional().describe('Available data sources'),
178+
// RETIRED with `ReportComponentSchema.dataSource` above (objectui#6121), one
179+
// degree further from a reader: no renderer is registered for
180+
// `report-builder`, the same measurement that retired the two handler keys
181+
// below (objectui#7344).
182+
dataSources: retirementTombstone(
183+
'Available data sources — RETIRED (objectui#6121, ADR-0049). No renderer is registered for ' +
184+
'`report-builder`, so nothing could ever read this key, and it was declared as an array of ' +
185+
'the runtime `DataSource` ADAPTER, which JSON has no value for. Bind a report through the ' +
186+
'semantic-layer `dataset` form (ADR-0021).',
187+
),
169188
availableFields: z.array(ReportFieldSchema).optional().describe('Available fields'),
170189
showPreview: z.boolean().optional().describe('Show preview'),
171190
// RETIRED (objectui#7344, the objectui#6182 ruling in the objectui#6124 shape):

0 commit comments

Comments
 (0)