Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .changeset/6121-retire-report-data-source.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
---
'@object-ui/types': minor
---

The two report data-source keys are retired on both faces (objectui#6121,
maintainer ruling of 2026-08-30, decision batch #8 — option A's retirement half;
ADR-0049 enforce-or-remove).

**The accept set of a published validator moves** (`@object-ui/types/zod`):

- `ReportComponentSchema.dataSource` was `z.any().optional()`, so any JSON value
parsed green and was then read by nobody.
- `ReportBuilderSchema.dataSources` was `z.array(z.any()).optional()`, on a node
type no renderer is registered for at all.

Both now carry `retirementTombstone(...)`: an authored value is refused at the
key's own path with `code: 'invalid_type'` and a message that names the key, says
why it is retired and points at the spelling that runs. Nothing that used to be
refused parses green.

**The TypeScript face** — both keys become `?: never` rather than being deleted,
so an author who still writes one gets a `tsc` error at the authoring site
instead of a silently stripped key. They were annotated `DataSource` /
`DataSource[]`, the runtime ADAPTER interface (`find(resource, params)`), which
no JSON document can author; that mis-annotation is the defect objectui#6121 was
filed for, since every example on `content/docs/core/report-schema.mdx` authored
a config object against it.

**Why this is a retirement and not a rename.** No read site consumed either key:
`@object-ui/plugin-report`'s `ReportRenderer` takes its adapter as a React prop
or from `SchemaRendererContext`, never off `schema.dataSource`, and the live
9.0 path binds a semantic-layer `dataset` (ADR-0021). Authored occurrences
measured zero in this repo and in the sibling `objectstack` checkout, whose
report metadata binds `dataset` throughout — the ruling's own deprecation-window
exit criterion. A stored document that still carries the key now fails loudly at
`safeParse` instead of being accepted and ignored; drop the key, and bind the
report through `dataset`.

The replacement binding key the ruling names (`data?: ViewData`) is deliberately
NOT declared here, and is escalated on objectui#6121: `data` is already a live
key on `ReportComponentSchema` — the report ROW array, read by
`LegacyReportRenderer` as `data.length` / `data.map` — so declaring the binding
under that name would put two authoring contracts on one key inside one
renderer.

Pinned in `packages/types/src/__tests__/report-schema-authoring-face.test.ts`:
the `never` twins, the named refusals with their issue envelope, the `.describe()`
metadata channel, and controls that a report without the key still parses.
27 changes: 12 additions & 15 deletions content/docs/core/report-schema.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,18 @@ complete list.
| `type` | `'report'` | Component type identifier (required) |
| `title` | `string` | Report title |
| `description` | `string` | Report description |
| `dataSource` | `DataSource` | Data source configuration |

> **Retired (objectui#6121):** `ReportComponentSchema.dataSource` and
> `ReportBuilderSchema.dataSources` used to be documented and declared here.
> Both were annotated with `DataSource`, the runtime **adapter** interface
> (`find(resource, params)`), which no JSON document can author — and no
> renderer ever read either key off a schema: the report renderers take their
> adapter as a React prop or from the renderer context. Both keys are now
> `never` on the TypeScript face and are refused **by name** by the published
> validator, so an authored value fails loudly instead of being accepted and
> ignored. A report binds its data through the semantic-layer `dataset` form
> (ADR-0021); a legacy presentation report receives already-fetched rows under
> `data`.

### Report Fields

Expand DownExpand Up@@ -227,15 +238,6 @@ const comprehensiveReport: ReportComponentSchema = {
title: 'Quarterly Sales Analysis',
description: 'Comprehensive sales performance analysis by region and product',

// Data source
dataSource: {
provider: 'api',
read: {
url: '/api/sales',
method: 'GET'
}
},

// Report fields
fields: [
{
Expand DownExpand Up@@ -406,11 +408,6 @@ const builder: ReportBuilderSchema = {
title: 'Untitled Report'
},

dataSources: [
{ provider: 'api', read: { url: '/api/sales' } },
{ provider: 'api', read: { url: '/api/customers' } }
],

availableFields: [
{ name: 'revenue', label: 'Revenue', type: 'number' },
{ name: 'units', label: 'Units Sold', type: 'number' }
Expand Down
113 changes: 112 additions & 1 deletion packages/types/src/__tests__/report-schema-authoring-face.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,17 +63,33 @@
* The `expect(…)` lines below are RUNTIME and are judged by vitest. Every
* relaxation therefore carries at least one assertion of each kind, so neither
* instrument going missing can make this file vacuous on its own.
*
* ## 3. The RETIREMENT this card's own ruling ordered (maintainer, 2026-08-30)
*
* The third section pins the opposite direction: `ReportComponentSchema.dataSource`
* and `ReportBuilderSchema.dataSources` are RETIRED. Both were annotated with the
* runtime `DataSource` ADAPTER — a shape no JSON document can author — and no read
* site ever consumed either key. A retirement needs its own pin for the mirror
* reason a widening does: `?: never` compiles for every existing caller (nobody
* wrote the key), so nothing would fail if a later edit restored the adapter
* annotation or relaxed the mirror back to `z.any()`. Both halves are pinned —
* the `never` TypeScript twin AND the mirror's named refusal — because either one
* alone leaves `declared !== enforced`, which is the defect ADR-0049 names.
*/

import { describe, it, expect } from 'vitest';
import type {
ReportBuilderSchema,
ReportComponentSchema,
ReportExportConfig,
ReportExportFormat,
} from '../reports.js';
import type { ChartDataSeries } from '../data-display.js';
import { ChartDataSeriesSchema } from '../zod/data-display.zod.js';
import { ReportComponentSchema as ReportComponentZodSchema } from '../zod/reports.zod.js';
import {
ReportBuilderSchema as ReportBuilderZodSchema,
ReportComponentSchema as ReportComponentZodSchema,
} from '../zod/reports.zod.js';

/** `true` only when the two types are mutually assignable AND identical. */
type Eq<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
Expand DownExpand Up@@ -162,3 +178,98 @@ describe('objectui#6121 — ChartDataSeries declares the per-series family overr
expect(ChartDataSeriesSchema.parse(plain).type).toBeUndefined();
});
});

describe('objectui#6121 — the two report data-source keys are retired on both faces', () => {
// 3a. THE TYPE PIN. `?: never` resolves the member type to `undefined`, so
// this line fails if either key is restored to `DataSource` / `DataSource[]`
// — or to any other value type, including the `ViewData` binding whose key
// name is still an open question on this card.
type RetiredDataSource = ReportComponentSchema['dataSource'];
type RetiredDataSources = ReportBuilderSchema['dataSources'];
type _DataSourceStaysRetired = Assert<Eq<RetiredDataSource, undefined>>;
type _DataSourcesStayRetired = Assert<Eq<RetiredDataSources, undefined>>;

it('refuses an authored `dataSource` on both faces, by name', () => {
const authored = {
type: 'report' as const,
title: 'Quarterly Sales Analysis',
// The exact face `content/docs/core/report-schema.mdx` used to teach.
dataSource: { provider: 'api', read: { url: '/api/sales', method: 'GET' } },
};

// @ts-expect-error `dataSource` is retired — `?: never` admits no value
const typed: ReportComponentSchema = authored;
expect(typed).toBeTruthy();

const result = ReportComponentZodSchema.safeParse(authored);
expect(result.success).toBe(false);
// The ENVELOPE, not the fact that something failed: one issue, at this
// key's own path, reported as `invalid_type` (what `z.never()` emits) —
// and carrying the tombstone's guidance rather than zod's generic text,
// which is the half `retirementTombstone` exists for.
const issues = result.success ? [] : result.error.issues;
expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'dataSource']]);
expect(issues[0]?.message).toContain('RETIRED (objectui#6121, ADR-0049)');
});

it('refuses an authored `dataSources` on the builder, by name', () => {
const authored = {
type: 'report-builder' as const,
dataSources: [{ provider: 'api', read: { url: '/api/sales' } }],
};

// @ts-expect-error `dataSources` is retired — `?: never` admits no value
const typed: ReportBuilderSchema = authored;
expect(typed).toBeTruthy();

const result = ReportBuilderZodSchema.safeParse(authored);
expect(result.success).toBe(false);
const issues = result.success ? [] : result.error.issues;
expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'dataSources']]);
expect(issues[0]?.message).toContain('RETIRED (objectui#6121, ADR-0049)');
});

// 3b. CONTROLS, in the same run. Two zeros above need two things that fire:
// without these, a mirror that refused EVERYTHING would read as a pass, and
// so would a `.safeParse` that had stopped being called at all.
it('the same report without the retired key still parses, and the row array is untouched', () => {
const report: ReportComponentSchema = {
type: 'report',
title: 'Quarterly Sales Analysis',
// `data` is the report ROW array — a live key with a live read
// (`LegacyReportRenderer` reads `data.length` / `data.map`). It is NOT
// the retired binding, and this control is what keeps the retirement
// above from reading as "reports refuse data".
data: [{ region: 'EMEA', revenue: 1 }],
};
const result = ReportComponentZodSchema.safeParse(report);
expect(result.success).toBe(true);
expect(result.success && result.data.data).toHaveLength(1);

const builder: ReportBuilderSchema = { type: 'report-builder', showPreview: true };
expect(ReportBuilderZodSchema.safeParse(builder).success).toBe(true);
});

// 3c. The guidance reaches the OTHER author-facing channel too — the
// `.describe()` metadata that feeds generated JSON Schema and the docs
// surface. One string, two channels, so they cannot drift apart.
it('publishes the retirement guidance as schema metadata', () => {
const shapeOf = (schema: { shape: Record<string, { description?: string }> }) => schema.shape;
const componentDescribe = shapeOf(
ReportComponentZodSchema as unknown as { shape: Record<string, { description?: string }> },
).dataSource?.description;
const builderDescribe = shapeOf(
ReportBuilderZodSchema as unknown as { shape: Record<string, { description?: string }> },
).dataSources?.description;

expect(componentDescribe).toContain('RETIRED (objectui#6121, ADR-0049)');
expect(builderDescribe).toContain('RETIRED (objectui#6121, ADR-0049)');
// Control for the reader itself: a NON-retired member's description is
// still its own noun, so the two hits above are not "every key says
// RETIRED".
expect(
shapeOf(ReportComponentZodSchema as unknown as { shape: Record<string, { description?: string }> })
.title?.description,
).toBe('Report title');
});
});
49 changes: 44 additions & 5 deletions packages/types/src/reports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,6 @@ import type { z } from 'zod';
import type { ReportType as SpecReportType } from '@objectstack/spec/ui';
import type { BaseSchema, SchemaNode } from './base.js';
import type { ChartSchema } from './data-display.js';
import type { DataSource } from './data.js';

/**
* Report Export Format
Expand DownExpand Up@@ -375,9 +374,37 @@ export interface ReportComponentSchema extends BaseSchema {
reportType?: ReportType;

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

/**
* Report fields
Expand DownExpand Up@@ -494,9 +521,21 @@ export interface ReportBuilderSchema extends BaseSchema {
report?: ReportComponentSchema;

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

/**
* Available fields
Expand Down
25 changes: 22 additions & 3 deletions packages/types/src/zod/reports.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,7 @@
import { z } from 'zod';
import { BaseSchema, SchemaNodeSchema } from './base.zod.js';
import { ChartSchema } from './data-display.zod.js';
import { handlerKeyRefusal } from './tombstone.zod.js';
import { handlerKeyRefusal, retirementTombstone } from './tombstone.zod.js';

/**
* Report Export Format Schema
Expand DownExpand Up@@ -143,7 +143,17 @@ export const ReportComponentSchema = BaseSchema.extend({
type: z.literal('report'),
title: z.string().optional().describe('Report title'),
description: z.string().optional().describe('Report description'),
dataSource: z.any().optional().describe('Data source configuration'),
// RETIRED (objectui#6121, maintainer ruling of 2026-08-30, decision batch #8;
// ADR-0049 enforce-or-remove). The TS twin is `dataSource?: never`; the key
// stays DECLARED so an authored value is refused BY NAME instead of being
// waved through by `z.any()` and then read by nobody.
dataSource: retirementTombstone(
'Data source configuration — RETIRED (objectui#6121, ADR-0049). The key was declared as the ' +
'runtime `DataSource` ADAPTER (`find(resource, params)`), which JSON has no value for, and ' +
'no renderer ever read it off a report schema: the report renderers take their adapter as a ' +
'React prop or from `SchemaRendererContext`. Bind a report through the semantic-layer ' +
'`dataset` form (ADR-0021); a legacy presentation report receives its rows under `data`.',
),
fields: z.array(ReportFieldSchema).optional().describe('Report fields'),
filters: z.array(ReportFilterSchema).optional().describe('Report filters'),
groupBy: z.array(ReportGroupBySchema).optional().describe('Group by configuration'),
Expand All@@ -165,7 +175,16 @@ export const ReportComponentSchema = BaseSchema.extend({
export const ReportBuilderSchema = BaseSchema.extend({
type: z.literal('report-builder'),
report: ReportComponentSchema.optional().describe('Initial report configuration'),
dataSources: z.array(z.any()).optional().describe('Available data sources'),
// RETIRED with `ReportComponentSchema.dataSource` above (objectui#6121), one
// degree further from a reader: no renderer is registered for
// `report-builder`, the same measurement that retired the two handler keys
// below (objectui#7344).
dataSources: retirementTombstone(
'Available data sources — RETIRED (objectui#6121, ADR-0049). No renderer is registered for ' +
'`report-builder`, so nothing could ever read this key, and it was declared as an array of ' +
'the runtime `DataSource` ADAPTER, which JSON has no value for. Bind a report through the ' +
'semantic-layer `dataset` form (ADR-0021).',
),
availableFields: z.array(ReportFieldSchema).optional().describe('Available fields'),
showPreview: z.boolean().optional().describe('Show preview'),
// RETIRED (objectui#7344, the objectui#6182 ruling in the objectui#6124 shape):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .changeset/6121-retire-report-data-source.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
---
'@object-ui/types': minor
---

The two report data-source keys are retired on both faces (objectui#6121,
maintainer ruling of 2026-08-30, decision batch #8 — option A's retirement half;
ADR-0049 enforce-or-remove).

**The accept set of a published validator moves** (`@object-ui/types/zod`):

- `ReportComponentSchema.dataSource` was `z.any().optional()`, so any JSON value
parsed green and was then read by nobody.
- `ReportBuilderSchema.dataSources` was `z.array(z.any()).optional()`, on a node
type no renderer is registered for at all.

Both now carry `retirementTombstone(...)`: an authored value is refused at the
key's own path with `code: 'invalid_type'` and a message that names the key, says
why it is retired and points at the spelling that runs. Nothing that used to be
refused parses green.

**The TypeScript face** — both keys become `?: never` rather than being deleted,
so an author who still writes one gets a `tsc` error at the authoring site
instead of a silently stripped key. They were annotated `DataSource` /
`DataSource[]`, the runtime ADAPTER interface (`find(resource, params)`), which
no JSON document can author; that mis-annotation is the defect objectui#6121 was
filed for, since every example on `content/docs/core/report-schema.mdx` authored
a config object against it.

**Why this is a retirement and not a rename.** No read site consumed either key:
`@object-ui/plugin-report`'s `ReportRenderer` takes its adapter as a React prop
or from `SchemaRendererContext`, never off `schema.dataSource`, and the live
9.0 path binds a semantic-layer `dataset` (ADR-0021). Authored occurrences
measured zero in this repo and in the sibling `objectstack` checkout, whose
report metadata binds `dataset` throughout — the ruling's own deprecation-window
exit criterion. A stored document that still carries the key now fails loudly at
`safeParse` instead of being accepted and ignored; drop the key, and bind the
report through `dataset`.

The replacement binding key the ruling names (`data?: ViewData`) is deliberately
NOT declared here, and is escalated on objectui#6121: `data` is already a live
key on `ReportComponentSchema` — the report ROW array, read by
`LegacyReportRenderer` as `data.length` / `data.map` — so declaring the binding
under that name would put two authoring contracts on one key inside one
renderer.

Pinned in `packages/types/src/__tests__/report-schema-authoring-face.test.ts`:
the `never` twins, the named refusals with their issue envelope, the `.describe()`
metadata channel, and controls that a report without the key still parses.
27 changes: 12 additions & 15 deletions content/docs/core/report-schema.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,18 @@ complete list.
| `type` | `'report'` | Component type identifier (required) |
| `title` | `string` | Report title |
| `description` | `string` | Report description |
| `dataSource` | `DataSource` | Data source configuration |

> **Retired (objectui#6121):** `ReportComponentSchema.dataSource` and
> `ReportBuilderSchema.dataSources` used to be documented and declared here.
> Both were annotated with `DataSource`, the runtime **adapter** interface
> (`find(resource, params)`), which no JSON document can author — and no
> renderer ever read either key off a schema: the report renderers take their
> adapter as a React prop or from the renderer context. Both keys are now
> `never` on the TypeScript face and are refused **by name** by the published
> validator, so an authored value fails loudly instead of being accepted and
> ignored. A report binds its data through the semantic-layer `dataset` form
> (ADR-0021); a legacy presentation report receives already-fetched rows under
> `data`.

### Report Fields

Expand DownExpand Up@@ -227,15 +238,6 @@ const comprehensiveReport: ReportComponentSchema = {
title: 'Quarterly Sales Analysis',
description: 'Comprehensive sales performance analysis by region and product',

// Data source
dataSource: {
provider: 'api',
read: {
url: '/api/sales',
method: 'GET'
}
},

// Report fields
fields: [
{
Expand DownExpand Up@@ -406,11 +408,6 @@ const builder: ReportBuilderSchema = {
title: 'Untitled Report'
},

dataSources: [
{ provider: 'api', read: { url: '/api/sales' } },
{ provider: 'api', read: { url: '/api/customers' } }
],

availableFields: [
{ name: 'revenue', label: 'Revenue', type: 'number' },
{ name: 'units', label: 'Units Sold', type: 'number' }
Expand Down
113 changes: 112 additions & 1 deletion packages/types/src/__tests__/report-schema-authoring-face.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,17 +63,33 @@
* The `expect(…)` lines below are RUNTIME and are judged by vitest. Every
* relaxation therefore carries at least one assertion of each kind, so neither
* instrument going missing can make this file vacuous on its own.
*
* ## 3. The RETIREMENT this card's own ruling ordered (maintainer, 2026-08-30)
*
* The third section pins the opposite direction: `ReportComponentSchema.dataSource`
* and `ReportBuilderSchema.dataSources` are RETIRED. Both were annotated with the
* runtime `DataSource` ADAPTER — a shape no JSON document can author — and no read
* site ever consumed either key. A retirement needs its own pin for the mirror
* reason a widening does: `?: never` compiles for every existing caller (nobody
* wrote the key), so nothing would fail if a later edit restored the adapter
* annotation or relaxed the mirror back to `z.any()`. Both halves are pinned —
* the `never` TypeScript twin AND the mirror's named refusal — because either one
* alone leaves `declared !== enforced`, which is the defect ADR-0049 names.
*/

import { describe, it, expect } from 'vitest';
import type {
ReportBuilderSchema,
ReportComponentSchema,
ReportExportConfig,
ReportExportFormat,
} from '../reports.js';
import type { ChartDataSeries } from '../data-display.js';
import { ChartDataSeriesSchema } from '../zod/data-display.zod.js';
import { ReportComponentSchema as ReportComponentZodSchema } from '../zod/reports.zod.js';
import {
ReportBuilderSchema as ReportBuilderZodSchema,
ReportComponentSchema as ReportComponentZodSchema,
} from '../zod/reports.zod.js';

/** `true` only when the two types are mutually assignable AND identical. */
type Eq<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
Expand DownExpand Up@@ -162,3 +178,98 @@ describe('objectui#6121 — ChartDataSeries declares the per-series family overr
expect(ChartDataSeriesSchema.parse(plain).type).toBeUndefined();
});
});

describe('objectui#6121 — the two report data-source keys are retired on both faces', () => {
// 3a. THE TYPE PIN. `?: never` resolves the member type to `undefined`, so
// this line fails if either key is restored to `DataSource` / `DataSource[]`
// — or to any other value type, including the `ViewData` binding whose key
// name is still an open question on this card.
type RetiredDataSource = ReportComponentSchema['dataSource'];
type RetiredDataSources = ReportBuilderSchema['dataSources'];
type _DataSourceStaysRetired = Assert<Eq<RetiredDataSource, undefined>>;
type _DataSourcesStayRetired = Assert<Eq<RetiredDataSources, undefined>>;

it('refuses an authored `dataSource` on both faces, by name', () => {
const authored = {
type: 'report' as const,
title: 'Quarterly Sales Analysis',
// The exact face `content/docs/core/report-schema.mdx` used to teach.
dataSource: { provider: 'api', read: { url: '/api/sales', method: 'GET' } },
};

// @ts-expect-error `dataSource` is retired — `?: never` admits no value
const typed: ReportComponentSchema = authored;
expect(typed).toBeTruthy();

const result = ReportComponentZodSchema.safeParse(authored);
expect(result.success).toBe(false);
// The ENVELOPE, not the fact that something failed: one issue, at this
// key's own path, reported as `invalid_type` (what `z.never()` emits) —
// and carrying the tombstone's guidance rather than zod's generic text,
// which is the half `retirementTombstone` exists for.
const issues = result.success ? [] : result.error.issues;
expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'dataSource']]);
expect(issues[0]?.message).toContain('RETIRED (objectui#6121, ADR-0049)');
});

it('refuses an authored `dataSources` on the builder, by name', () => {
const authored = {
type: 'report-builder' as const,
dataSources: [{ provider: 'api', read: { url: '/api/sales' } }],
};

// @ts-expect-error `dataSources` is retired — `?: never` admits no value
const typed: ReportBuilderSchema = authored;
expect(typed).toBeTruthy();

const result = ReportBuilderZodSchema.safeParse(authored);
expect(result.success).toBe(false);
const issues = result.success ? [] : result.error.issues;
expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'dataSources']]);
expect(issues[0]?.message).toContain('RETIRED (objectui#6121, ADR-0049)');
});

// 3b. CONTROLS, in the same run. Two zeros above need two things that fire:
// without these, a mirror that refused EVERYTHING would read as a pass, and
// so would a `.safeParse` that had stopped being called at all.
it('the same report without the retired key still parses, and the row array is untouched', () => {
const report: ReportComponentSchema = {
type: 'report',
title: 'Quarterly Sales Analysis',
// `data` is the report ROW array — a live key with a live read
// (`LegacyReportRenderer` reads `data.length` / `data.map`). It is NOT
// the retired binding, and this control is what keeps the retirement
// above from reading as "reports refuse data".
data: [{ region: 'EMEA', revenue: 1 }],
};
const result = ReportComponentZodSchema.safeParse(report);
expect(result.success).toBe(true);
expect(result.success && result.data.data).toHaveLength(1);

const builder: ReportBuilderSchema = { type: 'report-builder', showPreview: true };
expect(ReportBuilderZodSchema.safeParse(builder).success).toBe(true);
});

// 3c. The guidance reaches the OTHER author-facing channel too — the
// `.describe()` metadata that feeds generated JSON Schema and the docs
// surface. One string, two channels, so they cannot drift apart.
it('publishes the retirement guidance as schema metadata', () => {
const shapeOf = (schema: { shape: Record<string, { description?: string }> }) => schema.shape;
const componentDescribe = shapeOf(
ReportComponentZodSchema as unknown as { shape: Record<string, { description?: string }> },
).dataSource?.description;
const builderDescribe = shapeOf(
ReportBuilderZodSchema as unknown as { shape: Record<string, { description?: string }> },
).dataSources?.description;

expect(componentDescribe).toContain('RETIRED (objectui#6121, ADR-0049)');
expect(builderDescribe).toContain('RETIRED (objectui#6121, ADR-0049)');
// Control for the reader itself: a NON-retired member's description is
// still its own noun, so the two hits above are not "every key says
// RETIRED".
expect(
shapeOf(ReportComponentZodSchema as unknown as { shape: Record<string, { description?: string }> })
.title?.description,
).toBe('Report title');
});
});
49 changes: 44 additions & 5 deletions packages/types/src/reports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,6 @@ import type { z } from 'zod';
import type { ReportType as SpecReportType } from '@objectstack/spec/ui';
import type { BaseSchema, SchemaNode } from './base.js';
import type { ChartSchema } from './data-display.js';
import type { DataSource } from './data.js';

/**
* Report Export Format
Expand DownExpand Up@@ -375,9 +374,37 @@ export interface ReportComponentSchema extends BaseSchema {
reportType?: ReportType;

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

/**
* Report fields
Expand DownExpand Up@@ -494,9 +521,21 @@ export interface ReportBuilderSchema extends BaseSchema {
report?: ReportComponentSchema;

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

/**
* Available fields
Expand Down
25 changes: 22 additions & 3 deletions packages/types/src/zod/reports.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,7 @@
import { z } from 'zod';
import { BaseSchema, SchemaNodeSchema } from './base.zod.js';
import { ChartSchema } from './data-display.zod.js';
import { handlerKeyRefusal } from './tombstone.zod.js';
import { handlerKeyRefusal, retirementTombstone } from './tombstone.zod.js';

/**
* Report Export Format Schema
Expand DownExpand Up@@ -143,7 +143,17 @@ export const ReportComponentSchema = BaseSchema.extend({
type: z.literal('report'),
title: z.string().optional().describe('Report title'),
description: z.string().optional().describe('Report description'),
dataSource: z.any().optional().describe('Data source configuration'),
// RETIRED (objectui#6121, maintainer ruling of 2026-08-30, decision batch #8;
// ADR-0049 enforce-or-remove). The TS twin is `dataSource?: never`; the key
// stays DECLARED so an authored value is refused BY NAME instead of being
// waved through by `z.any()` and then read by nobody.
dataSource: retirementTombstone(
'Data source configuration — RETIRED (objectui#6121, ADR-0049). The key was declared as the ' +
'runtime `DataSource` ADAPTER (`find(resource, params)`), which JSON has no value for, and ' +
'no renderer ever read it off a report schema: the report renderers take their adapter as a ' +
'React prop or from `SchemaRendererContext`. Bind a report through the semantic-layer ' +
'`dataset` form (ADR-0021); a legacy presentation report receives its rows under `data`.',
),
fields: z.array(ReportFieldSchema).optional().describe('Report fields'),
filters: z.array(ReportFilterSchema).optional().describe('Report filters'),
groupBy: z.array(ReportGroupBySchema).optional().describe('Group by configuration'),
Expand All@@ -165,7 +175,16 @@ export const ReportComponentSchema = BaseSchema.extend({
export const ReportBuilderSchema = BaseSchema.extend({
type: z.literal('report-builder'),
report: ReportComponentSchema.optional().describe('Initial report configuration'),
dataSources: z.array(z.any()).optional().describe('Available data sources'),
// RETIRED with `ReportComponentSchema.dataSource` above (objectui#6121), one
// degree further from a reader: no renderer is registered for
// `report-builder`, the same measurement that retired the two handler keys
// below (objectui#7344).
dataSources: retirementTombstone(
'Available data sources — RETIRED (objectui#6121, ADR-0049). No renderer is registered for ' +
'`report-builder`, so nothing could ever read this key, and it was declared as an array of ' +
'the runtime `DataSource` ADAPTER, which JSON has no value for. Bind a report through the ' +
'semantic-layer `dataset` form (ADR-0021).',
),
availableFields: z.array(ReportFieldSchema).optional().describe('Available fields'),
showPreview: z.boolean().optional().describe('Show preview'),
// RETIRED (objectui#7344, the objectui#6182 ruling in the objectui#6124 shape):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .changeset/6121-retire-report-data-source.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
---
'@object-ui/types': minor
---

The two report data-source keys are retired on both faces (objectui#6121,
maintainer ruling of 2026-08-30, decision batch #8 — option A's retirement half;
ADR-0049 enforce-or-remove).

**The accept set of a published validator moves** (`@object-ui/types/zod`):

- `ReportComponentSchema.dataSource` was `z.any().optional()`, so any JSON value
parsed green and was then read by nobody.
- `ReportBuilderSchema.dataSources` was `z.array(z.any()).optional()`, on a node
type no renderer is registered for at all.

Both now carry `retirementTombstone(...)`: an authored value is refused at the
key's own path with `code: 'invalid_type'` and a message that names the key, says
why it is retired and points at the spelling that runs. Nothing that used to be
refused parses green.

**The TypeScript face** — both keys become `?: never` rather than being deleted,
so an author who still writes one gets a `tsc` error at the authoring site
instead of a silently stripped key. They were annotated `DataSource` /
`DataSource[]`, the runtime ADAPTER interface (`find(resource, params)`), which
no JSON document can author; that mis-annotation is the defect objectui#6121 was
filed for, since every example on `content/docs/core/report-schema.mdx` authored
a config object against it.

**Why this is a retirement and not a rename.** No read site consumed either key:
`@object-ui/plugin-report`'s `ReportRenderer` takes its adapter as a React prop
or from `SchemaRendererContext`, never off `schema.dataSource`, and the live
9.0 path binds a semantic-layer `dataset` (ADR-0021). Authored occurrences
measured zero in this repo and in the sibling `objectstack` checkout, whose
report metadata binds `dataset` throughout — the ruling's own deprecation-window
exit criterion. A stored document that still carries the key now fails loudly at
`safeParse` instead of being accepted and ignored; drop the key, and bind the
report through `dataset`.

The replacement binding key the ruling names (`data?: ViewData`) is deliberately
NOT declared here, and is escalated on objectui#6121: `data` is already a live
key on `ReportComponentSchema` — the report ROW array, read by
`LegacyReportRenderer` as `data.length` / `data.map` — so declaring the binding
under that name would put two authoring contracts on one key inside one
renderer.

Pinned in `packages/types/src/__tests__/report-schema-authoring-face.test.ts`:
the `never` twins, the named refusals with their issue envelope, the `.describe()`
metadata channel, and controls that a report without the key still parses.
27 changes: 12 additions & 15 deletions content/docs/core/report-schema.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,18 @@ complete list.
| `type` | `'report'` | Component type identifier (required) |
| `title` | `string` | Report title |
| `description` | `string` | Report description |
| `dataSource` | `DataSource` | Data source configuration |

> **Retired (objectui#6121):** `ReportComponentSchema.dataSource` and
> `ReportBuilderSchema.dataSources` used to be documented and declared here.
> Both were annotated with `DataSource`, the runtime **adapter** interface
> (`find(resource, params)`), which no JSON document can author — and no
> renderer ever read either key off a schema: the report renderers take their
> adapter as a React prop or from the renderer context. Both keys are now
> `never` on the TypeScript face and are refused **by name** by the published
> validator, so an authored value fails loudly instead of being accepted and
> ignored. A report binds its data through the semantic-layer `dataset` form
> (ADR-0021); a legacy presentation report receives already-fetched rows under
> `data`.

### Report Fields

Expand DownExpand Up@@ -227,15 +238,6 @@ const comprehensiveReport: ReportComponentSchema = {
title: 'Quarterly Sales Analysis',
description: 'Comprehensive sales performance analysis by region and product',

// Data source
dataSource: {
provider: 'api',
read: {
url: '/api/sales',
method: 'GET'
}
},

// Report fields
fields: [
{
Expand DownExpand Up@@ -406,11 +408,6 @@ const builder: ReportBuilderSchema = {
title: 'Untitled Report'
},

dataSources: [
{ provider: 'api', read: { url: '/api/sales' } },
{ provider: 'api', read: { url: '/api/customers' } }
],

availableFields: [
{ name: 'revenue', label: 'Revenue', type: 'number' },
{ name: 'units', label: 'Units Sold', type: 'number' }
Expand Down
113 changes: 112 additions & 1 deletion packages/types/src/__tests__/report-schema-authoring-face.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,17 +63,33 @@
* The `expect(…)` lines below are RUNTIME and are judged by vitest. Every
* relaxation therefore carries at least one assertion of each kind, so neither
* instrument going missing can make this file vacuous on its own.
*
* ## 3. The RETIREMENT this card's own ruling ordered (maintainer, 2026-08-30)
*
* The third section pins the opposite direction: `ReportComponentSchema.dataSource`
* and `ReportBuilderSchema.dataSources` are RETIRED. Both were annotated with the
* runtime `DataSource` ADAPTER — a shape no JSON document can author — and no read
* site ever consumed either key. A retirement needs its own pin for the mirror
* reason a widening does: `?: never` compiles for every existing caller (nobody
* wrote the key), so nothing would fail if a later edit restored the adapter
* annotation or relaxed the mirror back to `z.any()`. Both halves are pinned —
* the `never` TypeScript twin AND the mirror's named refusal — because either one
* alone leaves `declared !== enforced`, which is the defect ADR-0049 names.
*/

import { describe, it, expect } from 'vitest';
import type {
ReportBuilderSchema,
ReportComponentSchema,
ReportExportConfig,
ReportExportFormat,
} from '../reports.js';
import type { ChartDataSeries } from '../data-display.js';
import { ChartDataSeriesSchema } from '../zod/data-display.zod.js';
import { ReportComponentSchema as ReportComponentZodSchema } from '../zod/reports.zod.js';
import {
ReportBuilderSchema as ReportBuilderZodSchema,
ReportComponentSchema as ReportComponentZodSchema,
} from '../zod/reports.zod.js';

/** `true` only when the two types are mutually assignable AND identical. */
type Eq<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
Expand DownExpand Up@@ -162,3 +178,98 @@ describe('objectui#6121 — ChartDataSeries declares the per-series family overr
expect(ChartDataSeriesSchema.parse(plain).type).toBeUndefined();
});
});

describe('objectui#6121 — the two report data-source keys are retired on both faces', () => {
// 3a. THE TYPE PIN. `?: never` resolves the member type to `undefined`, so
// this line fails if either key is restored to `DataSource` / `DataSource[]`
// — or to any other value type, including the `ViewData` binding whose key
// name is still an open question on this card.
type RetiredDataSource = ReportComponentSchema['dataSource'];
type RetiredDataSources = ReportBuilderSchema['dataSources'];
type _DataSourceStaysRetired = Assert<Eq<RetiredDataSource, undefined>>;
type _DataSourcesStayRetired = Assert<Eq<RetiredDataSources, undefined>>;

it('refuses an authored `dataSource` on both faces, by name', () => {
const authored = {
type: 'report' as const,
title: 'Quarterly Sales Analysis',
// The exact face `content/docs/core/report-schema.mdx` used to teach.
dataSource: { provider: 'api', read: { url: '/api/sales', method: 'GET' } },
};

// @ts-expect-error `dataSource` is retired — `?: never` admits no value
const typed: ReportComponentSchema = authored;
expect(typed).toBeTruthy();

const result = ReportComponentZodSchema.safeParse(authored);
expect(result.success).toBe(false);
// The ENVELOPE, not the fact that something failed: one issue, at this
// key's own path, reported as `invalid_type` (what `z.never()` emits) —
// and carrying the tombstone's guidance rather than zod's generic text,
// which is the half `retirementTombstone` exists for.
const issues = result.success ? [] : result.error.issues;
expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'dataSource']]);
expect(issues[0]?.message).toContain('RETIRED (objectui#6121, ADR-0049)');
});

it('refuses an authored `dataSources` on the builder, by name', () => {
const authored = {
type: 'report-builder' as const,
dataSources: [{ provider: 'api', read: { url: '/api/sales' } }],
};

// @ts-expect-error `dataSources` is retired — `?: never` admits no value
const typed: ReportBuilderSchema = authored;
expect(typed).toBeTruthy();

const result = ReportBuilderZodSchema.safeParse(authored);
expect(result.success).toBe(false);
const issues = result.success ? [] : result.error.issues;
expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'dataSources']]);
expect(issues[0]?.message).toContain('RETIRED (objectui#6121, ADR-0049)');
});

// 3b. CONTROLS, in the same run. Two zeros above need two things that fire:
// without these, a mirror that refused EVERYTHING would read as a pass, and
// so would a `.safeParse` that had stopped being called at all.
it('the same report without the retired key still parses, and the row array is untouched', () => {
const report: ReportComponentSchema = {
type: 'report',
title: 'Quarterly Sales Analysis',
// `data` is the report ROW array — a live key with a live read
// (`LegacyReportRenderer` reads `data.length` / `data.map`). It is NOT
// the retired binding, and this control is what keeps the retirement
// above from reading as "reports refuse data".
data: [{ region: 'EMEA', revenue: 1 }],
};
const result = ReportComponentZodSchema.safeParse(report);
expect(result.success).toBe(true);
expect(result.success && result.data.data).toHaveLength(1);

const builder: ReportBuilderSchema = { type: 'report-builder', showPreview: true };
expect(ReportBuilderZodSchema.safeParse(builder).success).toBe(true);
});

// 3c. The guidance reaches the OTHER author-facing channel too — the
// `.describe()` metadata that feeds generated JSON Schema and the docs
// surface. One string, two channels, so they cannot drift apart.
it('publishes the retirement guidance as schema metadata', () => {
const shapeOf = (schema: { shape: Record<string, { description?: string }> }) => schema.shape;
const componentDescribe = shapeOf(
ReportComponentZodSchema as unknown as { shape: Record<string, { description?: string }> },
).dataSource?.description;
const builderDescribe = shapeOf(
ReportBuilderZodSchema as unknown as { shape: Record<string, { description?: string }> },
).dataSources?.description;

expect(componentDescribe).toContain('RETIRED (objectui#6121, ADR-0049)');
expect(builderDescribe).toContain('RETIRED (objectui#6121, ADR-0049)');
// Control for the reader itself: a NON-retired member's description is
// still its own noun, so the two hits above are not "every key says
// RETIRED".
expect(
shapeOf(ReportComponentZodSchema as unknown as { shape: Record<string, { description?: string }> })
.title?.description,
).toBe('Report title');
});
});
49 changes: 44 additions & 5 deletions packages/types/src/reports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,6 @@ import type { z } from 'zod';
import type { ReportType as SpecReportType } from '@objectstack/spec/ui';
import type { BaseSchema, SchemaNode } from './base.js';
import type { ChartSchema } from './data-display.js';
import type { DataSource } from './data.js';

/**
* Report Export Format
Expand DownExpand Up@@ -375,9 +374,37 @@ export interface ReportComponentSchema extends BaseSchema {
reportType?: ReportType;

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

/**
* Report fields
Expand DownExpand Up@@ -494,9 +521,21 @@ export interface ReportBuilderSchema extends BaseSchema {
report?: ReportComponentSchema;

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

/**
* Available fields
Expand Down
25 changes: 22 additions & 3 deletions packages/types/src/zod/reports.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,7 @@
import { z } from 'zod';
import { BaseSchema, SchemaNodeSchema } from './base.zod.js';
import { ChartSchema } from './data-display.zod.js';
import { handlerKeyRefusal } from './tombstone.zod.js';
import { handlerKeyRefusal, retirementTombstone } from './tombstone.zod.js';

/**
* Report Export Format Schema
Expand DownExpand Up@@ -143,7 +143,17 @@ export const ReportComponentSchema = BaseSchema.extend({
type: z.literal('report'),
title: z.string().optional().describe('Report title'),
description: z.string().optional().describe('Report description'),
dataSource: z.any().optional().describe('Data source configuration'),
// RETIRED (objectui#6121, maintainer ruling of 2026-08-30, decision batch #8;
// ADR-0049 enforce-or-remove). The TS twin is `dataSource?: never`; the key
// stays DECLARED so an authored value is refused BY NAME instead of being
// waved through by `z.any()` and then read by nobody.
dataSource: retirementTombstone(
'Data source configuration — RETIRED (objectui#6121, ADR-0049). The key was declared as the ' +
'runtime `DataSource` ADAPTER (`find(resource, params)`), which JSON has no value for, and ' +
'no renderer ever read it off a report schema: the report renderers take their adapter as a ' +
'React prop or from `SchemaRendererContext`. Bind a report through the semantic-layer ' +
'`dataset` form (ADR-0021); a legacy presentation report receives its rows under `data`.',
),
fields: z.array(ReportFieldSchema).optional().describe('Report fields'),
filters: z.array(ReportFilterSchema).optional().describe('Report filters'),
groupBy: z.array(ReportGroupBySchema).optional().describe('Group by configuration'),
Expand All@@ -165,7 +175,16 @@ export const ReportComponentSchema = BaseSchema.extend({
export const ReportBuilderSchema = BaseSchema.extend({
type: z.literal('report-builder'),
report: ReportComponentSchema.optional().describe('Initial report configuration'),
dataSources: z.array(z.any()).optional().describe('Available data sources'),
// RETIRED with `ReportComponentSchema.dataSource` above (objectui#6121), one
// degree further from a reader: no renderer is registered for
// `report-builder`, the same measurement that retired the two handler keys
// below (objectui#7344).
dataSources: retirementTombstone(
'Available data sources — RETIRED (objectui#6121, ADR-0049). No renderer is registered for ' +
'`report-builder`, so nothing could ever read this key, and it was declared as an array of ' +
'the runtime `DataSource` ADAPTER, which JSON has no value for. Bind a report through the ' +
'semantic-layer `dataset` form (ADR-0021).',
),
availableFields: z.array(ReportFieldSchema).optional().describe('Available fields'),
showPreview: z.boolean().optional().describe('Show preview'),
// RETIRED (objectui#7344, the objectui#6182 ruling in the objectui#6124 shape):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .changeset/6121-retire-report-data-source.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
---
'@object-ui/types': minor
---

The two report data-source keys are retired on both faces (objectui#6121,
maintainer ruling of 2026-08-30, decision batch #8 — option A's retirement half;
ADR-0049 enforce-or-remove).

**The accept set of a published validator moves** (`@object-ui/types/zod`):

- `ReportComponentSchema.dataSource` was `z.any().optional()`, so any JSON value
parsed green and was then read by nobody.
- `ReportBuilderSchema.dataSources` was `z.array(z.any()).optional()`, on a node
type no renderer is registered for at all.

Both now carry `retirementTombstone(...)`: an authored value is refused at the
key's own path with `code: 'invalid_type'` and a message that names the key, says
why it is retired and points at the spelling that runs. Nothing that used to be
refused parses green.

**The TypeScript face** — both keys become `?: never` rather than being deleted,
so an author who still writes one gets a `tsc` error at the authoring site
instead of a silently stripped key. They were annotated `DataSource` /
`DataSource[]`, the runtime ADAPTER interface (`find(resource, params)`), which
no JSON document can author; that mis-annotation is the defect objectui#6121 was
filed for, since every example on `content/docs/core/report-schema.mdx` authored
a config object against it.

**Why this is a retirement and not a rename.** No read site consumed either key:
`@object-ui/plugin-report`'s `ReportRenderer` takes its adapter as a React prop
or from `SchemaRendererContext`, never off `schema.dataSource`, and the live
9.0 path binds a semantic-layer `dataset` (ADR-0021). Authored occurrences
measured zero in this repo and in the sibling `objectstack` checkout, whose
report metadata binds `dataset` throughout — the ruling's own deprecation-window
exit criterion. A stored document that still carries the key now fails loudly at
`safeParse` instead of being accepted and ignored; drop the key, and bind the
report through `dataset`.

The replacement binding key the ruling names (`data?: ViewData`) is deliberately
NOT declared here, and is escalated on objectui#6121: `data` is already a live
key on `ReportComponentSchema` — the report ROW array, read by
`LegacyReportRenderer` as `data.length` / `data.map` — so declaring the binding
under that name would put two authoring contracts on one key inside one
renderer.

Pinned in `packages/types/src/__tests__/report-schema-authoring-face.test.ts`:
the `never` twins, the named refusals with their issue envelope, the `.describe()`
metadata channel, and controls that a report without the key still parses.
27 changes: 12 additions & 15 deletions content/docs/core/report-schema.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,18 @@ complete list.
| `type` | `'report'` | Component type identifier (required) |
| `title` | `string` | Report title |
| `description` | `string` | Report description |
| `dataSource` | `DataSource` | Data source configuration |

> **Retired (objectui#6121):** `ReportComponentSchema.dataSource` and
> `ReportBuilderSchema.dataSources` used to be documented and declared here.
> Both were annotated with `DataSource`, the runtime **adapter** interface
> (`find(resource, params)`), which no JSON document can author — and no
> renderer ever read either key off a schema: the report renderers take their
> adapter as a React prop or from the renderer context. Both keys are now
> `never` on the TypeScript face and are refused **by name** by the published
> validator, so an authored value fails loudly instead of being accepted and
> ignored. A report binds its data through the semantic-layer `dataset` form
> (ADR-0021); a legacy presentation report receives already-fetched rows under
> `data`.

### Report Fields

Expand DownExpand Up@@ -227,15 +238,6 @@ const comprehensiveReport: ReportComponentSchema = {
title: 'Quarterly Sales Analysis',
description: 'Comprehensive sales performance analysis by region and product',

// Data source
dataSource: {
provider: 'api',
read: {
url: '/api/sales',
method: 'GET'
}
},

// Report fields
fields: [
{
Expand DownExpand Up@@ -406,11 +408,6 @@ const builder: ReportBuilderSchema = {
title: 'Untitled Report'
},

dataSources: [
{ provider: 'api', read: { url: '/api/sales' } },
{ provider: 'api', read: { url: '/api/customers' } }
],

availableFields: [
{ name: 'revenue', label: 'Revenue', type: 'number' },
{ name: 'units', label: 'Units Sold', type: 'number' }
Expand Down
113 changes: 112 additions & 1 deletion packages/types/src/__tests__/report-schema-authoring-face.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,17 +63,33 @@
* The `expect(…)` lines below are RUNTIME and are judged by vitest. Every
* relaxation therefore carries at least one assertion of each kind, so neither
* instrument going missing can make this file vacuous on its own.
*
* ## 3. The RETIREMENT this card's own ruling ordered (maintainer, 2026-08-30)
*
* The third section pins the opposite direction: `ReportComponentSchema.dataSource`
* and `ReportBuilderSchema.dataSources` are RETIRED. Both were annotated with the
* runtime `DataSource` ADAPTER — a shape no JSON document can author — and no read
* site ever consumed either key. A retirement needs its own pin for the mirror
* reason a widening does: `?: never` compiles for every existing caller (nobody
* wrote the key), so nothing would fail if a later edit restored the adapter
* annotation or relaxed the mirror back to `z.any()`. Both halves are pinned —
* the `never` TypeScript twin AND the mirror's named refusal — because either one
* alone leaves `declared !== enforced`, which is the defect ADR-0049 names.
*/

import { describe, it, expect } from 'vitest';
import type {
ReportBuilderSchema,
ReportComponentSchema,
ReportExportConfig,
ReportExportFormat,
} from '../reports.js';
import type { ChartDataSeries } from '../data-display.js';
import { ChartDataSeriesSchema } from '../zod/data-display.zod.js';
import { ReportComponentSchema as ReportComponentZodSchema } from '../zod/reports.zod.js';
import {
ReportBuilderSchema as ReportBuilderZodSchema,
ReportComponentSchema as ReportComponentZodSchema,
} from '../zod/reports.zod.js';

/** `true` only when the two types are mutually assignable AND identical. */
type Eq<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
Expand DownExpand Up@@ -162,3 +178,98 @@ describe('objectui#6121 — ChartDataSeries declares the per-series family overr
expect(ChartDataSeriesSchema.parse(plain).type).toBeUndefined();
});
});

describe('objectui#6121 — the two report data-source keys are retired on both faces', () => {
// 3a. THE TYPE PIN. `?: never` resolves the member type to `undefined`, so
// this line fails if either key is restored to `DataSource` / `DataSource[]`
// — or to any other value type, including the `ViewData` binding whose key
// name is still an open question on this card.
type RetiredDataSource = ReportComponentSchema['dataSource'];
type RetiredDataSources = ReportBuilderSchema['dataSources'];
type _DataSourceStaysRetired = Assert<Eq<RetiredDataSource, undefined>>;
type _DataSourcesStayRetired = Assert<Eq<RetiredDataSources, undefined>>;

it('refuses an authored `dataSource` on both faces, by name', () => {
const authored = {
type: 'report' as const,
title: 'Quarterly Sales Analysis',
// The exact face `content/docs/core/report-schema.mdx` used to teach.
dataSource: { provider: 'api', read: { url: '/api/sales', method: 'GET' } },
};

// @ts-expect-error `dataSource` is retired — `?: never` admits no value
const typed: ReportComponentSchema = authored;
expect(typed).toBeTruthy();

const result = ReportComponentZodSchema.safeParse(authored);
expect(result.success).toBe(false);
// The ENVELOPE, not the fact that something failed: one issue, at this
// key's own path, reported as `invalid_type` (what `z.never()` emits) —
// and carrying the tombstone's guidance rather than zod's generic text,
// which is the half `retirementTombstone` exists for.
const issues = result.success ? [] : result.error.issues;
expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'dataSource']]);
expect(issues[0]?.message).toContain('RETIRED (objectui#6121, ADR-0049)');
});

it('refuses an authored `dataSources` on the builder, by name', () => {
const authored = {
type: 'report-builder' as const,
dataSources: [{ provider: 'api', read: { url: '/api/sales' } }],
};

// @ts-expect-error `dataSources` is retired — `?: never` admits no value
const typed: ReportBuilderSchema = authored;
expect(typed).toBeTruthy();

const result = ReportBuilderZodSchema.safeParse(authored);
expect(result.success).toBe(false);
const issues = result.success ? [] : result.error.issues;
expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'dataSources']]);
expect(issues[0]?.message).toContain('RETIRED (objectui#6121, ADR-0049)');
});

// 3b. CONTROLS, in the same run. Two zeros above need two things that fire:
// without these, a mirror that refused EVERYTHING would read as a pass, and
// so would a `.safeParse` that had stopped being called at all.
it('the same report without the retired key still parses, and the row array is untouched', () => {
const report: ReportComponentSchema = {
type: 'report',
title: 'Quarterly Sales Analysis',
// `data` is the report ROW array — a live key with a live read
// (`LegacyReportRenderer` reads `data.length` / `data.map`). It is NOT
// the retired binding, and this control is what keeps the retirement
// above from reading as "reports refuse data".
data: [{ region: 'EMEA', revenue: 1 }],
};
const result = ReportComponentZodSchema.safeParse(report);
expect(result.success).toBe(true);
expect(result.success && result.data.data).toHaveLength(1);

const builder: ReportBuilderSchema = { type: 'report-builder', showPreview: true };
expect(ReportBuilderZodSchema.safeParse(builder).success).toBe(true);
});

// 3c. The guidance reaches the OTHER author-facing channel too — the
// `.describe()` metadata that feeds generated JSON Schema and the docs
// surface. One string, two channels, so they cannot drift apart.
it('publishes the retirement guidance as schema metadata', () => {
const shapeOf = (schema: { shape: Record<string, { description?: string }> }) => schema.shape;
const componentDescribe = shapeOf(
ReportComponentZodSchema as unknown as { shape: Record<string, { description?: string }> },
).dataSource?.description;
const builderDescribe = shapeOf(
ReportBuilderZodSchema as unknown as { shape: Record<string, { description?: string }> },
).dataSources?.description;

expect(componentDescribe).toContain('RETIRED (objectui#6121, ADR-0049)');
expect(builderDescribe).toContain('RETIRED (objectui#6121, ADR-0049)');
// Control for the reader itself: a NON-retired member's description is
// still its own noun, so the two hits above are not "every key says
// RETIRED".
expect(
shapeOf(ReportComponentZodSchema as unknown as { shape: Record<string, { description?: string }> })
.title?.description,
).toBe('Report title');
});
});
49 changes: 44 additions & 5 deletions packages/types/src/reports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,6 @@ import type { z } from 'zod';
import type { ReportType as SpecReportType } from '@objectstack/spec/ui';
import type { BaseSchema, SchemaNode } from './base.js';
import type { ChartSchema } from './data-display.js';
import type { DataSource } from './data.js';

/**
* Report Export Format
Expand DownExpand Up@@ -375,9 +374,37 @@ export interface ReportComponentSchema extends BaseSchema {
reportType?: ReportType;

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

/**
* Report fields
Expand DownExpand Up@@ -494,9 +521,21 @@ export interface ReportBuilderSchema extends BaseSchema {
report?: ReportComponentSchema;

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

/**
* Available fields
Expand Down
25 changes: 22 additions & 3 deletions packages/types/src/zod/reports.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,7 @@
import { z } from 'zod';
import { BaseSchema, SchemaNodeSchema } from './base.zod.js';
import { ChartSchema } from './data-display.zod.js';
import { handlerKeyRefusal } from './tombstone.zod.js';
import { handlerKeyRefusal, retirementTombstone } from './tombstone.zod.js';

/**
* Report Export Format Schema
Expand DownExpand Up@@ -143,7 +143,17 @@ export const ReportComponentSchema = BaseSchema.extend({
type: z.literal('report'),
title: z.string().optional().describe('Report title'),
description: z.string().optional().describe('Report description'),
dataSource: z.any().optional().describe('Data source configuration'),
// RETIRED (objectui#6121, maintainer ruling of 2026-08-30, decision batch #8;
// ADR-0049 enforce-or-remove). The TS twin is `dataSource?: never`; the key
// stays DECLARED so an authored value is refused BY NAME instead of being
// waved through by `z.any()` and then read by nobody.
dataSource: retirementTombstone(
'Data source configuration — RETIRED (objectui#6121, ADR-0049). The key was declared as the ' +
'runtime `DataSource` ADAPTER (`find(resource, params)`), which JSON has no value for, and ' +
'no renderer ever read it off a report schema: the report renderers take their adapter as a ' +
'React prop or from `SchemaRendererContext`. Bind a report through the semantic-layer ' +
'`dataset` form (ADR-0021); a legacy presentation report receives its rows under `data`.',
),
fields: z.array(ReportFieldSchema).optional().describe('Report fields'),
filters: z.array(ReportFilterSchema).optional().describe('Report filters'),
groupBy: z.array(ReportGroupBySchema).optional().describe('Group by configuration'),
Expand All@@ -165,7 +175,16 @@ export const ReportComponentSchema = BaseSchema.extend({
export const ReportBuilderSchema = BaseSchema.extend({
type: z.literal('report-builder'),
report: ReportComponentSchema.optional().describe('Initial report configuration'),
dataSources: z.array(z.any()).optional().describe('Available data sources'),
// RETIRED with `ReportComponentSchema.dataSource` above (objectui#6121), one
// degree further from a reader: no renderer is registered for
// `report-builder`, the same measurement that retired the two handler keys
// below (objectui#7344).
dataSources: retirementTombstone(
'Available data sources — RETIRED (objectui#6121, ADR-0049). No renderer is registered for ' +
'`report-builder`, so nothing could ever read this key, and it was declared as an array of ' +
'the runtime `DataSource` ADAPTER, which JSON has no value for. Bind a report through the ' +
'semantic-layer `dataset` form (ADR-0021).',
),
availableFields: z.array(ReportFieldSchema).optional().describe('Available fields'),
showPreview: z.boolean().optional().describe('Show preview'),
// RETIRED (objectui#7344, the objectui#6182 ruling in the objectui#6124 shape):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .changeset/6121-retire-report-data-source.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
---
'@object-ui/types': minor
---

The two report data-source keys are retired on both faces (objectui#6121,
maintainer ruling of 2026-08-30, decision batch #8 — option A's retirement half;
ADR-0049 enforce-or-remove).

**The accept set of a published validator moves** (`@object-ui/types/zod`):

- `ReportComponentSchema.dataSource` was `z.any().optional()`, so any JSON value
parsed green and was then read by nobody.
- `ReportBuilderSchema.dataSources` was `z.array(z.any()).optional()`, on a node
type no renderer is registered for at all.

Both now carry `retirementTombstone(...)`: an authored value is refused at the
key's own path with `code: 'invalid_type'` and a message that names the key, says
why it is retired and points at the spelling that runs. Nothing that used to be
refused parses green.

**The TypeScript face** — both keys become `?: never` rather than being deleted,
so an author who still writes one gets a `tsc` error at the authoring site
instead of a silently stripped key. They were annotated `DataSource` /
`DataSource[]`, the runtime ADAPTER interface (`find(resource, params)`), which
no JSON document can author; that mis-annotation is the defect objectui#6121 was
filed for, since every example on `content/docs/core/report-schema.mdx` authored
a config object against it.

**Why this is a retirement and not a rename.** No read site consumed either key:
`@object-ui/plugin-report`'s `ReportRenderer` takes its adapter as a React prop
or from `SchemaRendererContext`, never off `schema.dataSource`, and the live
9.0 path binds a semantic-layer `dataset` (ADR-0021). Authored occurrences
measured zero in this repo and in the sibling `objectstack` checkout, whose
report metadata binds `dataset` throughout — the ruling's own deprecation-window
exit criterion. A stored document that still carries the key now fails loudly at
`safeParse` instead of being accepted and ignored; drop the key, and bind the
report through `dataset`.

The replacement binding key the ruling names (`data?: ViewData`) is deliberately
NOT declared here, and is escalated on objectui#6121: `data` is already a live
key on `ReportComponentSchema` — the report ROW array, read by
`LegacyReportRenderer` as `data.length` / `data.map` — so declaring the binding
under that name would put two authoring contracts on one key inside one
renderer.

Pinned in `packages/types/src/__tests__/report-schema-authoring-face.test.ts`:
the `never` twins, the named refusals with their issue envelope, the `.describe()`
metadata channel, and controls that a report without the key still parses.
27 changes: 12 additions & 15 deletions content/docs/core/report-schema.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,18 @@ complete list.
| `type` | `'report'` | Component type identifier (required) |
| `title` | `string` | Report title |
| `description` | `string` | Report description |
| `dataSource` | `DataSource` | Data source configuration |

> **Retired (objectui#6121):** `ReportComponentSchema.dataSource` and
> `ReportBuilderSchema.dataSources` used to be documented and declared here.
> Both were annotated with `DataSource`, the runtime **adapter** interface
> (`find(resource, params)`), which no JSON document can author — and no
> renderer ever read either key off a schema: the report renderers take their
> adapter as a React prop or from the renderer context. Both keys are now
> `never` on the TypeScript face and are refused **by name** by the published
> validator, so an authored value fails loudly instead of being accepted and
> ignored. A report binds its data through the semantic-layer `dataset` form
> (ADR-0021); a legacy presentation report receives already-fetched rows under
> `data`.

### Report Fields

Expand DownExpand Up@@ -227,15 +238,6 @@ const comprehensiveReport: ReportComponentSchema = {
title: 'Quarterly Sales Analysis',
description: 'Comprehensive sales performance analysis by region and product',

// Data source
dataSource: {
provider: 'api',
read: {
url: '/api/sales',
method: 'GET'
}
},

// Report fields
fields: [
{
Expand DownExpand Up@@ -406,11 +408,6 @@ const builder: ReportBuilderSchema = {
title: 'Untitled Report'
},

dataSources: [
{ provider: 'api', read: { url: '/api/sales' } },
{ provider: 'api', read: { url: '/api/customers' } }
],

availableFields: [
{ name: 'revenue', label: 'Revenue', type: 'number' },
{ name: 'units', label: 'Units Sold', type: 'number' }
Expand Down
113 changes: 112 additions & 1 deletion packages/types/src/__tests__/report-schema-authoring-face.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,17 +63,33 @@
* The `expect(…)` lines below are RUNTIME and are judged by vitest. Every
* relaxation therefore carries at least one assertion of each kind, so neither
* instrument going missing can make this file vacuous on its own.
*
* ## 3. The RETIREMENT this card's own ruling ordered (maintainer, 2026-08-30)
*
* The third section pins the opposite direction: `ReportComponentSchema.dataSource`
* and `ReportBuilderSchema.dataSources` are RETIRED. Both were annotated with the
* runtime `DataSource` ADAPTER — a shape no JSON document can author — and no read
* site ever consumed either key. A retirement needs its own pin for the mirror
* reason a widening does: `?: never` compiles for every existing caller (nobody
* wrote the key), so nothing would fail if a later edit restored the adapter
* annotation or relaxed the mirror back to `z.any()`. Both halves are pinned —
* the `never` TypeScript twin AND the mirror's named refusal — because either one
* alone leaves `declared !== enforced`, which is the defect ADR-0049 names.
*/

import { describe, it, expect } from 'vitest';
import type {
ReportBuilderSchema,
ReportComponentSchema,
ReportExportConfig,
ReportExportFormat,
} from '../reports.js';
import type { ChartDataSeries } from '../data-display.js';
import { ChartDataSeriesSchema } from '../zod/data-display.zod.js';
import { ReportComponentSchema as ReportComponentZodSchema } from '../zod/reports.zod.js';
import {
ReportBuilderSchema as ReportBuilderZodSchema,
ReportComponentSchema as ReportComponentZodSchema,
} from '../zod/reports.zod.js';

/** `true` only when the two types are mutually assignable AND identical. */
type Eq<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
Expand DownExpand Up@@ -162,3 +178,98 @@ describe('objectui#6121 — ChartDataSeries declares the per-series family overr
expect(ChartDataSeriesSchema.parse(plain).type).toBeUndefined();
});
});

describe('objectui#6121 — the two report data-source keys are retired on both faces', () => {
// 3a. THE TYPE PIN. `?: never` resolves the member type to `undefined`, so
// this line fails if either key is restored to `DataSource` / `DataSource[]`
// — or to any other value type, including the `ViewData` binding whose key
// name is still an open question on this card.
type RetiredDataSource = ReportComponentSchema['dataSource'];
type RetiredDataSources = ReportBuilderSchema['dataSources'];
type _DataSourceStaysRetired = Assert<Eq<RetiredDataSource, undefined>>;
type _DataSourcesStayRetired = Assert<Eq<RetiredDataSources, undefined>>;

it('refuses an authored `dataSource` on both faces, by name', () => {
const authored = {
type: 'report' as const,
title: 'Quarterly Sales Analysis',
// The exact face `content/docs/core/report-schema.mdx` used to teach.
dataSource: { provider: 'api', read: { url: '/api/sales', method: 'GET' } },
};

// @ts-expect-error `dataSource` is retired — `?: never` admits no value
const typed: ReportComponentSchema = authored;
expect(typed).toBeTruthy();

const result = ReportComponentZodSchema.safeParse(authored);
expect(result.success).toBe(false);
// The ENVELOPE, not the fact that something failed: one issue, at this
// key's own path, reported as `invalid_type` (what `z.never()` emits) —
// and carrying the tombstone's guidance rather than zod's generic text,
// which is the half `retirementTombstone` exists for.
const issues = result.success ? [] : result.error.issues;
expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'dataSource']]);
expect(issues[0]?.message).toContain('RETIRED (objectui#6121, ADR-0049)');
});

it('refuses an authored `dataSources` on the builder, by name', () => {
const authored = {
type: 'report-builder' as const,
dataSources: [{ provider: 'api', read: { url: '/api/sales' } }],
};

// @ts-expect-error `dataSources` is retired — `?: never` admits no value
const typed: ReportBuilderSchema = authored;
expect(typed).toBeTruthy();

const result = ReportBuilderZodSchema.safeParse(authored);
expect(result.success).toBe(false);
const issues = result.success ? [] : result.error.issues;
expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'dataSources']]);
expect(issues[0]?.message).toContain('RETIRED (objectui#6121, ADR-0049)');
});

// 3b. CONTROLS, in the same run. Two zeros above need two things that fire:
// without these, a mirror that refused EVERYTHING would read as a pass, and
// so would a `.safeParse` that had stopped being called at all.
it('the same report without the retired key still parses, and the row array is untouched', () => {
const report: ReportComponentSchema = {
type: 'report',
title: 'Quarterly Sales Analysis',
// `data` is the report ROW array — a live key with a live read
// (`LegacyReportRenderer` reads `data.length` / `data.map`). It is NOT
// the retired binding, and this control is what keeps the retirement
// above from reading as "reports refuse data".
data: [{ region: 'EMEA', revenue: 1 }],
};
const result = ReportComponentZodSchema.safeParse(report);
expect(result.success).toBe(true);
expect(result.success && result.data.data).toHaveLength(1);

const builder: ReportBuilderSchema = { type: 'report-builder', showPreview: true };
expect(ReportBuilderZodSchema.safeParse(builder).success).toBe(true);
});

// 3c. The guidance reaches the OTHER author-facing channel too — the
// `.describe()` metadata that feeds generated JSON Schema and the docs
// surface. One string, two channels, so they cannot drift apart.
it('publishes the retirement guidance as schema metadata', () => {
const shapeOf = (schema: { shape: Record<string, { description?: string }> }) => schema.shape;
const componentDescribe = shapeOf(
ReportComponentZodSchema as unknown as { shape: Record<string, { description?: string }> },
).dataSource?.description;
const builderDescribe = shapeOf(
ReportBuilderZodSchema as unknown as { shape: Record<string, { description?: string }> },
).dataSources?.description;

expect(componentDescribe).toContain('RETIRED (objectui#6121, ADR-0049)');
expect(builderDescribe).toContain('RETIRED (objectui#6121, ADR-0049)');
// Control for the reader itself: a NON-retired member's description is
// still its own noun, so the two hits above are not "every key says
// RETIRED".
expect(
shapeOf(ReportComponentZodSchema as unknown as { shape: Record<string, { description?: string }> })
.title?.description,
).toBe('Report title');
});
});
49 changes: 44 additions & 5 deletions packages/types/src/reports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,6 @@ import type { z } from 'zod';
import type { ReportType as SpecReportType } from '@objectstack/spec/ui';
import type { BaseSchema, SchemaNode } from './base.js';
import type { ChartSchema } from './data-display.js';
import type { DataSource } from './data.js';

/**
* Report Export Format
Expand DownExpand Up@@ -375,9 +374,37 @@ export interface ReportComponentSchema extends BaseSchema {
reportType?: ReportType;

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

/**
* Report fields
Expand DownExpand Up@@ -494,9 +521,21 @@ export interface ReportBuilderSchema extends BaseSchema {
report?: ReportComponentSchema;

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

/**
* Available fields
Expand Down
25 changes: 22 additions & 3 deletions packages/types/src/zod/reports.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,7 @@
import { z } from 'zod';
import { BaseSchema, SchemaNodeSchema } from './base.zod.js';
import { ChartSchema } from './data-display.zod.js';
import { handlerKeyRefusal } from './tombstone.zod.js';
import { handlerKeyRefusal, retirementTombstone } from './tombstone.zod.js';

/**
* Report Export Format Schema
Expand DownExpand Up@@ -143,7 +143,17 @@ export const ReportComponentSchema = BaseSchema.extend({
type: z.literal('report'),
title: z.string().optional().describe('Report title'),
description: z.string().optional().describe('Report description'),
dataSource: z.any().optional().describe('Data source configuration'),
// RETIRED (objectui#6121, maintainer ruling of 2026-08-30, decision batch #8;
// ADR-0049 enforce-or-remove). The TS twin is `dataSource?: never`; the key
// stays DECLARED so an authored value is refused BY NAME instead of being
// waved through by `z.any()` and then read by nobody.
dataSource: retirementTombstone(
'Data source configuration — RETIRED (objectui#6121, ADR-0049). The key was declared as the ' +
'runtime `DataSource` ADAPTER (`find(resource, params)`), which JSON has no value for, and ' +
'no renderer ever read it off a report schema: the report renderers take their adapter as a ' +
'React prop or from `SchemaRendererContext`. Bind a report through the semantic-layer ' +
'`dataset` form (ADR-0021); a legacy presentation report receives its rows under `data`.',
),
fields: z.array(ReportFieldSchema).optional().describe('Report fields'),
filters: z.array(ReportFilterSchema).optional().describe('Report filters'),
groupBy: z.array(ReportGroupBySchema).optional().describe('Group by configuration'),
Expand All@@ -165,7 +175,16 @@ export const ReportComponentSchema = BaseSchema.extend({
export const ReportBuilderSchema = BaseSchema.extend({
type: z.literal('report-builder'),
report: ReportComponentSchema.optional().describe('Initial report configuration'),
dataSources: z.array(z.any()).optional().describe('Available data sources'),
// RETIRED with `ReportComponentSchema.dataSource` above (objectui#6121), one
// degree further from a reader: no renderer is registered for
// `report-builder`, the same measurement that retired the two handler keys
// below (objectui#7344).
dataSources: retirementTombstone(
'Available data sources — RETIRED (objectui#6121, ADR-0049). No renderer is registered for ' +
'`report-builder`, so nothing could ever read this key, and it was declared as an array of ' +
'the runtime `DataSource` ADAPTER, which JSON has no value for. Bind a report through the ' +
'semantic-layer `dataset` form (ADR-0021).',
),
availableFields: z.array(ReportFieldSchema).optional().describe('Available fields'),
showPreview: z.boolean().optional().describe('Show preview'),
// RETIRED (objectui#7344, the objectui#6182 ruling in the objectui#6124 shape):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .changeset/6121-retire-report-data-source.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
---
'@object-ui/types': minor
---

The two report data-source keys are retired on both faces (objectui#6121,
maintainer ruling of 2026-08-30, decision batch #8 — option A's retirement half;
ADR-0049 enforce-or-remove).

**The accept set of a published validator moves** (`@object-ui/types/zod`):

- `ReportComponentSchema.dataSource` was `z.any().optional()`, so any JSON value
parsed green and was then read by nobody.
- `ReportBuilderSchema.dataSources` was `z.array(z.any()).optional()`, on a node
type no renderer is registered for at all.

Both now carry `retirementTombstone(...)`: an authored value is refused at the
key's own path with `code: 'invalid_type'` and a message that names the key, says
why it is retired and points at the spelling that runs. Nothing that used to be
refused parses green.

**The TypeScript face** — both keys become `?: never` rather than being deleted,
so an author who still writes one gets a `tsc` error at the authoring site
instead of a silently stripped key. They were annotated `DataSource` /
`DataSource[]`, the runtime ADAPTER interface (`find(resource, params)`), which
no JSON document can author; that mis-annotation is the defect objectui#6121 was
filed for, since every example on `content/docs/core/report-schema.mdx` authored
a config object against it.

**Why this is a retirement and not a rename.** No read site consumed either key:
`@object-ui/plugin-report`'s `ReportRenderer` takes its adapter as a React prop
or from `SchemaRendererContext`, never off `schema.dataSource`, and the live
9.0 path binds a semantic-layer `dataset` (ADR-0021). Authored occurrences
measured zero in this repo and in the sibling `objectstack` checkout, whose
report metadata binds `dataset` throughout — the ruling's own deprecation-window
exit criterion. A stored document that still carries the key now fails loudly at
`safeParse` instead of being accepted and ignored; drop the key, and bind the
report through `dataset`.

The replacement binding key the ruling names (`data?: ViewData`) is deliberately
NOT declared here, and is escalated on objectui#6121: `data` is already a live
key on `ReportComponentSchema` — the report ROW array, read by
`LegacyReportRenderer` as `data.length` / `data.map` — so declaring the binding
under that name would put two authoring contracts on one key inside one
renderer.

Pinned in `packages/types/src/__tests__/report-schema-authoring-face.test.ts`:
the `never` twins, the named refusals with their issue envelope, the `.describe()`
metadata channel, and controls that a report without the key still parses.
27 changes: 12 additions & 15 deletions content/docs/core/report-schema.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,18 @@ complete list.
| `type` | `'report'` | Component type identifier (required) |
| `title` | `string` | Report title |
| `description` | `string` | Report description |
| `dataSource` | `DataSource` | Data source configuration |

> **Retired (objectui#6121):** `ReportComponentSchema.dataSource` and
> `ReportBuilderSchema.dataSources` used to be documented and declared here.
> Both were annotated with `DataSource`, the runtime **adapter** interface
> (`find(resource, params)`), which no JSON document can author — and no
> renderer ever read either key off a schema: the report renderers take their
> adapter as a React prop or from the renderer context. Both keys are now
> `never` on the TypeScript face and are refused **by name** by the published
> validator, so an authored value fails loudly instead of being accepted and
> ignored. A report binds its data through the semantic-layer `dataset` form
> (ADR-0021); a legacy presentation report receives already-fetched rows under
> `data`.

### Report Fields

Expand DownExpand Up@@ -227,15 +238,6 @@ const comprehensiveReport: ReportComponentSchema = {
title: 'Quarterly Sales Analysis',
description: 'Comprehensive sales performance analysis by region and product',

// Data source
dataSource: {
provider: 'api',
read: {
url: '/api/sales',
method: 'GET'
}
},

// Report fields
fields: [
{
Expand DownExpand Up@@ -406,11 +408,6 @@ const builder: ReportBuilderSchema = {
title: 'Untitled Report'
},

dataSources: [
{ provider: 'api', read: { url: '/api/sales' } },
{ provider: 'api', read: { url: '/api/customers' } }
],

availableFields: [
{ name: 'revenue', label: 'Revenue', type: 'number' },
{ name: 'units', label: 'Units Sold', type: 'number' }
Expand Down
113 changes: 112 additions & 1 deletion packages/types/src/__tests__/report-schema-authoring-face.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,17 +63,33 @@
* The `expect(…)` lines below are RUNTIME and are judged by vitest. Every
* relaxation therefore carries at least one assertion of each kind, so neither
* instrument going missing can make this file vacuous on its own.
*
* ## 3. The RETIREMENT this card's own ruling ordered (maintainer, 2026-08-30)
*
* The third section pins the opposite direction: `ReportComponentSchema.dataSource`
* and `ReportBuilderSchema.dataSources` are RETIRED. Both were annotated with the
* runtime `DataSource` ADAPTER — a shape no JSON document can author — and no read
* site ever consumed either key. A retirement needs its own pin for the mirror
* reason a widening does: `?: never` compiles for every existing caller (nobody
* wrote the key), so nothing would fail if a later edit restored the adapter
* annotation or relaxed the mirror back to `z.any()`. Both halves are pinned —
* the `never` TypeScript twin AND the mirror's named refusal — because either one
* alone leaves `declared !== enforced`, which is the defect ADR-0049 names.
*/

import { describe, it, expect } from 'vitest';
import type {
ReportBuilderSchema,
ReportComponentSchema,
ReportExportConfig,
ReportExportFormat,
} from '../reports.js';
import type { ChartDataSeries } from '../data-display.js';
import { ChartDataSeriesSchema } from '../zod/data-display.zod.js';
import { ReportComponentSchema as ReportComponentZodSchema } from '../zod/reports.zod.js';
import {
ReportBuilderSchema as ReportBuilderZodSchema,
ReportComponentSchema as ReportComponentZodSchema,
} from '../zod/reports.zod.js';

/** `true` only when the two types are mutually assignable AND identical. */
type Eq<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
Expand DownExpand Up@@ -162,3 +178,98 @@ describe('objectui#6121 — ChartDataSeries declares the per-series family overr
expect(ChartDataSeriesSchema.parse(plain).type).toBeUndefined();
});
});

describe('objectui#6121 — the two report data-source keys are retired on both faces', () => {
// 3a. THE TYPE PIN. `?: never` resolves the member type to `undefined`, so
// this line fails if either key is restored to `DataSource` / `DataSource[]`
// — or to any other value type, including the `ViewData` binding whose key
// name is still an open question on this card.
type RetiredDataSource = ReportComponentSchema['dataSource'];
type RetiredDataSources = ReportBuilderSchema['dataSources'];
type _DataSourceStaysRetired = Assert<Eq<RetiredDataSource, undefined>>;
type _DataSourcesStayRetired = Assert<Eq<RetiredDataSources, undefined>>;

it('refuses an authored `dataSource` on both faces, by name', () => {
const authored = {
type: 'report' as const,
title: 'Quarterly Sales Analysis',
// The exact face `content/docs/core/report-schema.mdx` used to teach.
dataSource: { provider: 'api', read: { url: '/api/sales', method: 'GET' } },
};

// @ts-expect-error `dataSource` is retired — `?: never` admits no value
const typed: ReportComponentSchema = authored;
expect(typed).toBeTruthy();

const result = ReportComponentZodSchema.safeParse(authored);
expect(result.success).toBe(false);
// The ENVELOPE, not the fact that something failed: one issue, at this
// key's own path, reported as `invalid_type` (what `z.never()` emits) —
// and carrying the tombstone's guidance rather than zod's generic text,
// which is the half `retirementTombstone` exists for.
const issues = result.success ? [] : result.error.issues;
expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'dataSource']]);
expect(issues[0]?.message).toContain('RETIRED (objectui#6121, ADR-0049)');
});

it('refuses an authored `dataSources` on the builder, by name', () => {
const authored = {
type: 'report-builder' as const,
dataSources: [{ provider: 'api', read: { url: '/api/sales' } }],
};

// @ts-expect-error `dataSources` is retired — `?: never` admits no value
const typed: ReportBuilderSchema = authored;
expect(typed).toBeTruthy();

const result = ReportBuilderZodSchema.safeParse(authored);
expect(result.success).toBe(false);
const issues = result.success ? [] : result.error.issues;
expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'dataSources']]);
expect(issues[0]?.message).toContain('RETIRED (objectui#6121, ADR-0049)');
});

// 3b. CONTROLS, in the same run. Two zeros above need two things that fire:
// without these, a mirror that refused EVERYTHING would read as a pass, and
// so would a `.safeParse` that had stopped being called at all.
it('the same report without the retired key still parses, and the row array is untouched', () => {
const report: ReportComponentSchema = {
type: 'report',
title: 'Quarterly Sales Analysis',
// `data` is the report ROW array — a live key with a live read
// (`LegacyReportRenderer` reads `data.length` / `data.map`). It is NOT
// the retired binding, and this control is what keeps the retirement
// above from reading as "reports refuse data".
data: [{ region: 'EMEA', revenue: 1 }],
};
const result = ReportComponentZodSchema.safeParse(report);
expect(result.success).toBe(true);
expect(result.success && result.data.data).toHaveLength(1);

const builder: ReportBuilderSchema = { type: 'report-builder', showPreview: true };
expect(ReportBuilderZodSchema.safeParse(builder).success).toBe(true);
});

// 3c. The guidance reaches the OTHER author-facing channel too — the
// `.describe()` metadata that feeds generated JSON Schema and the docs
// surface. One string, two channels, so they cannot drift apart.
it('publishes the retirement guidance as schema metadata', () => {
const shapeOf = (schema: { shape: Record<string, { description?: string }> }) => schema.shape;
const componentDescribe = shapeOf(
ReportComponentZodSchema as unknown as { shape: Record<string, { description?: string }> },
).dataSource?.description;
const builderDescribe = shapeOf(
ReportBuilderZodSchema as unknown as { shape: Record<string, { description?: string }> },
).dataSources?.description;

expect(componentDescribe).toContain('RETIRED (objectui#6121, ADR-0049)');
expect(builderDescribe).toContain('RETIRED (objectui#6121, ADR-0049)');
// Control for the reader itself: a NON-retired member's description is
// still its own noun, so the two hits above are not "every key says
// RETIRED".
expect(
shapeOf(ReportComponentZodSchema as unknown as { shape: Record<string, { description?: string }> })
.title?.description,
).toBe('Report title');
});
});
49 changes: 44 additions & 5 deletions packages/types/src/reports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,6 @@ import type { z } from 'zod';
import type { ReportType as SpecReportType } from '@objectstack/spec/ui';
import type { BaseSchema, SchemaNode } from './base.js';
import type { ChartSchema } from './data-display.js';
import type { DataSource } from './data.js';

/**
* Report Export Format
Expand DownExpand Up@@ -375,9 +374,37 @@ export interface ReportComponentSchema extends BaseSchema {
reportType?: ReportType;

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

/**
* Report fields
Expand DownExpand Up@@ -494,9 +521,21 @@ export interface ReportBuilderSchema extends BaseSchema {
report?: ReportComponentSchema;

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

/**
* Available fields
Expand Down
25 changes: 22 additions & 3 deletions packages/types/src/zod/reports.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,7 @@
import { z } from 'zod';
import { BaseSchema, SchemaNodeSchema } from './base.zod.js';
import { ChartSchema } from './data-display.zod.js';
import { handlerKeyRefusal } from './tombstone.zod.js';
import { handlerKeyRefusal, retirementTombstone } from './tombstone.zod.js';

/**
* Report Export Format Schema
Expand DownExpand Up@@ -143,7 +143,17 @@ export const ReportComponentSchema = BaseSchema.extend({
type: z.literal('report'),
title: z.string().optional().describe('Report title'),
description: z.string().optional().describe('Report description'),
dataSource: z.any().optional().describe('Data source configuration'),
// RETIRED (objectui#6121, maintainer ruling of 2026-08-30, decision batch #8;
// ADR-0049 enforce-or-remove). The TS twin is `dataSource?: never`; the key
// stays DECLARED so an authored value is refused BY NAME instead of being
// waved through by `z.any()` and then read by nobody.
dataSource: retirementTombstone(
'Data source configuration — RETIRED (objectui#6121, ADR-0049). The key was declared as the ' +
'runtime `DataSource` ADAPTER (`find(resource, params)`), which JSON has no value for, and ' +
'no renderer ever read it off a report schema: the report renderers take their adapter as a ' +
'React prop or from `SchemaRendererContext`. Bind a report through the semantic-layer ' +
'`dataset` form (ADR-0021); a legacy presentation report receives its rows under `data`.',
),
fields: z.array(ReportFieldSchema).optional().describe('Report fields'),
filters: z.array(ReportFilterSchema).optional().describe('Report filters'),
groupBy: z.array(ReportGroupBySchema).optional().describe('Group by configuration'),
Expand All@@ -165,7 +175,16 @@ export const ReportComponentSchema = BaseSchema.extend({
export const ReportBuilderSchema = BaseSchema.extend({
type: z.literal('report-builder'),
report: ReportComponentSchema.optional().describe('Initial report configuration'),
dataSources: z.array(z.any()).optional().describe('Available data sources'),
// RETIRED with `ReportComponentSchema.dataSource` above (objectui#6121), one
// degree further from a reader: no renderer is registered for
// `report-builder`, the same measurement that retired the two handler keys
// below (objectui#7344).
dataSources: retirementTombstone(
'Available data sources — RETIRED (objectui#6121, ADR-0049). No renderer is registered for ' +
'`report-builder`, so nothing could ever read this key, and it was declared as an array of ' +
'the runtime `DataSource` ADAPTER, which JSON has no value for. Bind a report through the ' +
'semantic-layer `dataset` form (ADR-0021).',
),
availableFields: z.array(ReportFieldSchema).optional().describe('Available fields'),
showPreview: z.boolean().optional().describe('Show preview'),
// RETIRED (objectui#7344, the objectui#6182 ruling in the objectui#6124 shape):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .changeset/6121-retire-report-data-source.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
---
'@object-ui/types': minor
---

The two report data-source keys are retired on both faces (objectui#6121,
maintainer ruling of 2026-08-30, decision batch #8 — option A's retirement half;
ADR-0049 enforce-or-remove).

**The accept set of a published validator moves** (`@object-ui/types/zod`):

- `ReportComponentSchema.dataSource` was `z.any().optional()`, so any JSON value
parsed green and was then read by nobody.
- `ReportBuilderSchema.dataSources` was `z.array(z.any()).optional()`, on a node
type no renderer is registered for at all.

Both now carry `retirementTombstone(...)`: an authored value is refused at the
key's own path with `code: 'invalid_type'` and a message that names the key, says
why it is retired and points at the spelling that runs. Nothing that used to be
refused parses green.

**The TypeScript face** — both keys become `?: never` rather than being deleted,
so an author who still writes one gets a `tsc` error at the authoring site
instead of a silently stripped key. They were annotated `DataSource` /
`DataSource[]`, the runtime ADAPTER interface (`find(resource, params)`), which
no JSON document can author; that mis-annotation is the defect objectui#6121 was
filed for, since every example on `content/docs/core/report-schema.mdx` authored
a config object against it.

**Why this is a retirement and not a rename.** No read site consumed either key:
`@object-ui/plugin-report`'s `ReportRenderer` takes its adapter as a React prop
or from `SchemaRendererContext`, never off `schema.dataSource`, and the live
9.0 path binds a semantic-layer `dataset` (ADR-0021). Authored occurrences
measured zero in this repo and in the sibling `objectstack` checkout, whose
report metadata binds `dataset` throughout — the ruling's own deprecation-window
exit criterion. A stored document that still carries the key now fails loudly at
`safeParse` instead of being accepted and ignored; drop the key, and bind the
report through `dataset`.

The replacement binding key the ruling names (`data?: ViewData`) is deliberately
NOT declared here, and is escalated on objectui#6121: `data` is already a live
key on `ReportComponentSchema` — the report ROW array, read by
`LegacyReportRenderer` as `data.length` / `data.map` — so declaring the binding
under that name would put two authoring contracts on one key inside one
renderer.

Pinned in `packages/types/src/__tests__/report-schema-authoring-face.test.ts`:
the `never` twins, the named refusals with their issue envelope, the `.describe()`
metadata channel, and controls that a report without the key still parses.
27 changes: 12 additions & 15 deletions content/docs/core/report-schema.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,18 @@ complete list.
| `type` | `'report'` | Component type identifier (required) |
| `title` | `string` | Report title |
| `description` | `string` | Report description |
| `dataSource` | `DataSource` | Data source configuration |

> **Retired (objectui#6121):** `ReportComponentSchema.dataSource` and
> `ReportBuilderSchema.dataSources` used to be documented and declared here.
> Both were annotated with `DataSource`, the runtime **adapter** interface
> (`find(resource, params)`), which no JSON document can author — and no
> renderer ever read either key off a schema: the report renderers take their
> adapter as a React prop or from the renderer context. Both keys are now
> `never` on the TypeScript face and are refused **by name** by the published
> validator, so an authored value fails loudly instead of being accepted and
> ignored. A report binds its data through the semantic-layer `dataset` form
> (ADR-0021); a legacy presentation report receives already-fetched rows under
> `data`.

### Report Fields

Expand DownExpand Up@@ -227,15 +238,6 @@ const comprehensiveReport: ReportComponentSchema = {
title: 'Quarterly Sales Analysis',
description: 'Comprehensive sales performance analysis by region and product',

// Data source
dataSource: {
provider: 'api',
read: {
url: '/api/sales',
method: 'GET'
}
},

// Report fields
fields: [
{
Expand DownExpand Up@@ -406,11 +408,6 @@ const builder: ReportBuilderSchema = {
title: 'Untitled Report'
},

dataSources: [
{ provider: 'api', read: { url: '/api/sales' } },
{ provider: 'api', read: { url: '/api/customers' } }
],

availableFields: [
{ name: 'revenue', label: 'Revenue', type: 'number' },
{ name: 'units', label: 'Units Sold', type: 'number' }
Expand Down
113 changes: 112 additions & 1 deletion packages/types/src/__tests__/report-schema-authoring-face.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,17 +63,33 @@
* The `expect(…)` lines below are RUNTIME and are judged by vitest. Every
* relaxation therefore carries at least one assertion of each kind, so neither
* instrument going missing can make this file vacuous on its own.
*
* ## 3. The RETIREMENT this card's own ruling ordered (maintainer, 2026-08-30)
*
* The third section pins the opposite direction: `ReportComponentSchema.dataSource`
* and `ReportBuilderSchema.dataSources` are RETIRED. Both were annotated with the
* runtime `DataSource` ADAPTER — a shape no JSON document can author — and no read
* site ever consumed either key. A retirement needs its own pin for the mirror
* reason a widening does: `?: never` compiles for every existing caller (nobody
* wrote the key), so nothing would fail if a later edit restored the adapter
* annotation or relaxed the mirror back to `z.any()`. Both halves are pinned —
* the `never` TypeScript twin AND the mirror's named refusal — because either one
* alone leaves `declared !== enforced`, which is the defect ADR-0049 names.
*/

import { describe, it, expect } from 'vitest';
import type {
ReportBuilderSchema,
ReportComponentSchema,
ReportExportConfig,
ReportExportFormat,
} from '../reports.js';
import type { ChartDataSeries } from '../data-display.js';
import { ChartDataSeriesSchema } from '../zod/data-display.zod.js';
import { ReportComponentSchema as ReportComponentZodSchema } from '../zod/reports.zod.js';
import {
ReportBuilderSchema as ReportBuilderZodSchema,
ReportComponentSchema as ReportComponentZodSchema,
} from '../zod/reports.zod.js';

/** `true` only when the two types are mutually assignable AND identical. */
type Eq<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
Expand DownExpand Up@@ -162,3 +178,98 @@ describe('objectui#6121 — ChartDataSeries declares the per-series family overr
expect(ChartDataSeriesSchema.parse(plain).type).toBeUndefined();
});
});

describe('objectui#6121 — the two report data-source keys are retired on both faces', () => {
// 3a. THE TYPE PIN. `?: never` resolves the member type to `undefined`, so
// this line fails if either key is restored to `DataSource` / `DataSource[]`
// — or to any other value type, including the `ViewData` binding whose key
// name is still an open question on this card.
type RetiredDataSource = ReportComponentSchema['dataSource'];
type RetiredDataSources = ReportBuilderSchema['dataSources'];
type _DataSourceStaysRetired = Assert<Eq<RetiredDataSource, undefined>>;
type _DataSourcesStayRetired = Assert<Eq<RetiredDataSources, undefined>>;

it('refuses an authored `dataSource` on both faces, by name', () => {
const authored = {
type: 'report' as const,
title: 'Quarterly Sales Analysis',
// The exact face `content/docs/core/report-schema.mdx` used to teach.
dataSource: { provider: 'api', read: { url: '/api/sales', method: 'GET' } },
};

// @ts-expect-error `dataSource` is retired — `?: never` admits no value
const typed: ReportComponentSchema = authored;
expect(typed).toBeTruthy();

const result = ReportComponentZodSchema.safeParse(authored);
expect(result.success).toBe(false);
// The ENVELOPE, not the fact that something failed: one issue, at this
// key's own path, reported as `invalid_type` (what `z.never()` emits) —
// and carrying the tombstone's guidance rather than zod's generic text,
// which is the half `retirementTombstone` exists for.
const issues = result.success ? [] : result.error.issues;
expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'dataSource']]);
expect(issues[0]?.message).toContain('RETIRED (objectui#6121, ADR-0049)');
});

it('refuses an authored `dataSources` on the builder, by name', () => {
const authored = {
type: 'report-builder' as const,
dataSources: [{ provider: 'api', read: { url: '/api/sales' } }],
};

// @ts-expect-error `dataSources` is retired — `?: never` admits no value
const typed: ReportBuilderSchema = authored;
expect(typed).toBeTruthy();

const result = ReportBuilderZodSchema.safeParse(authored);
expect(result.success).toBe(false);
const issues = result.success ? [] : result.error.issues;
expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'dataSources']]);
expect(issues[0]?.message).toContain('RETIRED (objectui#6121, ADR-0049)');
});

// 3b. CONTROLS, in the same run. Two zeros above need two things that fire:
// without these, a mirror that refused EVERYTHING would read as a pass, and
// so would a `.safeParse` that had stopped being called at all.
it('the same report without the retired key still parses, and the row array is untouched', () => {
const report: ReportComponentSchema = {
type: 'report',
title: 'Quarterly Sales Analysis',
// `data` is the report ROW array — a live key with a live read
// (`LegacyReportRenderer` reads `data.length` / `data.map`). It is NOT
// the retired binding, and this control is what keeps the retirement
// above from reading as "reports refuse data".
data: [{ region: 'EMEA', revenue: 1 }],
};
const result = ReportComponentZodSchema.safeParse(report);
expect(result.success).toBe(true);
expect(result.success && result.data.data).toHaveLength(1);

const builder: ReportBuilderSchema = { type: 'report-builder', showPreview: true };
expect(ReportBuilderZodSchema.safeParse(builder).success).toBe(true);
});

// 3c. The guidance reaches the OTHER author-facing channel too — the
// `.describe()` metadata that feeds generated JSON Schema and the docs
// surface. One string, two channels, so they cannot drift apart.
it('publishes the retirement guidance as schema metadata', () => {
const shapeOf = (schema: { shape: Record<string, { description?: string }> }) => schema.shape;
const componentDescribe = shapeOf(
ReportComponentZodSchema as unknown as { shape: Record<string, { description?: string }> },
).dataSource?.description;
const builderDescribe = shapeOf(
ReportBuilderZodSchema as unknown as { shape: Record<string, { description?: string }> },
).dataSources?.description;

expect(componentDescribe).toContain('RETIRED (objectui#6121, ADR-0049)');
expect(builderDescribe).toContain('RETIRED (objectui#6121, ADR-0049)');
// Control for the reader itself: a NON-retired member's description is
// still its own noun, so the two hits above are not "every key says
// RETIRED".
expect(
shapeOf(ReportComponentZodSchema as unknown as { shape: Record<string, { description?: string }> })
.title?.description,
).toBe('Report title');
});
});
49 changes: 44 additions & 5 deletions packages/types/src/reports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,6 @@ import type { z } from 'zod';
import type { ReportType as SpecReportType } from '@objectstack/spec/ui';
import type { BaseSchema, SchemaNode } from './base.js';
import type { ChartSchema } from './data-display.js';
import type { DataSource } from './data.js';

/**
* Report Export Format
Expand DownExpand Up@@ -375,9 +374,37 @@ export interface ReportComponentSchema extends BaseSchema {
reportType?: ReportType;

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

/**
* Report fields
Expand DownExpand Up@@ -494,9 +521,21 @@ export interface ReportBuilderSchema extends BaseSchema {
report?: ReportComponentSchema;

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

/**
* Available fields
Expand Down
25 changes: 22 additions & 3 deletions packages/types/src/zod/reports.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,7 @@
import { z } from 'zod';
import { BaseSchema, SchemaNodeSchema } from './base.zod.js';
import { ChartSchema } from './data-display.zod.js';
import { handlerKeyRefusal } from './tombstone.zod.js';
import { handlerKeyRefusal, retirementTombstone } from './tombstone.zod.js';

/**
* Report Export Format Schema
Expand DownExpand Up@@ -143,7 +143,17 @@ export const ReportComponentSchema = BaseSchema.extend({
type: z.literal('report'),
title: z.string().optional().describe('Report title'),
description: z.string().optional().describe('Report description'),
dataSource: z.any().optional().describe('Data source configuration'),
// RETIRED (objectui#6121, maintainer ruling of 2026-08-30, decision batch #8;
// ADR-0049 enforce-or-remove). The TS twin is `dataSource?: never`; the key
// stays DECLARED so an authored value is refused BY NAME instead of being
// waved through by `z.any()` and then read by nobody.
dataSource: retirementTombstone(
'Data source configuration — RETIRED (objectui#6121, ADR-0049). The key was declared as the ' +
'runtime `DataSource` ADAPTER (`find(resource, params)`), which JSON has no value for, and ' +
'no renderer ever read it off a report schema: the report renderers take their adapter as a ' +
'React prop or from `SchemaRendererContext`. Bind a report through the semantic-layer ' +
'`dataset` form (ADR-0021); a legacy presentation report receives its rows under `data`.',
),
fields: z.array(ReportFieldSchema).optional().describe('Report fields'),
filters: z.array(ReportFilterSchema).optional().describe('Report filters'),
groupBy: z.array(ReportGroupBySchema).optional().describe('Group by configuration'),
Expand All@@ -165,7 +175,16 @@ export const ReportComponentSchema = BaseSchema.extend({
export const ReportBuilderSchema = BaseSchema.extend({
type: z.literal('report-builder'),
report: ReportComponentSchema.optional().describe('Initial report configuration'),
dataSources: z.array(z.any()).optional().describe('Available data sources'),
// RETIRED with `ReportComponentSchema.dataSource` above (objectui#6121), one
// degree further from a reader: no renderer is registered for
// `report-builder`, the same measurement that retired the two handler keys
// below (objectui#7344).
dataSources: retirementTombstone(
'Available data sources — RETIRED (objectui#6121, ADR-0049). No renderer is registered for ' +
'`report-builder`, so nothing could ever read this key, and it was declared as an array of ' +
'the runtime `DataSource` ADAPTER, which JSON has no value for. Bind a report through the ' +
'semantic-layer `dataset` form (ADR-0021).',
),
availableFields: z.array(ReportFieldSchema).optional().describe('Available fields'),
showPreview: z.boolean().optional().describe('Show preview'),
// RETIRED (objectui#7344, the objectui#6182 ruling in the objectui#6124 shape):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .changeset/6121-retire-report-data-source.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
---
'@object-ui/types': minor
---

The two report data-source keys are retired on both faces (objectui#6121,
maintainer ruling of 2026-08-30, decision batch #8 — option A's retirement half;
ADR-0049 enforce-or-remove).

**The accept set of a published validator moves** (`@object-ui/types/zod`):

- `ReportComponentSchema.dataSource` was `z.any().optional()`, so any JSON value
parsed green and was then read by nobody.
- `ReportBuilderSchema.dataSources` was `z.array(z.any()).optional()`, on a node
type no renderer is registered for at all.

Both now carry `retirementTombstone(...)`: an authored value is refused at the
key's own path with `code: 'invalid_type'` and a message that names the key, says
why it is retired and points at the spelling that runs. Nothing that used to be
refused parses green.

**The TypeScript face** — both keys become `?: never` rather than being deleted,
so an author who still writes one gets a `tsc` error at the authoring site
instead of a silently stripped key. They were annotated `DataSource` /
`DataSource[]`, the runtime ADAPTER interface (`find(resource, params)`), which
no JSON document can author; that mis-annotation is the defect objectui#6121 was
filed for, since every example on `content/docs/core/report-schema.mdx` authored
a config object against it.

**Why this is a retirement and not a rename.** No read site consumed either key:
`@object-ui/plugin-report`'s `ReportRenderer` takes its adapter as a React prop
or from `SchemaRendererContext`, never off `schema.dataSource`, and the live
9.0 path binds a semantic-layer `dataset` (ADR-0021). Authored occurrences
measured zero in this repo and in the sibling `objectstack` checkout, whose
report metadata binds `dataset` throughout — the ruling's own deprecation-window
exit criterion. A stored document that still carries the key now fails loudly at
`safeParse` instead of being accepted and ignored; drop the key, and bind the
report through `dataset`.

The replacement binding key the ruling names (`data?: ViewData`) is deliberately
NOT declared here, and is escalated on objectui#6121: `data` is already a live
key on `ReportComponentSchema` — the report ROW array, read by
`LegacyReportRenderer` as `data.length` / `data.map` — so declaring the binding
under that name would put two authoring contracts on one key inside one
renderer.

Pinned in `packages/types/src/__tests__/report-schema-authoring-face.test.ts`:
the `never` twins, the named refusals with their issue envelope, the `.describe()`
metadata channel, and controls that a report without the key still parses.
27 changes: 12 additions & 15 deletions content/docs/core/report-schema.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,7 +85,18 @@ complete list.
| `type` | `'report'` | Component type identifier (required) |
| `title` | `string` | Report title |
| `description` | `string` | Report description |
| `dataSource` | `DataSource` | Data source configuration |

> **Retired (objectui#6121):** `ReportComponentSchema.dataSource` and
> `ReportBuilderSchema.dataSources` used to be documented and declared here.
> Both were annotated with `DataSource`, the runtime **adapter** interface
> (`find(resource, params)`), which no JSON document can author — and no
> renderer ever read either key off a schema: the report renderers take their
> adapter as a React prop or from the renderer context. Both keys are now
> `never` on the TypeScript face and are refused **by name** by the published
> validator, so an authored value fails loudly instead of being accepted and
> ignored. A report binds its data through the semantic-layer `dataset` form
> (ADR-0021); a legacy presentation report receives already-fetched rows under
> `data`.

### Report Fields

Expand DownExpand Up@@ -227,15 +238,6 @@ const comprehensiveReport: ReportComponentSchema = {
title: 'Quarterly Sales Analysis',
description: 'Comprehensive sales performance analysis by region and product',

// Data source
dataSource: {
provider: 'api',
read: {
url: '/api/sales',
method: 'GET'
}
},

// Report fields
fields: [
{
Expand DownExpand Up@@ -406,11 +408,6 @@ const builder: ReportBuilderSchema = {
title: 'Untitled Report'
},

dataSources: [
{ provider: 'api', read: { url: '/api/sales' } },
{ provider: 'api', read: { url: '/api/customers' } }
],

availableFields: [
{ name: 'revenue', label: 'Revenue', type: 'number' },
{ name: 'units', label: 'Units Sold', type: 'number' }
Expand Down
113 changes: 112 additions & 1 deletion packages/types/src/__tests__/report-schema-authoring-face.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,17 +63,33 @@
* The `expect(…)` lines below are RUNTIME and are judged by vitest. Every
* relaxation therefore carries at least one assertion of each kind, so neither
* instrument going missing can make this file vacuous on its own.
*
* ## 3. The RETIREMENT this card's own ruling ordered (maintainer, 2026-08-30)
*
* The third section pins the opposite direction: `ReportComponentSchema.dataSource`
* and `ReportBuilderSchema.dataSources` are RETIRED. Both were annotated with the
* runtime `DataSource` ADAPTER — a shape no JSON document can author — and no read
* site ever consumed either key. A retirement needs its own pin for the mirror
* reason a widening does: `?: never` compiles for every existing caller (nobody
* wrote the key), so nothing would fail if a later edit restored the adapter
* annotation or relaxed the mirror back to `z.any()`. Both halves are pinned —
* the `never` TypeScript twin AND the mirror's named refusal — because either one
* alone leaves `declared !== enforced`, which is the defect ADR-0049 names.
*/

import { describe, it, expect } from 'vitest';
import type {
ReportBuilderSchema,
ReportComponentSchema,
ReportExportConfig,
ReportExportFormat,
} from '../reports.js';
import type { ChartDataSeries } from '../data-display.js';
import { ChartDataSeriesSchema } from '../zod/data-display.zod.js';
import { ReportComponentSchema as ReportComponentZodSchema } from '../zod/reports.zod.js';
import {
ReportBuilderSchema as ReportBuilderZodSchema,
ReportComponentSchema as ReportComponentZodSchema,
} from '../zod/reports.zod.js';

/** `true` only when the two types are mutually assignable AND identical. */
type Eq<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
Expand DownExpand Up@@ -162,3 +178,98 @@ describe('objectui#6121 — ChartDataSeries declares the per-series family overr
expect(ChartDataSeriesSchema.parse(plain).type).toBeUndefined();
});
});

describe('objectui#6121 — the two report data-source keys are retired on both faces', () => {
// 3a. THE TYPE PIN. `?: never` resolves the member type to `undefined`, so
// this line fails if either key is restored to `DataSource` / `DataSource[]`
// — or to any other value type, including the `ViewData` binding whose key
// name is still an open question on this card.
type RetiredDataSource = ReportComponentSchema['dataSource'];
type RetiredDataSources = ReportBuilderSchema['dataSources'];
type _DataSourceStaysRetired = Assert<Eq<RetiredDataSource, undefined>>;
type _DataSourcesStayRetired = Assert<Eq<RetiredDataSources, undefined>>;

it('refuses an authored `dataSource` on both faces, by name', () => {
const authored = {
type: 'report' as const,
title: 'Quarterly Sales Analysis',
// The exact face `content/docs/core/report-schema.mdx` used to teach.
dataSource: { provider: 'api', read: { url: '/api/sales', method: 'GET' } },
};

// @ts-expect-error `dataSource` is retired — `?: never` admits no value
const typed: ReportComponentSchema = authored;
expect(typed).toBeTruthy();

const result = ReportComponentZodSchema.safeParse(authored);
expect(result.success).toBe(false);
// The ENVELOPE, not the fact that something failed: one issue, at this
// key's own path, reported as `invalid_type` (what `z.never()` emits) —
// and carrying the tombstone's guidance rather than zod's generic text,
// which is the half `retirementTombstone` exists for.
const issues = result.success ? [] : result.error.issues;
expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'dataSource']]);
expect(issues[0]?.message).toContain('RETIRED (objectui#6121, ADR-0049)');
});

it('refuses an authored `dataSources` on the builder, by name', () => {
const authored = {
type: 'report-builder' as const,
dataSources: [{ provider: 'api', read: { url: '/api/sales' } }],
};

// @ts-expect-error `dataSources` is retired — `?: never` admits no value
const typed: ReportBuilderSchema = authored;
expect(typed).toBeTruthy();

const result = ReportBuilderZodSchema.safeParse(authored);
expect(result.success).toBe(false);
const issues = result.success ? [] : result.error.issues;
expect(issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'dataSources']]);
expect(issues[0]?.message).toContain('RETIRED (objectui#6121, ADR-0049)');
});

// 3b. CONTROLS, in the same run. Two zeros above need two things that fire:
// without these, a mirror that refused EVERYTHING would read as a pass, and
// so would a `.safeParse` that had stopped being called at all.
it('the same report without the retired key still parses, and the row array is untouched', () => {
const report: ReportComponentSchema = {
type: 'report',
title: 'Quarterly Sales Analysis',
// `data` is the report ROW array — a live key with a live read
// (`LegacyReportRenderer` reads `data.length` / `data.map`). It is NOT
// the retired binding, and this control is what keeps the retirement
// above from reading as "reports refuse data".
data: [{ region: 'EMEA', revenue: 1 }],
};
const result = ReportComponentZodSchema.safeParse(report);
expect(result.success).toBe(true);
expect(result.success && result.data.data).toHaveLength(1);

const builder: ReportBuilderSchema = { type: 'report-builder', showPreview: true };
expect(ReportBuilderZodSchema.safeParse(builder).success).toBe(true);
});

// 3c. The guidance reaches the OTHER author-facing channel too — the
// `.describe()` metadata that feeds generated JSON Schema and the docs
// surface. One string, two channels, so they cannot drift apart.
it('publishes the retirement guidance as schema metadata', () => {
const shapeOf = (schema: { shape: Record<string, { description?: string }> }) => schema.shape;
const componentDescribe = shapeOf(
ReportComponentZodSchema as unknown as { shape: Record<string, { description?: string }> },
).dataSource?.description;
const builderDescribe = shapeOf(
ReportBuilderZodSchema as unknown as { shape: Record<string, { description?: string }> },
).dataSources?.description;

expect(componentDescribe).toContain('RETIRED (objectui#6121, ADR-0049)');
expect(builderDescribe).toContain('RETIRED (objectui#6121, ADR-0049)');
// Control for the reader itself: a NON-retired member's description is
// still its own noun, so the two hits above are not "every key says
// RETIRED".
expect(
shapeOf(ReportComponentZodSchema as unknown as { shape: Record<string, { description?: string }> })
.title?.description,
).toBe('Report title');
});
});
49 changes: 44 additions & 5 deletions packages/types/src/reports.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,6 @@ import type { z } from 'zod';
import type { ReportType as SpecReportType } from '@objectstack/spec/ui';
import type { BaseSchema, SchemaNode } from './base.js';
import type { ChartSchema } from './data-display.js';
import type { DataSource } from './data.js';

/**
* Report Export Format
Expand DownExpand Up@@ -375,9 +374,37 @@ export interface ReportComponentSchema extends BaseSchema {
reportType?: ReportType;

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

/**
* Report fields
Expand DownExpand Up@@ -494,9 +521,21 @@ export interface ReportBuilderSchema extends BaseSchema {
report?: ReportComponentSchema;

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

/**
* Available fields
Expand Down
25 changes: 22 additions & 3 deletions packages/types/src/zod/reports.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,7 @@
import { z } from 'zod';
import { BaseSchema, SchemaNodeSchema } from './base.zod.js';
import { ChartSchema } from './data-display.zod.js';
import { handlerKeyRefusal } from './tombstone.zod.js';
import { handlerKeyRefusal, retirementTombstone } from './tombstone.zod.js';

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