From 61b382d931cfe28998453f8af94548882224b574 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 15:31:24 +0000 Subject: [PATCH 1/3] fix(spec): InlineAction, ViewMetadataParsed and AssembledViewArtifact(Parsed) name their shapes, not unknown Each alias was derived from a schema whose own static type erases to `unknown` (a z.preprocess input, or a union whose members are cast to z.ZodTypeAny), so any value type-checked against it. Each is now derived from the member schema the parse actually runs, as ViewMetadata already is. diagnoseViewMetadata asserts the union's parse output to the new ViewMetadataParsed, restoring the type the member cast erased; no value changes. Type pins per site plus a patch changeset. JoinedReportBlock is untouched: its region is held by an open PR. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude --- .../19920-exported-types-not-unknown.md | 19 +++++ packages/spec/src/ui/action.zod.ts | 17 +++- .../ui/assembled-view-artifact-type.test.ts | 80 +++++++++++++++++++ packages/spec/src/ui/assembled-views.zod.ts | 26 +++++- .../spec/src/ui/inline-action-type.test.ts | 73 +++++++++++++++++ .../spec/src/ui/view-metadata-type.test.ts | 26 ++++++ packages/spec/src/ui/view.zod.ts | 27 ++++++- 7 files changed, 260 insertions(+), 8 deletions(-) create mode 100644 .changeset/19920-exported-types-not-unknown.md create mode 100644 packages/spec/src/ui/assembled-view-artifact-type.test.ts create mode 100644 packages/spec/src/ui/inline-action-type.test.ts diff --git a/.changeset/19920-exported-types-not-unknown.md b/.changeset/19920-exported-types-not-unknown.md new file mode 100644 index 00000000000..f0072c49b99 --- /dev/null +++ b/.changeset/19920-exported-types-not-unknown.md @@ -0,0 +1,19 @@ +--- +'@objectstack/spec': patch +--- + +fix(spec): `InlineAction`, `ViewMetadataParsed`, `AssembledViewArtifact` and `AssembledViewArtifactParsed` name the shapes their TSDoc promises instead of being `unknown` (#19920) + +Clause-②: no + +Types only. No schema, no parse and no export moves: every runtime accept set is unchanged, and so is the declared type of every schema. Four published type aliases were derived from a schema whose own static type erases to `unknown`, so any value type-checked against them. Each is now derived from the member schema the parse actually runs: + +- `InlineAction`: FROM `z.input` (`unknown`, because the schema is a `z.preprocess` whose input is the preprocess function's `unknown` parameter) TO `z.input<(typeof InlineActionSchema)['out']>`, the input type of the picked action object. +- `ViewMetadataParsed`: FROM `z.infer` (`unknown`, because the union's members are cast to `z.ZodTypeAny` where it is built) TO the union of the OUTPUT types of `VIEW_METADATA_MEMBERS`, the same record `ViewMetadata` reads its input types from. `diagnoseViewMetadata` keeps returning the schema's own parse output as `data`; only that value's static type changes. +- `AssembledViewArtifact` / `AssembledViewArtifactParsed`: FROM `z.input` / `z.infer` of `AssembledViewArtifactSchema` (`unknown`, the same cast) TO the input / output union of the three non-container `VIEW_METADATA_MEMBERS`, the members that schema's union is mapped from. A container body is now a compile error here, as it always was at the schema. + +**If your code stops compiling.** A value you annotated with one of these names is not the shape the name describes: correct it, or type a value that is still unvalidated as `unknown` and let the schema's `safeParse` decide. For `InlineAction`, the legacy `type: 'navigation'` and `to` spellings are refused by the type while `InlineActionSchema` still folds them onto `url` / `target`: write `type: 'url'` and `target`. + +The types are the members' declared shapes, not the schemas' verdicts. Each schema still accepts some bodies its type refuses (the preprocess folds and strips) and still refuses some bodies its type admits (refinements are not types), so the schema remains the only judge. + +`JoinedReportBlock` is not changed by this release. It is still `unknown`. diff --git a/packages/spec/src/ui/action.zod.ts b/packages/spec/src/ui/action.zod.ts index 12f089f7a25..9915b849a6a 100644 --- a/packages/spec/src/ui/action.zod.ts +++ b/packages/spec/src/ui/action.zod.ts @@ -2158,7 +2158,22 @@ export const InlineActionSchema = lazySchema(() => z.preprocess( }), )); -export type InlineAction = z.input; +/** + * An inline action as an author writes it: the INPUT type of the object {@link InlineActionSchema} + * hands its body to, read off the pipe's `out` member. + * + * [#19920] Deliberately NOT `z.input`. That schema is a `z.preprocess`, + * whose input type is the preprocess function's parameter — `unknown` for + * {@link normalizeInlineAction} — so this name used to type-check any value at all. The pipe's + * `out` member is the `.pick()`ed object itself, so the type now carries the shape its fields + * declare. `inline-action-type.test.ts` pins both halves. + * + * A static type, not the door's verdict, in both directions: the door accepts bodies this type + * refuses (the legacy `type: 'navigation'` and `to` spellings, which the preprocess folds onto + * `url` / `target`, are canonical-only here on purpose), and refuses bodies it admits (the + * `target`-required refinement is not a type). `InlineActionSchema` remains the only judge. + */ +export type InlineAction = z.input<(typeof InlineActionSchema)['out']>; /** Post-parse shape of {@link InlineAction} — defaults applied, transforms run (ADR-0122). */ export type InlineActionParsed = z.infer; diff --git a/packages/spec/src/ui/assembled-view-artifact-type.test.ts b/packages/spec/src/ui/assembled-view-artifact-type.test.ts new file mode 100644 index 00000000000..4bc0dc6df3f --- /dev/null +++ b/packages/spec/src/ui/assembled-view-artifact-type.test.ts @@ -0,0 +1,80 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#19920] The published types `AssembledViewArtifact` / `AssembledViewArtifactParsed` name a + * non-container view artifact; they are not `unknown`. + * + * `AssembledViewArtifactSchema`'s union is built from `VIEW_METADATA_MEMBERS` values cast to + * `z.ZodTypeAny`, so both types, derived from the schema itself, WERE `unknown`: any value — a + * container included — type-checked against a name that promises one `viewItems:` entry. They are + * now the union of the input (resp. output) types of the three non-container members. + * + * Two halves, judged by two programs (the `view-metadata-type.test.ts` shape): + * + * - The TYPE half is judged by `tsc -p tsconfig.test.json` (the package's `typecheck` script, via + * `check:test-typecheck`), not by vitest. Each `@ts-expect-error` below asserts that its line + * does NOT compile; while the types were `unknown` every one of them compiled, so each directive + * was unused (TS2578), which reds the gate. The bodies are typed through the published names, so + * a later narrowing that drops a member is a compile error on its line. + * - The RUNTIME half ties those typed bodies to the schema: each one parses, and its parse output + * is a value of `AssembledViewArtifactParsed`. + */ + +import { describe, it, expect } from 'vitest'; +import { + AssembledViewArtifactSchema, + type AssembledViewArtifact, + type AssembledViewArtifactParsed, +} from './assembled-views.zod'; +import { VIEW_METADATA_BRANCHES, VIEW_METADATA_MEMBERS, type ViewMetadataBranch } from './view.zod'; + +type ArtifactBranch = Exclude; + +// ── One body per non-container member, each typed through the published name ───────────────── + +const viewItem: AssembledViewArtifact = { + name: 'crm_lead.all', + object: 'crm_lead', + viewKind: 'list', + config: { type: 'grid', data: { provider: 'object', object: 'crm_lead' }, columns: ['name'] }, +}; +const listOverlay: AssembledViewArtifact = { type: 'grid', columns: ['name'], object: 'crm_lead', viewKind: 'list' }; +const formOverlay: AssembledViewArtifact = { + type: 'simple', + sections: [{ label: 'Main', fields: ['name'] }], + object: 'crm_lead', + viewKind: 'form', +}; + +const BODY_OF_EACH_MEMBER: Record = { viewItem, listOverlay, formOverlay }; + +// ── What the two types refuse at compile time ──────────────────────────────────────────────── + +const someValue: unknown = JSON.parse('{"nope":1}'); +// @ts-expect-error -- `unknown` is not an artifact; it was assignable while AssembledViewArtifact was `unknown`. +const fromUnknown: AssembledViewArtifact = someValue; +const CONTAINER_LIST = { type: 'grid', data: { provider: 'object', object: 'crm_lead' }, columns: ['name'] } as const; +// @ts-expect-error -- a container travels in `views:`, never in `viewItems:`; no artifact member admits a `list` config. +const container: AssembledViewArtifact = { object: 'crm_lead', list: CONTAINER_LIST }; +// @ts-expect-error -- `notAViewKey` is declared by no member (TS2353). +const undeclaredKey: AssembledViewArtifact = { type: 'grid', columns: ['name'], object: 'crm_lead', viewKind: 'list', notAViewKey: 1 }; +// @ts-expect-error -- `unknown` is not a parsed artifact either; it was assignable while AssembledViewArtifactParsed was `unknown`. +const parsedFromUnknown: AssembledViewArtifactParsed = someValue; +void [fromUnknown, container, undeclaredKey, parsedFromUnknown]; + +describe('[#19920] AssembledViewArtifact is a non-container view artifact, not unknown', () => { + it('has a typed body for every non-container member', () => { + expect(Object.keys(BODY_OF_EACH_MEMBER).sort()).toEqual( + VIEW_METADATA_BRANCHES.filter((branch) => branch !== 'container').sort(), + ); + }); + + for (const branch of Object.keys(BODY_OF_EACH_MEMBER) as ArtifactBranch[]) { + it(`the ${branch} body typed as AssembledViewArtifact parses, through the ${branch} member`, () => { + const body = BODY_OF_EACH_MEMBER[branch]; + expect(AssembledViewArtifactSchema.safeParse(body).success).toBe(true); + const parsed: AssembledViewArtifactParsed = VIEW_METADATA_MEMBERS[branch].parse(body); + expect(parsed).toMatchObject({ object: 'crm_lead' }); + }); + } +}); diff --git a/packages/spec/src/ui/assembled-views.zod.ts b/packages/spec/src/ui/assembled-views.zod.ts index 1f79dbd99ec..2ad85131f62 100644 --- a/packages/spec/src/ui/assembled-views.zod.ts +++ b/packages/spec/src/ui/assembled-views.zod.ts @@ -63,6 +63,7 @@ import { VIEW_METADATA_MEMBERS, expandViewContainer, isAggregatedViewContainer, + type ViewMetadataBranch, } from './view.zod'; /** @@ -98,10 +99,27 @@ export const AssembledViewArtifactSchema = lazySchema(() => { ); }); -/** One assembled `viewItems:` entry (input shape). */ -export type AssembledViewArtifact = z.input; -/** Post-parse shape of {@link AssembledViewArtifact} — defaults applied, transforms run (ADR-0122). */ -export type AssembledViewArtifactParsed = z.infer; +/** + * One assembled `viewItems:` entry (input shape): the union of the INPUT types of the + * non-container members of {@link VIEW_METADATA_MEMBERS} — the same members + * {@link AssembledViewArtifactSchema}'s union is mapped from, so the two cannot drift. + * + * [#19920] Deliberately NOT `z.input`: the union's members are + * cast to `z.ZodTypeAny` where it is built, so every type derived from the schema itself is + * `unknown`, and this name used to type-check any value — a container included. + * `assembled-view-artifact-type.test.ts` pins that `unknown` and a container are refused here and + * that a body of each member type-checks. + * + * A static type, not the schema's verdict: the members' refinements are not types, and TypeScript + * checks an object literal's keys against the union as a whole. `AssembledViewArtifactSchema` + * remains the only judge. + */ +export type AssembledViewArtifact = z.input<(typeof VIEW_METADATA_MEMBERS)[Exclude]>; +/** + * Post-parse shape of {@link AssembledViewArtifact} — defaults applied, transforms run (ADR-0122): + * the union of the same members' OUTPUT types, for the same reason. + */ +export type AssembledViewArtifactParsed = z.infer<(typeof VIEW_METADATA_MEMBERS)[Exclude]>; /** Result of {@link partitionAssembledViewArtifacts}. */ export interface AssembledViewPartition { diff --git a/packages/spec/src/ui/inline-action-type.test.ts b/packages/spec/src/ui/inline-action-type.test.ts new file mode 100644 index 00000000000..dda74e15c2c --- /dev/null +++ b/packages/spec/src/ui/inline-action-type.test.ts @@ -0,0 +1,73 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#19920] The published type `InlineAction` names an inline action body; it is not `unknown`. + * + * `InlineActionSchema` is a `z.preprocess`, whose input type is the preprocess function's + * parameter (`unknown`), so the type declared as `z.input` WAS + * `unknown`: any value type-checked against a name that promises an inline action. It is now the + * input type of the pipe's `out` member, the `.pick()`ed action object. + * + * Two halves, judged by two programs (the `view-metadata-type.test.ts` shape): + * + * - The TYPE half is judged by `tsc -p tsconfig.test.json` (the package's `typecheck` script, via + * `check:test-typecheck`), not by vitest. Each `@ts-expect-error` below asserts that its line + * does NOT compile. While `InlineAction` was `unknown` every one of them compiled, so each + * directive was unused: TS2578 in a file with no `test-typecheck-debt.json` entry, which reds the + * gate. Every key of the input shape is optional (`type` has a default, `name` and `label` are + * `.partial()`), so there is no missing-required-key case to pin here. + * - The RUNTIME half ties the typed bodies to the door: each one parses. It also pins the one + * direction the TSDoc states in words — the door still ACCEPTS the legacy `navigation` / `to` + * spellings this type refuses, folding them onto `url` / `target`. + */ + +import { describe, it, expect } from 'vitest'; +import { InlineActionSchema, type InlineAction } from './action.zod'; + +// ── Real bodies, each typed through the published name ─────────────────────────────────────── + +const urlAction: InlineAction = { type: 'url', target: '/pricing', openIn: 'new-tab' }; +const apiAction: InlineAction = { + type: 'api', + target: '/api/v1/billing/refresh', + method: 'POST', + bodyExtra: { plan: 'pro' }, + confirmText: 'Refresh billing?', + successMessage: 'Refreshed', + refreshAfter: true, +}; +const namedModal: InlineAction = { name: 'open_upgrade', label: 'Upgrade', type: 'modal', target: 'upgrade_dialog' }; + +// ── What `InlineAction` refuses at compile time ────────────────────────────────────────────── + +const someValue: unknown = JSON.parse('{"nope":1}'); +// @ts-expect-error -- `unknown` is not an inline action; it was assignable while InlineAction was `unknown`. +const fromUnknown: InlineAction = someValue; +// @ts-expect-error -- `navigation` is a legacy spelling the preprocess folds; the type names canonical `type` values only. +const legacyType: InlineAction = { type: 'navigation', target: '/pricing' }; +// @ts-expect-error -- `to` is a legacy spelling of `target`, declared by no field (TS2353). +const legacyTo: InlineAction = { type: 'url', to: '/pricing' }; +// @ts-expect-error -- an inline action is an object. +const scalar: InlineAction = 42; +void [fromUnknown, scalar]; + +describe('[#19920] InlineAction is an inline action body, not unknown', () => { + it.each([ + ['a url action', urlAction], + ['an api action with a static payload', apiAction], + ['a named modal action', namedModal], + ])('%s typed as InlineAction parses', (_label, body) => { + expect(InlineActionSchema.safeParse(body).success).toBe(true); + }); + + it('the door still accepts the legacy spellings the type refuses, folding them onto url / target', () => { + const typeFold = InlineActionSchema.safeParse(legacyType); + expect(typeFold.success).toBe(true); + expect(typeFold.data).toMatchObject({ type: 'url', target: '/pricing' }); + + const targetFold = InlineActionSchema.safeParse(legacyTo); + expect(targetFold.success).toBe(true); + expect(targetFold.data).toMatchObject({ type: 'url', target: '/pricing' }); + expect(targetFold.data).not.toHaveProperty('to'); + }); +}); diff --git a/packages/spec/src/ui/view-metadata-type.test.ts b/packages/spec/src/ui/view-metadata-type.test.ts index 936e351db28..8236389fac1 100644 --- a/packages/spec/src/ui/view-metadata-type.test.ts +++ b/packages/spec/src/ui/view-metadata-type.test.ts @@ -23,9 +23,11 @@ import { describe, it, expect } from 'vitest'; import { VIEW_METADATA_BRANCHES, + VIEW_METADATA_MEMBERS, diagnoseViewMetadata, type ViewMetadata, type ViewMetadataBranch, + type ViewMetadataParsed, } from './view.zod'; // ── One body per member, each typed through the published name ──────────────────────────────── @@ -66,6 +68,16 @@ const undeclaredKey: ViewMetadata = { type: 'grid', columns: ['name'], object: ' const scalar: ViewMetadata = 42; void [fromUnknown, undeclaredKey, scalar]; +// ── [#19920] `ViewMetadataParsed`: the members' OUTPUT union, refused the same way ───────────── + +// @ts-expect-error -- `unknown` is not a parsed view body; it was assignable while ViewMetadataParsed was `unknown`. +const parsedFromUnknown: ViewMetadataParsed = someValue; +// @ts-expect-error -- `notAViewKey` is declared by no member's output (TS2353). +const parsedUndeclaredKey: ViewMetadataParsed = { type: 'grid', columns: ['name'], object: 'crm_lead', viewKind: 'list', notAViewKey: 1 }; +// @ts-expect-error -- a parsed view body is an object. +const parsedScalar: ViewMetadataParsed = 42; +void [parsedFromUnknown, parsedUndeclaredKey, parsedScalar]; + describe('[#19871] ViewMetadata is a view body, not unknown', () => { it('has a typed body for every member of the union', () => { expect(Object.keys(BODY_OF_EACH_MEMBER).sort()).toEqual([...VIEW_METADATA_BRANCHES].sort()); @@ -79,3 +91,17 @@ describe('[#19871] ViewMetadata is a view body, not unknown', () => { }); } }); + +describe('[#19920] ViewMetadataParsed is a parsed view body, not unknown', () => { + for (const branch of VIEW_METADATA_BRANCHES) { + it(`the ${branch} member's parse output and diagnoseViewMetadata's data are both ViewMetadataParsed`, () => { + const memberOutput: ViewMetadataParsed = VIEW_METADATA_MEMBERS[branch].parse(BODY_OF_EACH_MEMBER[branch]); + const diagnosis = diagnoseViewMetadata(BODY_OF_EACH_MEMBER[branch]); + if (!diagnosis.success) throw new Error(`the ${branch} body must parse`); + const data: ViewMetadataParsed = diagnosis.data; + // The assertion in diagnoseViewMetadata changes no value: its data is the union's output, + // which is the accepting member's own output. + expect(data).toEqual(memberOutput); + }); + } +}); diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index cce26f86bcc..25d7ad851ed 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -6206,7 +6206,16 @@ export function diagnoseViewMetadata(body: unknown): ViewMetadataDiagnosis { ); // The union accepted, so some member did; `selectViewMetadataBranch` is the // fallback only for the impossible case, never the primary answer. - return { success: true, branch: accepting ?? selectViewMetadataBranch(stripped) ?? 'viewItem', data: parsed.data }; + // + // [#19920] `parsed.data` is typed `unknown` only because the union's members + // are cast to `z.ZodTypeAny` where `ViewMetadataSchema` builds it; at runtime + // it IS one member's output, which is what `ViewMetadataParsed` names. The + // assertion restores the type that cast erased and changes no value. + return { + success: true, + branch: accepting ?? selectViewMetadataBranch(stripped) ?? 'viewItem', + data: parsed.data as ViewMetadataParsed, + }; } // (2) The identity precondition short-circuited the pipe with `z.NEVER`, so @@ -6654,8 +6663,20 @@ export type ViewItemWire = z.input; * optional. `ViewMetadataSchema` remains the only judge. */ export type ViewMetadata = z.input<(typeof VIEW_METADATA_MEMBERS)[ViewMetadataBranch]>; -/** Post-parse shape of {@link ViewMetadata} — defaults applied, transforms run (ADR-0122). */ -export type ViewMetadataParsed = z.infer; +/** + * Post-parse shape of {@link ViewMetadata} — defaults applied, transforms run (ADR-0122): the union + * of the OUTPUT types of the members {@link ViewMetadataSchema}'s union runs, read off + * {@link VIEW_METADATA_MEMBERS} exactly as {@link ViewMetadata} reads their input types. + * + * [#19920] Deliberately NOT `z.infer`, for the reason + * {@link ViewMetadata} gives: the union's members are cast to `z.ZodTypeAny` where the union is + * built, so the schema's own output type is `unknown` and this name used to type-check any value. + * A successful parse returns the output of the one member that accepted the stripped body (the + * union's `.check()` transforms nothing), so every parse result is a value of this type. + * `view-metadata-type.test.ts` pins that `unknown` is refused here and that a parsed body of each + * member type-checks. + */ +export type ViewMetadataParsed = z.infer<(typeof VIEW_METADATA_MEMBERS)[ViewMetadataBranch]>; export type ViewScope = z.input; export type ViewKind = z.input; export type ListView = z.input; From cf1236622004ca0288f637d2e388132c7320c9e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 16:01:10 +0000 Subject: [PATCH 2/3] docs(changeset): keep the ViewMetadataParsed sentences true whichever release carries them The unreleased ViewMetadata changeset said ViewMetadataParsed "is not changed by this release: it is still unknown", which this branch makes false if both land in one release. Both changesets now state what their own change did, which holds under either release order. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude --- .changeset/19920-exported-types-not-unknown.md | 2 +- .changeset/view-metadata-type-not-unknown.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/19920-exported-types-not-unknown.md b/.changeset/19920-exported-types-not-unknown.md index f0072c49b99..2b189e84b62 100644 --- a/.changeset/19920-exported-types-not-unknown.md +++ b/.changeset/19920-exported-types-not-unknown.md @@ -16,4 +16,4 @@ Types only. No schema, no parse and no export moves: every runtime accept set is The types are the members' declared shapes, not the schemas' verdicts. Each schema still accepts some bodies its type refuses (the preprocess folds and strips) and still refuses some bodies its type admits (refinements are not types), so the schema remains the only judge. -`JoinedReportBlock` is not changed by this release. It is still `unknown`. +`JoinedReportBlock` is not changed by this change, and still resolves to `unknown`. diff --git a/.changeset/view-metadata-type-not-unknown.md b/.changeset/view-metadata-type-not-unknown.md index 5a13c0c4360..7b0a4a9ff6b 100644 --- a/.changeset/view-metadata-type-not-unknown.md +++ b/.changeset/view-metadata-type-not-unknown.md @@ -30,7 +30,7 @@ judge. member shapes. Correct the body, or type a value that is still unvalidated as `unknown` and let `ViewMetadataSchema.safeParse` decide. -`ViewMetadataParsed` is not changed by this release: it is still `unknown`. +`ViewMetadataParsed` is not changed by this change. It is re-derived from the same members, as their output types, by its own entry (#19920). The `@objectstack/metadata` changelog entry for #19852 gives `ViewMetadata` being `unknown` as the reason a saved `view` file is written with no annotation; that reason is superseded here, and the From f8792c931bf34e113314e158f2edde1b3cfcbecd Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 16:38:57 +0000 Subject: [PATCH 3/3] docs(changeset): declare the #19920 type narrowing as BREAKING minor, like #19919 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The seat aligned this changeset with the #19919 precedent for the same defect class: level minor, `Clause-②: no (narrowing)`, the BREAKING banner for TypeScript code annotating the four aliases, and the ADR-0087 `not-required (no-migration-prescription)` disposition. The runtime accept set is unchanged; only TypeScript annotations narrow. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude --- .changeset/19920-exported-types-not-unknown.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.changeset/19920-exported-types-not-unknown.md b/.changeset/19920-exported-types-not-unknown.md index 2b189e84b62..bce741afb85 100644 --- a/.changeset/19920-exported-types-not-unknown.md +++ b/.changeset/19920-exported-types-not-unknown.md @@ -1,12 +1,14 @@ --- -'@objectstack/spec': patch +'@objectstack/spec': minor --- fix(spec): `InlineAction`, `ViewMetadataParsed`, `AssembledViewArtifact` and `AssembledViewArtifactParsed` name the shapes their TSDoc promises instead of being `unknown` (#19920) -Clause-②: no +Clause-②: no (narrowing) -Types only. No schema, no parse and no export moves: every runtime accept set is unchanged, and so is the declared type of every schema. Four published type aliases were derived from a schema whose own static type erases to `unknown`, so any value type-checked against them. Each is now derived from the member schema the parse actually runs: +**BREAKING for TypeScript code that annotates with `InlineAction`, `ViewMetadataParsed`, `AssembledViewArtifact` or `AssembledViewArtifactParsed`**: a narrowing of published TYPES, landing in the launch window as `minor` (the lockstep convention: the bump level is not the carrier, this banner and the disposition below are). The runtime accept set does not move at all: no schema, no parse and no export changes, and neither does the declared type of any schema. + +Four published type aliases were derived from a schema whose own static type erases to `unknown`, so any value type-checked against them. Each is now derived from the member schema the parse actually runs: - `InlineAction`: FROM `z.input` (`unknown`, because the schema is a `z.preprocess` whose input is the preprocess function's `unknown` parameter) TO `z.input<(typeof InlineActionSchema)['out']>`, the input type of the picked action object. - `ViewMetadataParsed`: FROM `z.infer` (`unknown`, because the union's members are cast to `z.ZodTypeAny` where it is built) TO the union of the OUTPUT types of `VIEW_METADATA_MEMBERS`, the same record `ViewMetadata` reads its input types from. `diagnoseViewMetadata` keeps returning the schema's own parse output as `data`; only that value's static type changes. @@ -17,3 +19,5 @@ Types only. No schema, no parse and no export moves: every runtime accept set is The types are the members' declared shapes, not the schemas' verdicts. Each schema still accepts some bodies its type refuses (the preprocess folds and strips) and still refuses some bodies its type admits (refinements are not types), so the schema remains the only judge. `JoinedReportBlock` is not changed by this change, and still resolves to `unknown`. + +