diff --git a/.changeset/10940-joined-report-block-from-spec.md b/.changeset/10940-joined-report-block-from-spec.md new file mode 100644 index 0000000000..24062d1385 --- /dev/null +++ b/.changeset/10940-joined-report-block-from-spec.md @@ -0,0 +1,15 @@ +--- +'@object-ui/types': minor +--- + +`JoinedReportBlock` is now the spec's own type, derived from the installed `@objectstack/spec` instead of hand-written (objectui#10940). It is `JoinedReportBlock` from `@objectstack/spec/ui`, the input shape of `JoinedReportBlockSchema`. `JoinedSpecReport.blocks` is now the spec's own `Report.blocks`, made required. It is on the parsed tier, like `SpecReport` itself, so each element is the spec's parsed block, which can be assigned to `JoinedReportBlock`. + +Through `@objectstack/spec` 17.4.0 the spec erased this block to `unknown`, so objectui published its own interface. That interface described the legacy inline-query block. Spec 17.5.0, installed since objectui#11073, types the block as the ADR-0021 dataset-bound block in a closed schema. The spec refuses every key that is only in the old interface. The published type follows the spec, member by member: + +- **Removed (narrowing):** `objectName`, `groupingsDown`, `groupingsAcross`, `filter` and `chart`. The spec's schema refuses all five, and refuses `chart` by name (objectstack#20161). The `[k: string]: unknown` index signature is gone too, so a literal typed `JoinedReportBlock` no longer accepts an undeclared key. +- **Changed concept:** `columns` was a required array of column objects (`{ field, label?, aggregate? }`). It is now an optional array of dimension names (`string[]`) for the across axis of a matrix block. +- **Narrowed:** `label` and `description` still accept a plain string. Their object form is now the spec's inline locale map, where every value is a string. The legacy `{ default, translations }` object is no longer accepted. +- **Added (widening):** `dataset`, `rows`, `values`, `runtimeFilter` (a `FilterCondition`) and `order` (`{ by, direction? }[]`). +- **Unchanged:** `name` (a required string) and `type` (optional `'tabular' | 'summary' | 'matrix'`, defaulting to `tabular`). + +⚠️ This is breaking for TypeScript code that builds or reads legacy inline-query blocks. It is marked `minor` under this repository's version-alignment rule. Nothing changes at runtime: `isJoinedSpecReport` is untouched, and when this change was made no renderer in this repository typed its blocks with either type (the dataset-bound renderer uses its own local shape). diff --git a/.changeset/3162-types-ledger-batch8-verdicts.md b/.changeset/3162-types-ledger-batch8-verdicts.md index fb757ca76f..38bc0380b8 100644 --- a/.changeset/3162-types-ledger-batch8-verdicts.md +++ b/.changeset/3162-types-ledger-batch8-verdicts.md @@ -37,3 +37,9 @@ Later in this same release the separator `label` stopped being a blocker: `Navig separator arm admits exactly the spec separator's keys, and `spec-derived-unions.test.ts` pins that agreement where the blocker stood. Two semantic blockers remain (`visible: boolean`, and `pinned` / `defaultOpen`), so the symbol is still not bindable. The rest of this entry still holds. + +⚠️ **Dated note, 2026-09-30 — `JoinedReportBlock` is no longer erased, and is now derived — objectui#10940.** +Later in this same release, `@objectstack/spec` 17.5.0 (installed by objectui#11073) typed `JoinedReportBlockSchema`, +and objectui#10940 replaced the hand-written `JoinedReportBlock` with the spec's type. Its "still erased" verdict above +and its place among the "declared dialects" no longer hold: the `check:spec-symbols` ALLOW entry for it is retired. +The rest of this entry still holds. diff --git a/packages/types/src/__tests__/report-chart-query-spec-parity.test.ts b/packages/types/src/__tests__/report-chart-query-spec-parity.test.ts index 496f153bc0..7c22d15f98 100644 --- a/packages/types/src/__tests__/report-chart-query-spec-parity.test.ts +++ b/packages/types/src/__tests__/report-chart-query-spec-parity.test.ts @@ -24,11 +24,11 @@ * wearing. The risk is picking a new name the spec ALREADY owns — the * `PageComponentSchema` mistake from objectui#3074 — so each new name is * asserted absent from the spec's export set, types and values alike. - * - **Burnable now, on its own card** (`JoinedReportBlock`): erased through the + * - **Burned down** (`JoinedReportBlock`, objectui#10940): erased through the * published 17.4.0, typed — as a different shape — since 17.5.0, which this - * repository installs since objectui#11073. The tripwire has fired and been - * flipped; the burn-down (a published-type change) is objectui#10940's. See the - * bottom of this file. + * repository installs since objectui#11073. The published type IS the spec's + * now, a published-type change; the bottom of this file pins the derivation + * and, member by member, what replacing the hand-written interface changed. */ import { describe, it, expect } from 'vitest'; @@ -42,8 +42,11 @@ import { ChartTypeSchema as SpecChartTypeSchema, DashboardWidgetSchema as SpecDashboardWidgetSchema, } from '@objectstack/spec/ui'; -import type { JoinedReportBlock as SpecJoinedReportBlock } from '@objectstack/spec/ui'; -import type { JoinedReportBlock as LocalJoinedReportBlock } from '../spec-report.js'; +import type { + JoinedReportBlock as SpecJoinedReportBlock, + JoinedReportBlockParsed as SpecJoinedReportBlockParsed, +} from '@objectstack/spec/ui'; +import type { JoinedReportBlock as PublishedJoinedReportBlock, JoinedSpecReport } from '../spec-report.js'; import { AppContextSelectorSchema } from '../zod/app.zod.js'; import { DashboardWidgetSchema, DashboardWidgetTypeSchema, GlobalFilterSchema } from '../zod/complex.zod.js'; import { DASHBOARD_COMPONENT_WIDGET_TYPES, DASHBOARD_WIDGET_TYPE_EXTENSIONS } from '../complex.js'; @@ -734,78 +737,64 @@ describe('renamed local dialects do not collide with a spec export (objectui#307 }); // ───────────────────────────────────────────────────────────────────────────── -// JoinedReportBlock — erased in the INSTALLED spec, typed on spec `main`, and -// not burnable until the bump that installs the typed one +// JoinedReportBlock — derived from the spec since objectui#10940 // ───────────────────────────────────────────────────────────────────────────── /** * `JoinedReportBlock` collides with a spec export. Through the published * `@objectstack/spec` 17.4.0 the spec annotated `JoinedReportBlockSchema` as a * bare `z.ZodTypeAny`, so `z.input` — and therefore the exported - * `JoinedReportBlock` type — resolves to `unknown`. Re-exporting that would - * replace objectui's block interface with nothing at all: a type-safety + * `JoinedReportBlock` type — resolved to `unknown`. Re-exporting that would have + * replaced objectui's block interface with nothing at all: a type-safety * regression wearing a burn-down's clothes. It is the objectstack#4171 failure * mode one variant wider: `any` is caught by the `0 extends (1 & T)` probe, * `unknown` is not (that probe answers `false` for it), so a triage that only * screens for `any` concludes the symbol is derivable. * - * ## The day the old pin was waiting for came, on spec `main` first - * - * This section used to hold ONE inverted pin, `true satisfies IsUnknown<…>`, - * whose docblock said: "The day the spec types the schema properly, - * `IsUnknown<…>` flips to `false`, `true satisfies false` stops compiling, and - * the failure is the instruction: re-run the triage and burn it down." - * objectstack#20369 (`681868ca7`) removed the annotation, and the pin fired - * where it could first see that — the Spec Main Shape Gate, which compiles this - * file against `@objectstack/spec` built from objectstack `main` (objectui#10916). - * The pull-request type-check still compiles the SAME file against the INSTALLED - * spec, the published 17.4.0, which predates #20369 and still erases the type. - * So whatever stands here must compile against both, until a spec bump - * installs a release that carries #20369. - * - * ## The re-run triage: precise now, and a DIFFERENT shape (guard case 2c, no longer 2b) + * ## Typed upstream, installed here, derived here * - * Against spec `main`, member by member (the reading is on objectui#10916): - * the spec's block is neither `any` nor `unknown`, and it is not the local - * interface either. The spec types the ADR-0021 dataset-bound block — `dataset`, - * `rows`, `columns` as dimension NAMES, `values`, `runtimeFilter`, `order` — in - * a closed schema. The local interface types the legacy inline-query block — - * `objectName`, `columns` as column OBJECTS, `groupingsDown`, `groupingsAcross`, - * `filter`, `chart` — plus an index signature. The spec's schema refuses all - * five of those local-only keys, `chart` by name since objectstack#20161 - * retired it. `name` and `type` agree; `label` / `description` do not quite - * (the local object arm is not a spec inline locale map). + * objectstack#20369 (`681868ca7`) typed the block; `@objectstack/spec` 17.5.0 + * carries it, and this repository installs 17.5.0 since objectui#11073. The + * spec's block is the ADR-0021 dataset-bound one — `dataset`, `rows`, `columns` + * as dimension NAMES, `values`, `runtimeFilter`, `order` — in a closed schema. + * The hand-written interface objectui published typed the legacy inline-query + * block instead — `objectName`, `columns` as column OBJECTS, `groupingsDown`, + * `groupingsAcross`, `filter`, `chart` — plus an index signature; the spec's + * schema refuses all five of those keys, `chart` by name since objectstack#20161. * - * ⇒ The burn-down is owed, and it is not a re-export beside the local - * interface: it REPLACES the published `JoinedReportBlock` (and the `blocks` of - * `JoinedSpecReport`) with the spec's type, which changes a published type and - * is its own slice. It cannot land before the bump: against the installed spec - * the derived type would be `unknown`, the exact regression this section exists - * to stop. + * objectui#10940 REPLACED the published `JoinedReportBlock` with the spec's type, + * and `JoinedSpecReport.blocks` with the spec's own `Report.blocks`: a + * published-type change, narrowing and widening, stated member by member in its + * changeset. The divergence probes that stood here — local interface against + * spec, each written to stop compiling the day its difference moved — are + * answered by that replacement and are deleted with the interface. * - * ## What guards it now: one state, on both runs (objectui#11073) + * ## What guards it now, on both runs * - * - COMPILE TIME, both runs. Until the bump that installed `@objectstack/spec` - * 17.5.0 the state pin admitted `erased` as well as `typed`, because the - * pull-request type-check read the erasing 17.4.0 while the Spec Main Shape - * Gate read `main`. 17.5.0 carries #20369, so both runs now read a typed - * block, and the pin admits `typed` ALONE: an erasure — on the installed - * spec or on `main` — stops compiling. `any` is refused as before. The - * divergence lines under it each name one difference and stop compiling the - * day that one moves, on the Spec Main Shape Gate first. - * - TEST TIME, the resolved spec only (the Spec Main Shape Gate type-checks and - * runs no test). The tripwire at the bottom reads the resolved spec's built - * declarations. It fired at that bump, as it was written to, and was flipped: - * it now expects `typed`, so an erasure is reported as the upstream - * regression it would be. The burn-down it announced — replacing the - * PUBLISHED `JoinedReportBlock` with the spec's type — is objectui#10940's - * own slice, not the bump's. + * This file is compiled by the pull-request type-check against the INSTALLED + * spec, and by the Spec Main Shape Gate against spec `main`. * - * The window that was NOT guardable from here before that bump — an erasure on - * objectstack `main` looked, at the type level, exactly like the erasing - * 17.4.0 the pull-request run read — closed with the `erased` arm. The - * producer's own pin (`packages/spec/src/ui/joined-report-block-type.test.ts` - * in objectstack, landed with #20369) still covers it upstream. + * - THE STATE PIN admits `typed` alone. The `erased` arm is gone from the pin + * AND from under it: the `OnTypedSpec` licence that answered `true` for every + * probe on an erased spec is deleted, so every probe below is asked on every + * compile and an erasure, on either spec, stops compiling. `any` is refused + * as before. + * - THE DERIVATION PINS: the published block IS the spec's block, and the + * published joined report's `blocks` ARE the spec's report blocks. Re-forking + * the block by hand under the spec's name is also refused by + * `check:spec-symbols`, whose ALLOW row for this symbol retired with the + * burn-down. + * - THE MEMBER PINS state, on the PUBLISHED type, what the replacement changed. + * Each looks its member up through `BlockMember`, which yields `never` for a + * key the type does not declare — and `never` satisfies a `[never] extends [X]` + * probe vacuously. So every lookup is guarded, `[BlockMember OF …] extends + * [never] ? false : …`, and a member that disappears turns ITS OWN probe red + * rather than only some other line of the file. That gap was recorded on the + * probes these replace by the contract review of PR objectui#10934, posted on + * objectui#10940. + * - TEST TIME, the installed spec only (the Spec Main Shape Gate type-checks and + * runs no test): the tripwire at the bottom reads the installed spec's built + * declarations and reports an erasure as the upstream regression it would be. * * ⚠️ objectstack#4171 (CLOSED 2026-07-30) was never this symbol's release: it * typed the RECURSIVE schemas, and this erasure's cause was the bare @@ -817,89 +806,133 @@ type IsAny = 0 extends 1 & T ? true : false; /** `any` is asked first: it passes `IsUnknown` as well. */ type SpecTyping = IsAny extends true ? 'any' : IsUnknown extends true ? 'erased' : 'typed'; -// THE STATE PIN. `typed` — the installed `@objectstack/spec` 17.5.0 and spec -// `main` since objectstack#20369 both type the block. The `erased` arm (the -// published 17.4.0) was deleted at the bump that installed 17.5.0 (objectui#11073), -// as the tripwire below instructed, so an erasure stops compiling again on both -// runs. `any` is refused (case 2). +// THE STATE PIN. `typed` — the installed `@objectstack/spec` and spec `main` both +// type the block. There is no `erased` arm: an erasure stops compiling. `any` is +// refused (case 2). const _specJoinedReportBlockTyping = null as unknown as SpecTyping satisfies 'typed'; const _specJoinedReportBlockIsNotEvenAny = false satisfies IsAny; -/** - * A divergence probe, asked of a TYPED spec only; on an erased spec the answer - * would be `true` by construction. That licence was the `erased` arm of the - * state pin, deleted at the 17.5.0 bump (objectui#11073), so today every probe - * below is asked. - */ -type OnTypedSpec = SpecTyping extends 'typed' ? Probe : true; -/** Keys a type DECLARES: the local `[k: string]: unknown` is not a member. */ +/** Keys a type DECLARES: an index signature is not a member. */ type DeclaredBlockKeys = keyof { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K]; }; type SameType = [A] extends [B] ? ([B] extends [A] ? true : false) : false; -/** `T[K]`, or `never` where `T` is the erased `unknown` (which declares no `K`). */ +/** + * `T[K]`, or `never` where `T` does not declare `K`. That `never` is why every + * member probe below asks `[BlockMember OF …] extends [never]` first. + */ type BlockMember = K extends keyof T ? T[K] : never; -// 0. Umbrella: neither block is the other, in either direction. The lines under -// it say why, and are the ones to act on. -const _localBlockIsNotTheSpecBlock = true satisfies OnTypedSpec< - [LocalJoinedReportBlock] extends [SpecJoinedReportBlock] +// 0. The derivation. The published block is neither erased nor `any`, and it is +// the spec's block in both directions. Mutual assignability alone cannot see +// an OPTIONAL key dropped or added (every block member but `name` is +// optional), so the declared key sets are compared as well. +const _publishedBlockTyping = null as unknown as SpecTyping satisfies 'typed'; +const _publishedBlockIsTheSpecBlock = true satisfies ( + SameType extends true + ? SameType, DeclaredBlockKeys> + : false +); +// …and a published joined report's `blocks` are the spec's own report blocks, +// on the PARSED tier of `SpecReport`: required, each element the spec's parsed +// block, assignable to the authoring-tier block but not the other way round +// (`type` and `order[].direction` are defaulted on the parsed side). +type PublishedJoinedBlocks = BlockMember; +const _joinedReportBlocksAreTheSpecReportBlocks = true satisfies ( + [PublishedJoinedBlocks] extends [never] ? false - : [SpecJoinedReportBlock] extends [LocalJoinedReportBlock] + : undefined extends PublishedJoinedBlocks ? false - : true ->; -// 1. The ADR-0021 dataset-bound selection: declared by the spec, by the local -// interface not at all. -const _specOnlyBlockKeys = true satisfies OnTypedSpec< - SameType< - Exclude, DeclaredBlockKeys>, - 'dataset' | 'rows' | 'values' | 'runtimeFilter' | 'order' - > ->; -// 2. The legacy inline-query block: declared locally, refused by the spec's -// closed schema — `chart` by name, retired by objectstack#20161. -const _localOnlyBlockKeys = true satisfies OnTypedSpec< - SameType< - Exclude, DeclaredBlockKeys>, - 'objectName' | 'groupingsDown' | 'groupingsAcross' | 'filter' | 'chart' - > ->; -// 3. `columns`: one name, two concepts. The spec's are dimension NAMES across a -// matrix; the local ones are column OBJECTS (`{ field, aggregate, … }`). -type SpecBlockColumns = NonNullable>; -const _blockColumnsAreTwoConcepts = true satisfies OnTypedSpec< - [SpecBlockColumns] extends [string[]] - ? [LocalJoinedReportBlock['columns']] extends [SpecBlockColumns] + : SameType +); +const _joinedReportBlocksAreParsedTier = true satisfies ( + [PublishedJoinedBlocks] extends [never] + ? false + : [SpecJoinedReportBlockParsed] extends [PublishedJoinedReportBlock] + ? [PublishedJoinedReportBlock] extends [SpecJoinedReportBlockParsed] + ? false + : true + : false +); + +// 1. NARROWED: the legacy inline-query keys are gone from the published block, +// and so is the index signature that let any other key through. +type LegacyBlockKey = 'objectName' | 'groupingsDown' | 'groupingsAcross' | 'filter' | 'chart'; +const _legacyBlockKeysAreGone = true satisfies ( + [DeclaredBlockKeys] extends [never] + ? false + : SameType, LegacyBlockKey>, never> +); +const _publishedBlockHasNoIndexSignature = true satisfies ( + [keyof PublishedJoinedReportBlock] extends [never] + ? false + : string extends keyof PublishedJoinedReportBlock ? false : true - : false ->; -// 4. `label` / `description`: the local `{ default, translations }` arm is not a -// spec inline locale map, every value of which is a string. -const _blockLabelObjectArmIsNotASpecLabel = true satisfies OnTypedSpec< - [LocalJoinedReportBlock['label']] extends [BlockMember] +); +// 2. WIDENED: the ADR-0021 dataset-bound selection arrived with the spec's type. +const _datasetBoundBlockKeysArrived = true satisfies ( + [DeclaredBlockKeys] extends [never] + ? false + : ['dataset' | 'rows' | 'values' | 'runtimeFilter' | 'order'] extends [DeclaredBlockKeys] + ? true + : false +); +// 3. CHANGED CONCEPT: `columns` is optional dimension NAMES across a matrix, no +// longer the required column OBJECTS (`{ field, aggregate, … }`). +const _blockColumnsAreDimensionNames = true satisfies ( + [BlockMember] extends [never] + ? false + : SameType, string[] | undefined> +); +// 4. NARROWED: `label` / `description` still take a plain string, and refuse the +// legacy `{ default, translations }` object arm, which is not a spec inline +// locale map (every value of which is a string). One probe per member, so a +// member that disappears is named by its own line. +type LegacyLabelObjectArm = { default: string; translations?: Record }; +const _blockLabelRefusesTheLegacyObjectArm = true satisfies ( + [BlockMember] extends [never] ? false - : [LocalJoinedReportBlock['description']] extends [BlockMember] + : [LegacyLabelObjectArm] extends [BlockMember] ? false - : true ->; -// 5. Agreement: `name` and `type` mean the same on both sides. -const _blockNameAgrees = true satisfies OnTypedSpec< - SameType> ->; -const _blockTypeAgrees = true satisfies OnTypedSpec< - SameType> ->; + : [string] extends [BlockMember] + ? true + : false +); +const _blockDescriptionRefusesTheLegacyObjectArm = true satisfies ( + [BlockMember] extends [never] + ? false + : [LegacyLabelObjectArm] extends [BlockMember] + ? false + : [string] extends [BlockMember] + ? true + : false +); +// 5. UNCHANGED: `name` and `type` mean what the hand-written interface said. +const _blockNameIsUnchanged = true satisfies ( + [BlockMember] extends [never] + ? false + : SameType, string> +); +const _blockTypeIsUnchanged = true satisfies ( + [BlockMember] extends [never] + ? false + : SameType, 'tabular' | 'summary' | 'matrix' | undefined> +); void _specJoinedReportBlockTyping; void _specJoinedReportBlockIsNotEvenAny; -void _localBlockIsNotTheSpecBlock; -void _specOnlyBlockKeys; -void _localOnlyBlockKeys; -void _blockColumnsAreTwoConcepts; -void _blockLabelObjectArmIsNotASpecLabel; -void _blockNameAgrees; -void _blockTypeAgrees; +void _publishedBlockTyping; +void _publishedBlockIsTheSpecBlock; +void _joinedReportBlocksAreTheSpecReportBlocks; +void _joinedReportBlocksAreParsedTier; +void _legacyBlockKeysAreGone; +void _publishedBlockHasNoIndexSignature; +void _datasetBoundBlockKeysArrived; +void _blockColumnsAreDimensionNames; +void _blockLabelRefusesTheLegacyObjectArm; +void _blockDescriptionRefusesTheLegacyObjectArm; +void _blockNameIsUnchanged; +void _blockTypeIsUnchanged; /** * How the INSTALLED spec declares an export: the compiler's reading of its @@ -917,7 +950,7 @@ function installedSpecTyping(name: string): { version: string; typing: 'any' | ' return { version, typing }; } -describe('JoinedReportBlock burn-down tripwire, read off the INSTALLED spec (objectui#10916)', () => { +describe('JoinedReportBlock: the installed spec types the block the published type derives from (objectui#10940)', () => { const installed = installedSpecTyping('JoinedReportBlock'); it('reads a typed export as `typed` (the lit control for the rows below)', () => { @@ -927,17 +960,17 @@ describe('JoinedReportBlock burn-down tripwire, read off the INSTALLED spec (obj expect(installedSpecTyping('ReportSort').typing).toBe('typed'); }); - // FLIPPED at the bump (objectui#11073). This row read "the installed spec still - // erases the block" and fired when `@objectstack/spec` 17.5.0 was installed, which - // types it; its companion version row ("still the published 17.4.0") retired with - // it, having done its one job. The state pin's `erased` arm is deleted above. The - // burn-down it announced — deriving the PUBLISHED `JoinedReportBlock` from the - // spec, a published-type change — is objectui#10940's own slice, not this bump's. - it('the installed spec types the block (the erasure is gone; an erasure is an upstream regression)', () => { + // Was the objectui#10916 burn-down tripwire. It fired at the bump that installed + // `@objectstack/spec` 17.5.0 (objectui#11073) and was flipped there; the burn-down + // it announced landed in objectui#10940, which derived the PUBLISHED + // `JoinedReportBlock` from the spec. What it guards now: the published type IS + // the spec's, so an upstream erasure would erase objectui's published block too. + it('the installed spec types the block (an erasure is an upstream regression)', () => { expect( installed.typing, `the installed @objectstack/spec ${installed.version} erases JoinedReportBlock again — an ` + - 'upstream regression to report; the state pin above refuses it at compile time too', + "upstream regression to report, and it erases @object-ui/types' published JoinedReportBlock " + + 'with it; the state pin above refuses it at compile time too', ).toBe('typed'); }); }); diff --git a/packages/types/src/__tests__/spec-derived-unions.test.ts b/packages/types/src/__tests__/spec-derived-unions.test.ts index 5e27ccf61a..d405a56ec6 100644 --- a/packages/types/src/__tests__/spec-derived-unions.test.ts +++ b/packages/types/src/__tests__/spec-derived-unions.test.ts @@ -203,9 +203,11 @@ const _validationErrorShape: ValidationError = { field: 'name', message: 'requir // - `NavigationItemSchema`— upstream IS precise now; pinned below. The live // blocker is SHAPE, and it is a RUNTIME one: see // `navigation-spec-parity.test.ts`. -// - `JoinedReportBlock` — STILL erased, to `unknown`, by a cause #4171 never -// covered. Pinned in -// `report-chart-query-spec-parity.test.ts`. +// - `JoinedReportBlock` — STILL erased then, to `unknown`, by a cause #4171 never +// covered. Since BURNED DOWN: objectstack#20369 typed +// it, spec 17.5.0 ships that, and objectui#10940 +// derived the published type from it. The derivation +// is pinned in `report-chart-query-spec-parity.test.ts`. // // So the batch burns down to zero re-exports and the ledger's remaining debt is // now carried as state pins that name their own release condition, rather than as diff --git a/packages/types/src/spec-report.ts b/packages/types/src/spec-report.ts index 512e5b333f..cba69323ab 100644 --- a/packages/types/src/spec-report.ts +++ b/packages/types/src/spec-report.ts @@ -53,6 +53,7 @@ import type { Report as SpecReportInputType_, ReportChartParsed as SpecReportChartType_, ReportChart as SpecReportChartInputType_, + JoinedReportBlock as SpecJoinedReportBlockType_, } from '@objectstack/spec/ui'; import { @@ -261,72 +262,45 @@ export function isSpecReport(value: unknown): value is SpecReport { } // --------------------------------------------------------------------------- -// Joined Report (UI-layer extension to the spec) +// Joined Report — derived from the spec (objectui#10940) // --------------------------------------------------------------------------- /** - * UI-layer extension for `type: 'joined'` reports. + * A single block inside a `type: 'joined'` report: the spec's own + * `JoinedReportBlock` (`@objectstack/spec/ui`), the INPUT shape of + * `JoinedReportBlockSchema` — the same name with the same meaning it has + * upstream, so the authoring tier is not silently swapped for the parsed one. * - * The upstream spec (`@objectstack/spec`) declares the `'joined'` report type - * but does not yet define how the constituent blocks are carried in the JSON. - * ObjectUI bridges that gap with a `blocks` field on the report: each block is - * a fully self-contained `SpecReport` rendered independently (its own data - * fetch, aggregations, drill), stacked vertically. + * Each block is an independently queried, ADR-0021 dataset-bound sub-report: + * it names its own `dataset`, selects `rows` / `columns` / `values` by + * dimension and measure NAME, may declare its own `runtimeFilter` (merged with + * the container's at query time) and orders itself through `order`. Its `type` + * defaults to `tabular`, and a block cannot itself be `joined` (no recursion). + * The schema is closed: an undeclared key is refused, and `chart` is refused + * by name (objectstack#20161). * - * Semantics: - * - `report.filter` is merged into every block as a logical `$and` (block - * filters take precedence on key collisions — they're the "more specific" - * constraint by convention). This lets a top-level report-wide filter - * (e.g. "owner = me") flow down to all blocks without repetition. - * - Each block keeps its own `objectName`, so blocks may query different - * objects (e.g. new customers + churned customers + silent customers). - * - `actionRunner`, `dataSource`, `drillView`, `drillOpenIn` propagate - * uniformly so any block's drill behaves like a standalone report. - * - * Status: forward-compatible. When the upstream spec adopts a `blocks` field - * the only churn here will be replacing this interface with a re-export. - */ -/** - * A single block inside a `type: 'joined'` report. - * - * The block IS a (constrained) report — its `columns`, `groupingsDown`, - * `groupingsAcross`, `filter`, `chart` etc. are merged with the block's own - * `objectName` (or the joined container's `objectName` as fallback) at render - * time. Blocks cannot themselves be `joined` (no recursion). + * This replaced a hand-written interface that typed the legacy inline-query + * block (`objectName`, `columns` as column objects, `groupingsDown`, + * `groupingsAcross`, `filter`, `chart`, an index signature) while the spec + * still erased its schema to `unknown` (through `@objectstack/spec` 17.4.0). + * The spec's block has been typed since 17.5.0, so the local shape is gone + * rather than kept beside it. */ -export interface JoinedReportBlock { - /** Stable name within the joined report — used for React keys + drill scoping. */ - name: string; - /** Display label rendered above the block. Falls back to `name`. */ - label?: string | { default: string; translations?: Record }; - /** Optional description rendered below the label. */ - description?: string | { default: string; translations?: Record }; - /** Block report type. `joined` is excluded — no recursion. Defaults to `tabular`. */ - type?: 'tabular' | 'summary' | 'matrix'; - /** Object queried by this block. Defaults to the container's `objectName`. */ - objectName?: string; - /** Columns to display / aggregate. Same shape as `Report.columns`. */ - columns: Array<{ field: string; label?: string; aggregate?: string; [k: string]: unknown }>; - /** Row groupings. */ - groupingsDown?: Array<{ field: string; sortOrder?: 'asc' | 'desc'; dateGranularity?: string; [k: string]: unknown }>; - /** Column groupings — only meaningful when `type: matrix`. */ - groupingsAcross?: Array<{ field: string; sortOrder?: 'asc' | 'desc'; dateGranularity?: string; [k: string]: unknown }>; - /** Block-specific filter, ANDed with the container filter at render time. */ - filter?: Record; - /** Optional inline chart configuration. */ - chart?: Record; - /** Forward-compatibility escape hatch. */ - [k: string]: unknown; -} +export type JoinedReportBlock = SpecJoinedReportBlockType_; /** * A `SpecReport` of `type: 'joined'` carrying its constituent blocks. * Use this as the schema input to `ReportRenderer` (dataset-bound blocks render * through `DatasetReportRenderer`). + * + * `blocks` is the spec's own `Report.blocks`, made required. It is on the + * PARSED tier, like `SpecReport` itself (`type` and each `order[].direction` + * are defaulted), so each element is assignable to the authoring-tier + * {@link JoinedReportBlock}, but not the other way round. */ export type JoinedSpecReport = SpecReport & { type: 'joined'; - blocks: JoinedReportBlock[]; + blocks: NonNullable; }; /** Type guard for joined reports with a `blocks` array. */ diff --git a/scripts/check-spec-symbol-derivation.mjs b/scripts/check-spec-symbol-derivation.mjs index 97f75bd06c..572e50ff72 100644 --- a/scripts/check-spec-symbol-derivation.mjs +++ b/scripts/check-spec-symbol-derivation.mjs @@ -687,23 +687,6 @@ const ALLOW = { "Converging on the union is a breaking change tracked separately.", issue: 4115, }, - "@object-ui/types:JoinedReportBlock": { - reason: - "Two specs, two cases (objectui#10916). The INSTALLED spec, the published 17.4.0, is guard " + - "header case 2b: it declares `JoinedReportBlockSchema` as a bare `z.ZodTypeAny`, so its " + - "exported type resolves to `unknown`, and re-exporting would replace this package's block " + - "interface with nothing at all. objectstack `main` typed it in objectstack#20369, and there it " + - "is case 2c: precise, but a DIFFERENT shape — the ADR-0021 dataset-bound block (`dataset`/" + - "`rows`/`columns` as names/`values`/`runtimeFilter`/`order`) in a closed schema, against this " + - "package's legacy inline-query block (`objectName`/`columns` as objects/`groupingsDown`/" + - "`groupingsAcross`/`filter`/`chart`), all five of whose local-only keys the spec refuses. The " + - "burn-down is owed at the first spec bump past 17.4.0, and it REPLACES the published " + - "interface with the spec's type in its own slice (a published type changes); before that bump " + - "the derived type would be `unknown`. packages/types/src/__tests__/report-chart-query-spec-parity.test.ts " + - "pins each divergence at compile time and carries the test-time tripwire that fails at that " + - "bump — that tripwire, not the state of any upstream issue, is the release condition.", - issue: 4115, - }, "@object-ui/types:SelectOption": { reason: "TS twin of the SelectOptionSchema dialect (objectui#3090): carries every spec key " + @@ -906,10 +889,11 @@ const ALLOW = { // Both sets live in `spec-derived-unions.test.ts` / // `validation-rule-spec-parity.test.ts`, written to fail the day the // blocker they name lifts. -// 2b. The SPEC export resolves to `unknown` (`JoinedReportBlock` in the -// published 17.4.0, whose `JoinedReportBlockSchema` that release declares -// as `z.ZodTypeAny`; objectstack `main` typed it in objectstack#20369, -// which moves it to case 2c there — see its ALLOW entry). Just as +// 2b. The SPEC export resolves to `unknown` (`JoinedReportBlock` through the +// published 17.4.0, whose `JoinedReportBlockSchema` those releases declare +// as `z.ZodTypeAny`; objectstack#20369 typed it, 17.5.0 ships that, and +// objectui#10940 derived the published type once 17.5.0 was installed — its +// ALLOW entry retired there). Just as // empty as case 2 and just as unburnable, but the `any` probe reports // `false` for it, so a triage that only screens for `any` waves it through // as "safely derivable". Detect: `[unknown] extends [Spec]`. Pinned in