Skip to content

Commit 119a02b

Browse files
fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown (#19919)
Fixes #19871 Clause-②: no (narrowing) ## What changed - `packages/spec/src/ui/view.zod.ts`, the type export region only: `ViewMetadata` was `z.input` of `typeof ViewMetadataSchema`, which is exactly `unknown`. It is now `z.input` over `(typeof VIEW_METADATA_MEMBERS)[ViewMetadataBranch]`: the union of the input types of the four members the schema's union runs. The TSDoc says why the type cannot be derived from the schema itself, and what it still does not express. - ⛔ No runtime change. `ViewMetadataSchema`, its members and its `z.preprocess` are untouched, and a 44-body parse probe answers byte-identically before and after (below). - New pin: `packages/spec/src/ui/view-metadata-type.test.ts`. - `packages/metadata/src/serializers/typescript-serializer.ts`: the comment that documented the `unknown` now says why `view` stays unannotated. The reason is the table's own rule (the spec type must equal the bound schema's `z.input`), and it holds for a real reason: the door accepts bodies `ViewMetadata` refuses. One word in the sibling test's docblock ("is" to "once was") keeps it true. - Changeset `.changeset/view-metadata-type-not-unknown.md`: `@objectstack/spec` minor, BREAKING banner, `Clause-②: no (narrowing)`, ADR-0087 disposition `not-required (no-migration-prescription)`. ## Why this type: the reading - The TSDoc promises "any persisted view metadata body: container | ViewItem record | flattened overlay". Under the ADR-0122 naming, the bare name is the input shape and `ViewMetadataParsed` is the output shape. So the fix is the members' INPUT union. - It cannot be read off the schema. The `z.preprocess` input type is `unknown`, and where the union is built its members are cast to `z.ZodTypeAny`, so both `z.input` and `z.infer` of `ViewMetadataSchema` are `unknown`. `VIEW_METADATA_MEMBERS` is the union's member list by construction, so it is read there instead. - Consumers: `git grep -w ViewMetadata` finds no annotation consumer in this repo. objectui has zero hits at the pinned sha `62597c5880` and at its `origin/main` `544ecba`. The control grep for `ViewMetadataSchema` / `VIEW_METADATA_MEMBERS` at the pin answers exit 0 with 9 files, so the negative reading is valid. ## Measurements: before `8490127962` (`origin/main`), after `2f378ec5cf` Type probe, compiled against `src` AND against the BUILT `dist/ui/index.d.mts`, with the same results on both: | reading | before | after | |---|---|---| | `unknown extends ViewMetadata` | holds | does not hold (TS2322) | | a value typed `unknown`, assigned to `ViewMetadata` | compiles | TS2322 | | list overlay plus undeclared key `notAViewKey` | compiles | TS2353 | | `42` | compiles | TS2322 | | the #19852 repro body (`name`, `type: 'grid'`, `object`, `columns`), which the parse rejects | compiles | still compiles, see Residual | | `ViewMetadataParsed` is `unknown` | yes | yes, not changed here | Probe liveness control: at base, a `ListView` literal with an undeclared key reports TS2353 in both programs. `--listFiles` shows 11 spec `dist` files and 36 spec `src` files in the two programs. Runtime acceptance is byte-identical. The probe runs 44 bodies through `ViewMetadataSchema.safeParse`: the `view-union-diagnostics` battery plus the undeclared-key body, the repro and a list overlay with console row ids. It records the full result (verdict, parse output, issue code/path/message). On `src` and on `dist`, before and after, it gives 20 accepted and 24 refused, report sha256 `f652fb343387bf490eda6fd7efdde6330d6103db811f246c8234ded9ee4993d7` all four times. Type against door over the same 44 bodies. Before, all 44 compile (`unknown`). After: | | door accepts | door refuses | |---|---|---| | type-checks | 17 | 8 | | type error | 3 | 16 | - The 3 in "door accepts, type error" are expected. Two carry the console's row `id` on `sort`/`filter` rows, which the preprocess strips. The third is the undeclared key, which a strip-mode member drops. - The 8 in "door refuses, type-checks": - the identity precondition refuses `{}`, name-only and stamped-identity-only bodies; - the container member's refinement refuses an empty `listViews`; - the viewItem member types `config` as `unknown` (see Acceptance notes), so a bad config or a decorated config type-checks; - two bodies mix keys of different members: the #19852 repro and `container.withAux`. ## Residual: the #19852 repro body still type-checks TypeScript checks an object literal's keys against the union AS A WHOLE. The container member (`ViewSchema`) has only optional keys. So once `type` and `columns` are found on another member, `name` + `type` + `object` + `columns` is assignable to the container member. Measured: the same union without the container member refuses the body (TS2741, `viewKind` missing). Each member alone refuses it too. An exclusive union would add, to each member, every other member's keys as optional-never. That refuses the repro and `container.withAux`. But it also refuses four bodies the door accepts and the platform writes itself: `put.isPinned`, `put.sortOrder`, `put.pinAndOrder` and `overlay.badOperator`. Both forms are wrong somewhere, and the dispatch named the plain members' input union. So this PR ships the plain union and documents the gap in the TSDoc. ## Firing control for the pin The fix was committed first. `scripts/ablation-replace.mjs` then put the declaration back to today's spelling (anchor hit 1 to 0, blob `bb6384c31aeb` to `a74e16786768`). `pnpm --filter @objectstack/spec check:test-typecheck` then goes red with "src/ui/view-metadata-type.test.ts: 3 type error(s) in a file the ledger does not cover". tsc names them TS2578 `Unused '@ts-expect-error' directive` at lines 61, 63 and 65. Restored: blob equals HEAD, and `git diff HEAD` is empty. On the fixed tree the same gate is green: 53 files, 255 errors and 142 signatures, held exactly by the unchanged `test-typecheck-debt.json`. ## Generated artifacts, and what `check:api-surface` sees `gen:api-surface`, `gen:export-origins` and `gen:declaration-map` all ran on the rebuilt dist and wrote zero diff ("0 shard(s) rewritten"). `check:api-surface` answers "public API surface + factory signatures unchanged". The snapshot records that an export EXISTS and its kind, not what a type alias resolves to (see the `build-api-surface.ts` header), so this type change is invisible to it. The change is stated here instead: **`ViewMetadata` narrows from `unknown` to the union of the four members' input types.** The dispatch expected `check:api-surface` to report it. Measured, it does not. ## Clause-② reading - **Value `no`.** The runtime accept set does not move (the probe above), and no export is added or removed. - **Arm `(narrowing)`.** A published TYPE narrows from `unknown`: code that assigns a non-view value to a `ViewMetadata` slot stops compiling. ADR-0087's 2026-08-30 addendum reads a published type-surface narrowing as BREAKING, truthfully. - **Level `minor`.** This follows the launch-window convention in the `check-changeset-no-major.mjs` header: breaking-ness is carried by the banner and the disposition, not by the level. - **Disposition `not-required (no-migration-prescription)`.** `type-surface-only` is closed to this diff by its predicate 2 (it touches `packages/spec/**`) and predicate 3 (a `*.zod.ts` moved). Nothing authorable is removed or renamed, and no stored row moves. `check-adr-0087-registration` answers "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition". `check-changeset-no-major` answers "no `major` bump". - The claim's provisional `Clause-②: no` is superseded by `no (narrowing)`. ## Local verification at `2f378ec5cf` - `pnpm --filter @objectstack/spec test`: 528 files, 15508 passed, 1 todo. The new file: 5 passed, one per member plus the coverage check. - `pnpm --filter @objectstack/spec typecheck` (tsc, scripts program, test layer): exit 0. - `pnpm --filter @objectstack/metadata typecheck` and `test`: exit 0, 54 files, 821 passed. - `pnpm --filter @objectstack/spec check:generated`: all 15 artifacts up to date. It ran against a dist built from `aa5156d9de`; the only later commit adds the changeset, so no spec source differs. - `dispatch-gates --ran`: 83 families derived, 81 run with exit 0, 0 unrun, and 2 NOT MEASURED with exit 3, PREREQUISITE NOT MET: `check:dual-build-cjs-loads` and `check:type-check-debt`. Both need the whole workspace built. Neither reads the changed symbol: it has no in-repo importer. The four type-check lanes the tool lists as CI-only are CI's. - `origin/main` moved 2 commits since the base (`beac798026`, `a34c27cbe5`). Neither touches `packages/spec` or `packages/metadata`, so they are not merged here. ## Acceptance notes Sibling census: every exported type alias in `packages/spec/src` that resolves to `unknown`, compiled with the package's own tsconfig at `2f378ec5cf`. 2359 aliases were scanned. ⛔ None of these is changed here; they are listed for the seat. - `InlineAction` (`src/ui/action.zod.ts:2161`) is `z.input` of a `lazySchema` wrapping a `z.preprocess`, so it is `unknown`. It has the same shape as this card's type, in another file. (`InlineActionParsed` resolves to a concrete type.) - `ViewMetadataParsed` (`src/ui/view.zod.ts:6329`) is `unknown`: its `z.infer` reads the union whose members are cast to `z.ZodTypeAny`. Narrowing it the same way is not a one-line change. `diagnoseViewMetadata` returns `data: parsed.data`, and `parsed.data` is `unknown`, which is not assignable to the members' output union (TS2322, measured). So the fix also needs an edit in that function, outside this region. - `AssembledViewArtifact` and `AssembledViewArtifactParsed` (`src/ui/assembled-views.zod.ts:102/104`) are `unknown`. The cause is a union of `z.ZodTypeAny`-cast members (the same `VIEW_METADATA_MEMBERS` values), not a preprocess. The TSDoc says "One assembled `viewItems:` entry (input shape)". - `JoinedReportBlock` (`src/ui/report.zod.ts:407`) is `unknown` because `JoinedReportBlockSchema` is annotated `z.ZodTypeAny`. - `AssignmentValue(Parsed)` and `GetPublishedMetaItemResponse(Parsed)` are `unknown` by design: their schemas are `z.unknown()`. - Nested, not top-level: the viewItem member's `config` is typed `unknown`, because `viewItemArmShape` takes `config: z.ZodTypeAny`. So at the type level `ViewItem`, `ViewItemWire` and this union's viewItem arm accept any `config`. - `check:exported-any` catches a `ZodType` of `any`, not an exported alias that resolves to `unknown`. No gate sees this class today. The census is about 60 lines of TypeScript compiler API and could become one. Not in scope here. ## Patch round (head `3cb0a8f6a`), after the at-tier FAIL `5808373817` Written by the `domain:spec` seat 4 (`session_019c3Hi6ZMU1p6m6aA6Bz45d`), which took the card over on the maintainer's 「你接手派补丁轮」. Two fast-forward commits; no code moved. - `packages/metadata/README.md:77` (`7ca39fe366`): the published parenthetical said `ViewMetadata` is `unknown`, which this PR makes false. It now reads: `ViewMetadataSchema` is a `z.preprocess`, whose `z.input` type is `unknown`; `ViewMetadata` is declared instead as the union of the input types of the members that schema's union runs, so it is not that schema's `z.input` type. That is the TSDoc's own wording, and it is consistent with the serializer comment and the annotation-test docblock. - `.changeset/view-metadata-type-not-unknown.md` (`3cb0a8f6a`): one paragraph naming the still-pending `.changeset/19852-*.md` sentence (`ViewMetadata` is `unknown`) as superseded. That file is not edited, because editing it is the deliberate-correction class. - No second changeset for `@objectstack/metadata`. The README does publish (`files[]`), but the two packages sit in one `fixed` group, so the corrected README ships in the same release, and the false clause never shipped. At-tier re-review `5815765984`: PASS. --- _Generated by [Claude Code](https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent cc6dfd9 commit 119a02b

6 files changed

Lines changed: 149 additions & 7 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec): the published type `ViewMetadata` names a `view` body instead of being `unknown` (#19871)
6+
7+
Clause-②: no (narrowing)
8+
9+
**BREAKING for TypeScript code that annotates with `ViewMetadata`**: a narrowing of a published
10+
TYPE, landing in the launch window as `minor` (the lockstep convention: the bump level is not the
11+
carrier, this banner and the disposition below are). The runtime accept set does not move at all:
12+
`ViewMetadataSchema` is unchanged, and a 44-body parse probe answers byte-identically before and
13+
after.
14+
15+
`ViewMetadata` was declared as `z.input<typeof ViewMetadataSchema>`. That schema is a
16+
`z.preprocess`, whose input type is `unknown`, so the name documented as "any persisted view
17+
metadata body: container | ViewItem record | flattened overlay" type-checked any value at all,
18+
including bodies the schema refuses. It is now the union of the input types of
19+
`VIEW_METADATA_MEMBERS`, the four members the schema's union runs, so `unknown`, a non-object and a
20+
key no member declares are compile errors, and a body of each member still type-checks.
21+
22+
**What still differs from the runtime verdict.** The type is the members' declared shape, not the
23+
door's answer. The door still accepts bodies the type refuses (it removes the console's row `id`s
24+
and three members strip undeclared keys), and still refuses bodies the type admits (the identity
25+
precondition, the members' refinements, and a body mixing keys of different members, which
26+
TypeScript checks against the union as a whole). `ViewMetadataSchema.safeParse` remains the only
27+
judge.
28+
29+
**If your code stops compiling.** A value you annotated as `ViewMetadata` is not one of the four
30+
member shapes. Correct the body, or type a value that is still unvalidated as `unknown` and let
31+
`ViewMetadataSchema.safeParse` decide.
32+
33+
`ViewMetadataParsed` is not changed by this release: it is still `unknown`.
34+
35+
The `@objectstack/metadata` changelog entry for #19852 gives `ViewMetadata` being `unknown` as the
36+
reason a saved `view` file is written with no annotation; that reason is superseded here, and the
37+
outcome stands for another one: `ViewMetadata` is no longer the `z.input` type of
38+
`ViewMetadataSchema`, the schema `getMetadataTypeSchema('view')` binds.
39+
40+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is removed, renamed or re-typed at runtime: no spec key, no export, no stored row moves, and every view body parses byte-identically, so `objectstack migrate meta` has nothing to reach. The only thing that moved is a TypeScript annotation, whose channel is the consumer's compiler. `type-surface-only` does not apply because this diff touches `packages/spec/**` (its predicate 2). -->

‎packages/metadata/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ Serializers convert metadata objects to/from different file formats:
7474

7575
- **JSONSerializer** — `.json` files with optional key sorting
7676
- **YAMLSerializer** — `.yaml`/`.yml` files (JSON_SCHEMA for security)
77-
- **TypeScriptSerializer** — the `typescript` / `javascript` formats (`.ts` / `.js`): a JSON document wrapped in a module, the file format `FilesystemLoader` writes and reads for those two formats (`typescript` is `FilesystemLoader.save()`'s default, so `MetadataManager.save()` routed to the filesystem loader writes `{rootDir}/{type}/{name}.ts`). It writes `export const metadata = { …JSON… };` then `export default metadata;`. A `typescript`-format file that `FilesystemLoader.save()` writes with the built-in serializer the package wires in also annotates that constant with the spec type of the item's metadata type, and imports it: `ServiceObject` from `@objectstack/spec/data` for an `object`, `Flow` from `@objectstack/spec/automation` for a `flow`, and so on. Each is exactly the `z.input` type of the schema `getMetadataTypeSchema()` resolves for that metadata type. A metadata type with no such spec type is written with no annotation and no import: `view` (its `ViewMetadata` type is `unknown`), `book`, `external_catalog`, or a plugin's own type. Only the loader knows the metadata type, so it picks the annotation through a package-internal function, and `TypeScriptSerializer.serialize()` called directly writes no annotation. A serializer wired in by hand (a custom one, a subclass that overrides `serialize()`, or a `TypeScriptSerializer` from the package's other entry point) is called through its own `serialize()`. It reads back the first `{ … }` block after the first `export const` (or, failing that, `export default`), which must be JSON: double-quoted keys and strings, no comments, no trailing commas, no functions. It is **not** an authoring shape: authored metadata such as a `*.object.ts` is written `ObjectSchema.create({ … })` (or `defineView()`, …), which this serializer never emits, and an authored file with unquoted keys is refused rather than read.
77+
- **TypeScriptSerializer** — the `typescript` / `javascript` formats (`.ts` / `.js`): a JSON document wrapped in a module, the file format `FilesystemLoader` writes and reads for those two formats (`typescript` is `FilesystemLoader.save()`'s default, so `MetadataManager.save()` routed to the filesystem loader writes `{rootDir}/{type}/{name}.ts`). It writes `export const metadata = { …JSON… };` then `export default metadata;`. A `typescript`-format file that `FilesystemLoader.save()` writes with the built-in serializer the package wires in also annotates that constant with the spec type of the item's metadata type, and imports it: `ServiceObject` from `@objectstack/spec/data` for an `object`, `Flow` from `@objectstack/spec/automation` for a `flow`, and so on. Each is exactly the `z.input` type of the schema `getMetadataTypeSchema()` resolves for that metadata type. A metadata type with no such spec type is written with no annotation and no import: `view` (`ViewMetadataSchema` is a `z.preprocess`, whose `z.input` type is `unknown`; `ViewMetadata` is declared instead as the union of the input types of the members that schema's union runs, so it is not that schema's `z.input` type), `book`, `external_catalog`, or a plugin's own type. Only the loader knows the metadata type, so it picks the annotation through a package-internal function, and `TypeScriptSerializer.serialize()` called directly writes no annotation. A serializer wired in by hand (a custom one, a subclass that overrides `serialize()`, or a `TypeScriptSerializer` from the package's other entry point) is called through its own `serialize()`. It reads back the first `{ … }` block after the first `export const` (or, failing that, `export default`), which must be JSON: double-quoted keys and strings, no comments, no trailing commas, no functions. It is **not** an authoring shape: authored metadata such as a `*.object.ts` is written `ObjectSchema.create({ … })` (or `defineView()`, …), which this serializer never emits, and an authored file with unquoted keys is refused rather than read.
7878

7979
### 4. Overlay / Customization System
8080

‎packages/metadata/src/serializers/typescript-serializer-annotation.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
* - a spec-valid body must type-check with no diagnostic at all;
1313
* - the same body plus one undeclared key must fail with exactly TS2353. That
1414
* is what makes the annotation a check: a spec type that is `unknown` (as
15-
* `ViewMetadata` is) would pass the first file and the second.
15+
* `ViewMetadata` once was) would pass the first file and the second.
1616
*
1717
* `@objectstack/spec` is resolved the way a consumer of a saved file resolves
1818
* it, through this package's `node_modules` and the `exports` map's `types`,

‎packages/metadata/src/serializers/typescript-serializer.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,13 @@ import { sortObjectKeys } from './sort-object-keys.js';
2727
* `unknown` or another type's shape.
2828
*
2929
* Two metadata types are deliberately absent:
30-
* - `view`: `ViewMetadataSchema` is a `z.preprocess`, so its input type, and
31-
* with it `ViewMetadata`, is `unknown`. Annotating with it would check
32-
* nothing.
30+
* - `view`: `ViewMetadataSchema` is a `z.preprocess`, so its input type is
31+
* `unknown`. `ViewMetadata` is declared instead as the union of the input
32+
* types of the members the schema's union runs, so it is not the bound
33+
* schema's `z.input` and this table's rule excludes it. The rule holds for a
34+
* reason here: the view door accepts bodies `ViewMetadata` refuses (the
35+
* preprocess removes the console's row `id`s before any member judges the
36+
* body), so annotating a saved view with it could be false.
3337
* - `book`: `Book` is written by hand and lacks the `_packageId` /
3438
* `_provenance` protection keys `BookSchema` accepts, so a book the loader
3539
* has stamped would fail against it.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#19871] The published type `ViewMetadata` names a `view` body; it is not `unknown`.
5+
*
6+
* `ViewMetadataSchema` is a `z.preprocess`, whose input type is `unknown`, so the type declared as
7+
* `z.input<typeof ViewMetadataSchema>` WAS `unknown`: any value type-checked against a name and a
8+
* TSDoc that promise a persisted view body. It is now the union of the input types of
9+
* `VIEW_METADATA_MEMBERS`, the members the schema's union runs.
10+
*
11+
* Two halves, judged by two programs:
12+
*
13+
* - The TYPE half is judged by `tsc -p tsconfig.test.json` (the package's `typecheck` script, via
14+
* `check:test-typecheck`), not by vitest. Each `@ts-expect-error` below asserts that its line
15+
* does NOT compile. While `ViewMetadata` was `unknown` every one of them compiled, so each
16+
* directive was unused: TS2578 in a file with no `test-typecheck-debt.json` entry, which reds the
17+
* gate. The member bodies are typed through the published name, so a later narrowing that drops
18+
* a member is a compile error on its line.
19+
* - The RUNTIME half ties those typed bodies to the door: each one parses, through the member it
20+
* is written for, so the fixture really covers every member.
21+
*/
22+
23+
import { describe, it, expect } from 'vitest';
24+
import {
25+
VIEW_METADATA_BRANCHES,
26+
diagnoseViewMetadata,
27+
type ViewMetadata,
28+
type ViewMetadataBranch,
29+
} from './view.zod';
30+
31+
// ── One body per member, each typed through the published name ────────────────────────────────
32+
33+
const viewItem: ViewMetadata = {
34+
name: 'crm_lead.all',
35+
object: 'crm_lead',
36+
viewKind: 'list',
37+
config: { type: 'grid', data: { provider: 'object', object: 'crm_lead' }, columns: ['name'] },
38+
};
39+
const container: ViewMetadata = {
40+
object: 'crm_lead',
41+
list: { type: 'grid', data: { provider: 'object', object: 'crm_lead' }, columns: ['name'] },
42+
};
43+
const listOverlay: ViewMetadata = { type: 'grid', columns: ['name'], object: 'crm_lead', viewKind: 'list' };
44+
const formOverlay: ViewMetadata = {
45+
type: 'simple',
46+
sections: [{ label: 'Main', fields: ['name'] }],
47+
object: 'crm_lead',
48+
viewKind: 'form',
49+
};
50+
51+
const BODY_OF_EACH_MEMBER: Record<ViewMetadataBranch, ViewMetadata> = {
52+
viewItem,
53+
container,
54+
listOverlay,
55+
formOverlay,
56+
};
57+
58+
// ── What `ViewMetadata` refuses at compile time ──────────────────────────────────────────────
59+
60+
const someValue: unknown = JSON.parse('{"nope":1}');
61+
// @ts-expect-error -- `unknown` is not a view body; it was assignable while ViewMetadata was `unknown`.
62+
const fromUnknown: ViewMetadata = someValue;
63+
// @ts-expect-error -- `notAViewKey` is declared by no member (TS2353).
64+
const undeclaredKey: ViewMetadata = { type: 'grid', columns: ['name'], object: 'crm_lead', viewKind: 'list', notAViewKey: 1 };
65+
// @ts-expect-error -- a view body is an object.
66+
const scalar: ViewMetadata = 42;
67+
void [fromUnknown, undeclaredKey, scalar];
68+
69+
describe('[#19871] ViewMetadata is a view body, not unknown', () => {
70+
it('has a typed body for every member of the union', () => {
71+
expect(Object.keys(BODY_OF_EACH_MEMBER).sort()).toEqual([...VIEW_METADATA_BRANCHES].sort());
72+
});
73+
74+
for (const branch of VIEW_METADATA_BRANCHES) {
75+
it(`the ${branch} body typed as ViewMetadata parses, through the ${branch} member`, () => {
76+
const diagnosis = diagnoseViewMetadata(BODY_OF_EACH_MEMBER[branch]);
77+
expect(diagnosis.success).toBe(true);
78+
expect(diagnosis.branch).toBe(branch);
79+
});
80+
}
81+
});

‎packages/spec/src/ui/view.zod.ts‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6352,8 +6352,25 @@ export type ViewParsed = z.infer<typeof ViewSchema>;
63526352
export type ViewItem = z.input<typeof ViewItemSchema>;
63536353
/** A ViewItem record as it travels the WIRE — the authoring shape plus Studio's round-trip keys (#5074). */
63546354
export type ViewItemWire = z.input<typeof ViewItemWireSchema>;
6355-
/** Any persisted `view` metadata body: container | ViewItem record | flattened overlay (#3095). */
6356-
export type ViewMetadata = z.input<typeof ViewMetadataSchema>;
6355+
/**
6356+
* Any persisted `view` metadata body: container | ViewItem record | flattened overlay (#3095) —
6357+
* the union of the INPUT types of the members {@link ViewMetadataSchema}'s union runs, read off
6358+
* {@link VIEW_METADATA_MEMBERS} (the union's member list by construction).
6359+
*
6360+
* [#19871] Deliberately NOT `z.input<typeof ViewMetadataSchema>`. That schema is a `z.preprocess`,
6361+
* whose input type is `unknown`, and its union's members are cast to `z.ZodTypeAny` where the
6362+
* union is built — so every type derived from the schema itself is `unknown`, and this name used to
6363+
* type-check any body at all. `view-metadata-type.test.ts` pins that `unknown` and an undeclared
6364+
* key are refused here, and that a body of each member still type-checks.
6365+
*
6366+
* A static type, not the door's verdict, in both directions: the door accepts bodies this type
6367+
* refuses (the preprocess removes the console's row `id`s, and three members strip undeclared
6368+
* top-level keys), and refuses bodies it admits — the identity precondition, the members'
6369+
* refinements, and a body that mixes keys of different members, because TypeScript checks an
6370+
* object literal's keys against the union as a whole and the container member's keys are all
6371+
* optional. `ViewMetadataSchema` remains the only judge.
6372+
*/
6373+
export type ViewMetadata = z.input<(typeof VIEW_METADATA_MEMBERS)[ViewMetadataBranch]>;
63576374
/** Post-parse shape of {@link ViewMetadata} — defaults applied, transforms run (ADR-0122). */
63586375
export type ViewMetadataParsed = z.infer<typeof ViewMetadataSchema>;
63596376
export type ViewScope = z.input<typeof ViewScopeSchema>;

0 commit comments

Comments
 (0)