Repository navigation
fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown - #19919
Conversation
…pes, not unknown ViewMetadata was z.input of ViewMetadataSchema, a z.preprocess whose input type is unknown, so the published name type-checked any value. It is now read off VIEW_METADATA_MEMBERS, the members the schema's union runs. The runtime schema is unchanged. A type-level pin (view-metadata-type.test.ts) refuses unknown, a key no member declares and a non-object, and types one body per member that the door accepts through that member. The metadata serializer's comment that documented the unknown now states why view stays unannotated. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
A minor, BREAKING-bannered changeset for @objectstack/spec: the published type narrows while the runtime accept set does not move. Its ADR-0087 disposition is no-migration-prescription. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 139 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5763b14fd6729d60510836b8a039d1684bd77a6d && git checkout 5763b14fd6729d60510836b8a039d1684bd77a6d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2c1011b01bc071c545f72f2761647b8d9ab56375 3cb0a8f6a226175eb523a9b4a60d0fb00d25a10c && git checkout -B drift-repro 2c1011b01bc071c545f72f2761647b8d9ab56375 && git merge --no-ff 3cb0a8f6a226175eb523a9b4a60d0fb00d25a10c
node scripts/docs-audit/affected-docs.mjs --json 2c1011b01bc071c545f72f2761647b8d9ab56375 |
Contract reviewServed-tier: 61/61 Isolated at-tier reviewer subagent, run by the ① Derived judgments
② Semver level
③ Boundary flagsBLOCKING
Non-blocking
Implemented-by: VERDICT: FAIL — one blocking item: Generated by Claude Code |
The TypeScriptSerializer paragraph gave "its ViewMetadata type is unknown" as the reason a saved view is written with no annotation. This branch narrows ViewMetadata to the union of the input types of the members ViewMetadataSchema's union runs, so that parenthetical is false, and README.md ships in the @objectstack/metadata tarball. The clause now says what the serializer comment says: the schema's z.input is unknown, and ViewMetadata is declared as that member union instead, so it is not the bound schema's z.input type. Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d Co-authored-by: Claude <noreply@anthropic.com>
…n as superseded
The still-pending @objectstack/metadata changeset for the per-type
annotation says a view file is unannotated because ViewMetadata is
unknown. That file is another change's release note and is not edited
here; one sentence in this changeset says its reason is superseded and
its outcome stands, now because ViewMetadata is no longer the z.input
type of the schema getMetadataTypeSchema('view') binds.
Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d
Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: 67/67 Isolated at-tier reviewer subagent, run by the ① Derived judgments
② Semver level
③ Boundary flagsNon-blocking
Implemented-by: VERDICT: PASS Generated by Claude Code |
… like #19919 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 <noreply@anthropic.com>
…(Parsed) name their shapes, not unknown (objectstack-ai#19920) (objectstack-ai#20260) Part of objectstack-ai#19920 Clause-②: no Three of the four sites objectstack-ai#19920 names now resolve to the shape their TSDoc promises. `JoinedReportBlock` is the remainder: its region of `report.zod.ts` is held by the open PR objectstack-ai#20238 (objectstack-ai#20161), and the dispatch put this site after that PR merges. objectstack-ai#19920 remains open for it. The measured options for it are under "The remainder" below. ## What changed Types only. No schema, no parse, no export and no declared type of any schema moves. Each alias was derived from a schema whose own static type erases to `unknown`, so any value type-checked against it. Each is now derived from the member schema the parse actually runs, the way PR objectstack-ai#19919 re-derived `ViewMetadata`. | name | FROM (all `unknown` at `e0f17a37`) | TO | |:--|:--|:--| | `InlineAction` (`ui/action.zod.ts`) | `z.input` of `typeof InlineActionSchema`. The schema is a `z.preprocess`, whose input type is the preprocess function's `unknown` parameter. | `z.input` of `(typeof InlineActionSchema)['out']`: the pipe's `out` member, the `.pick()`ed action object. | | `ViewMetadataParsed` (`ui/view.zod.ts`) | `z.infer` of `typeof ViewMetadataSchema`. The union's members are cast to `z.ZodTypeAny` where it is built. | `z.infer` over `(typeof VIEW_METADATA_MEMBERS)[ViewMetadataBranch]`: the members' OUTPUT union, the same record `ViewMetadata` reads its input types from. | | `AssembledViewArtifact` (`ui/assembled-views.zod.ts`) | `z.input` of `typeof AssembledViewArtifactSchema`. Same cast. | `z.input` over the `VIEW_METADATA_MEMBERS` entries minus `container`: the three members that schema's union is mapped from. | | `AssembledViewArtifactParsed` | `z.infer` of the same schema. Same cast. | `z.infer` over the same three members. | - `diagnoseViewMetadata`: the one edit the new type forces. Its success branch returned `data: parsed.data`, and `parsed.data` is `unknown` for the same member cast. Without an edit that line is TS2322 (measured below). It now asserts `parsed.data` to `ViewMetadataParsed`, with a comment saying why that holds. At runtime the union's output IS the accepting member's output, since the union's `.check()` transforms nothing. No value changes. A new test asserts `diagnosis.data` deep-equals the member's own parse output for every member. - Every changed TSDoc states what the type does NOT express. The schema is still the only judge: the preprocess folds and strips, and refinements are not types. For `InlineAction`, the legacy `type: 'navigation'` and `to` spellings are refused by the type while the door still folds them. That is pinned in both directions. - The `JoinedReportBlockSchema` `z.ZodTypeAny` annotation and the member casts inside `ViewMetadataSchema` / `AssembledViewArtifactSchema` are all untouched. - Changeset `.changeset/19920-exported-types-not-unknown.md`: `minor` on `@objectstack/spec`, `Clause-②: no (narrowing)` with the BREAKING banner (a narrowing of published TYPES; the runtime accept set does not move) and the ADR-0087 `not-required (no-migration-prescription)` disposition, matching the objectstack-ai#19919 precedent; FROM and TO per name, plus the "if your code stops compiling" instruction. - `.changeset/view-metadata-type-not-unknown.md` is the unreleased objectstack-ai#19919 entry. It said "`ViewMetadataParsed` is not changed by this release: it is still `unknown`", which this branch makes false if both entries ship in one release. It now reads "`ViewMetadataParsed` is not changed by this change. It is re-derived from the same members, as their output types, by its own entry (objectstack-ai#19920)." That holds whichever release carries either entry. My own entry's `JoinedReportBlock` sentence is worded the same way. ## Confirmation needed: a pending release note is corrected on purpose (`Check Changeset` stays red) `check-empty-changeset` refuses this PR because it changes `.changeset/view-metadata-type-not-unknown.md`, a changeset it did not add. This is the gate's DELIBERATE CORRECTION class, not a filename collision: - **Note:** the pending (unreleased) objectstack-ai#19919 entry for `ViewMetadata`. - **What changed under it:** this PR re-derives `ViewMetadataParsed`, so the note's sentence "`ViewMetadataParsed` is not changed by this release: it is still `unknown`" would ship false in any release that carries both entries. - **The rewrite:** that one sentence now reads "`ViewMetadataParsed` is not changed by this change. It is re-derived from the same members, as their output types, by its own entry (objectstack-ai#19920)." That holds whichever release carries either entry. Nothing else in the note moves. Per the gate's own prescription, the file is ⛔ not restored from the base, which would put the false sentence back. `Check Changeset` stays red until the correction is confirmed here in writing. It is not a required context. If a release consumes the objectstack-ai#19919 entry before this PR lands, the correction becomes moot: the merge resolves by keeping `main`'s deletion. ## Measurements All readings are TypeScript compiler-API reads of the package's own `tsconfig.json` / `tsconfig.test.json`, unless named otherwise. 1. **The premise holds.** At base `e0f17a37`: `ViewMetadataParsed`, `InlineAction`, `AssembledViewArtifact`, `AssembledViewArtifactParsed` and `JoinedReportBlock` all have type flag `Unknown`. The control `ViewMetadata` (re-derived in PR objectstack-ai#19919) is NOT unknown, and `InlineActionParsed` was never unknown. At head, the four changed names are not unknown, and `JoinedReportBlock` still is. 2. **Declaration cost.** No cast is removed; each alias is emitted verbatim. `pnpm --filter @objectstack/spec build` was run on both trees in one lock turn: - total `.d.ts` bytes: 30,830,702 at `e0f17a37` → 30,836,475 at `61b382d9` (+5,773, +0.019%, TSDoc and alias text); - affected chunks: `view.zod` 499,735 → 500,541, `action.zod` 76,621 → 77,606, `page.zod` 304,750 → 305,845; - `TS7056` occurrences in the build log: 0 on both. The casts are real declaration-size dodges, which is why this PR derives from the members and leaves the casts alone. In-memory declaration emit, replacing each union's `z.ZodTypeAny` tuple with the real member tuple: - `view.zod.d.ts`: 500,881 → 663,301 bytes (+32%); `ViewMetadataSchema`'s own declaration grows 323 → 162,743 bytes (3,995 lines); - `assembled-views.zod.d.ts`: 8,824 → 64,776 bytes (×7.3); the schema's declaration grows 306 → 56,258 bytes. Neither emits TS7056. The same root fix would also have removed the `diagnoseViewMetadata` assertion, so that assertion is the cheaper of the two ways to type `data`. 3. **What the type change forces.** Census, whole tree, `git grep -w` excluding `.md`/`.mdx`: outside `packages/spec`, nothing names the four types or `diagnoseViewMetadata`. Inside, only `view.zod.ts` itself and six test files do. An in-memory ablation removing the assertion yields exactly one diagnostic: `view.zod.ts` TS2322 "Type 'unknown' is not assignable to type 'ViewMetadataParsed'". The six census test files carry 8 diagnostics under `tsconfig.test.json`, identical on base and head apart from line numbers, all in the ledgered `view.test.ts` debt. 4. **Consumer compile.** - objectui at the pinned `f8a9d0fb` names none of the three changed types. It names `JoinedReportBlock`, which this PR leaves alone. - cloud (local checkout `48d7066`) names none of the four. The control leg (`defineStack`) hits 18 files. - No consumer package in this repo imports them, so there is no consumer suite to run beyond `@objectstack/spec`'s own. ## Reverse verification In-memory ablation: each alias reverted to its base spelling through a compiler-host override, with the anchor matched exactly once and nothing written to disk. The pin file is then compiled under `tsconfig.test.json`, and every `@ts-expect-error` pin turns red: | alias reverted | pin file | result | |:--|:--|:--| | `InlineAction` | `inline-action-type.test.ts` | 4 × TS2578 (unused directive) | | `ViewMetadataParsed` | `view-metadata-type.test.ts` | 3 × TS2578 | | `AssembledViewArtifact` | `assembled-view-artifact-type.test.ts` | 3 × TS2578 | | `AssembledViewArtifactParsed` | `assembled-view-artifact-type.test.ts` | 1 × TS2578 | With the fix in place the three pin files compile with 0 diagnostics. `tsc -p tsconfig.test.json --listFilesOnly` lists all three, among 523 test files. ## Tests Final head `f8792c93`. Its code is identical to `61b382d9`; `cf123662` and `f8792c93` touch only `.changeset/`. Round 2 (`f8792c93`, the changeset level / arm / banner / marker only) re-ran the 17 derived families that read `.changeset` plus `check:spec-changes`: `check-adr-0087-registration` exit 0 (`[BREAKING+clause-②-narrowing] not-required (no-migration-prescription)`), `check-changeset-no-major` exit 0 (including the level axis driven with this PR's payload), and `check-empty-changeset` exit 1 on the deliberate correction only. The other 66 stand at their `cf123662` reading. Heavy runs went through `scripts/pm/os-verify-lock.sh`, and each exit code was written to disk before it was read. - **Build, typecheck and tests** at `61b382d9` (lock turn 1): - `pnpm --filter @objectstack/spec build`: exit 0, TS7056 ×0; - `pnpm --filter @objectstack/spec typecheck`: exit 0, "check:test-typecheck: OK — @objectstack/spec's test layer compiles under packages/spec/tsconfig.test.json; 53 file(s) / 255 error(s) / 142 pinned signature(s) held in test-typecheck-debt.json"; - `pnpm --filter @objectstack/spec test`: "Test Files 550 passed (550)", "Tests 16120 passed | 2 todo (16122)". - **Generated artifacts and pins** at `cf123662` (lock turn 2): - `pnpm --filter @objectstack/spec check:generated`: exit 0, "All 15 generated artifacts are up to date"; - the three pin files by name, `vitest run --project local --maxWorkers=2`: "Test Files 3 passed (3)", "Tests 17 passed (17)". - **Derived gate union** at `cf123662`: `node scripts/pm/dispatch-gates.mjs --commands`, 83 commands, all run. - 80 exit 0. Among them: - `check:api-surface`: "public API surface + factory signatures unchanged"; - `check:exported-any`: "no exported type resolves to `any`: 2376 types + 1452 schemas"; - `check:export-origins`: "5213 exports across 18 entry points resolve exactly as recorded"; - `check:docs`: "226 generated files in sync"; - `check:dual-source-exports`, `check:entry-nameability`, `check:liveness`, `check:spec-parsed-alias`, `check:test-source-alias`, `check:issue-citations`, `check:nul-bytes`; - `check:lean-entry-closure` and `check:doc-formula-expressions`, measured after building their closures. - `check-empty-changeset --base origin/main`: exit 1, the deliberate correction above, red on purpose. - `check:dual-build-cjs-loads` and `check:type-check-debt`: exit 3, PREREQUISITE NOT MET. NOT MEASURED: both need the whole `./packages/*` build closure, which CI builds. - `dispatch-gates.mjs --ran` over the exit-coded record: "83 derived, 81 run, 2 NOT-MEASURED, 0 UNRUN". - **Lint, narrowed and proven.** Repo-wide `pnpm lint` is CI's. I ran `eslint --no-inline-config --format json` over the six changed `.ts` files: - the JSON counts 6 files, 0 errors, 0 warnings; - the population is `eslint.config.mjs`'s own TS/JS globs, so the two `.changeset/*.md` files are outside it; - that config "never enables type-aware linting (no `parserOptions.project`, no typed `@typescript-eslint` rules) for ANY file" (`eslint.config.mjs:327`), so this diff cannot move the verdict on any untouched file. - **Consumer suites:** none owed. No package outside `@objectstack/spec` imports the four types or `diagnoseViewMetadata` (census above). - **NOT MEASURED, left to CI:** - the Type Check workspace and consumer lanes; - Test Core shards; - Dogfood; - Build Core; - the two gates above. ## The remainder: `JoinedReportBlock` - **Why it is not here.** PR objectstack-ai#20238 (objectstack-ai#20161, +77/−14 in `report.zod.ts`) edits the joined-report block schema this type is derived from, and it is still open (draft). The dispatch fixed the order: this site lands on the merged `main`. - **Measured fork, for whoever takes it.** `JoinedReportBlockSchema` is annotated `z.ZodTypeAny`. I did an in-memory declaration emit of `report.zod.ts` on current `main`, with the annotation removed. It produced no TS7056 and no other diagnostic. `report.zod.d.ts` grows 18,608 → 34,390 bytes (×1.85): the block schema's declaration grows to 7,582 bytes, and `ReportSchema` doubles (8,677 → 16,937) because `blocks:` now inlines the block type. So the annotation buys declaration size, not an escape from TS7056. The two options are: - remove the annotation and derive the type from the schema; - keep the annotation and derive the type from an explicitly named shape. Both need re-measuring on the post-objectstack-ai#20238 tree. - **objectui tripwire.** objectui at the pin holds an inverted pin, `true satisfies IsUnknown` of the spec's `JoinedReportBlock`, in `packages/types/src/__tests__/report-chart-query-spec-parity.test.ts`. Its docblock says the day the spec types this, the pin stops compiling, and "the failure is the instruction: re-run the triage and burn it down". The Console Pin Gate only builds objectui (the `types` build config excludes `__tests__/`), so that pin reds objectui's own `type-check` on its next spec bump, not this repo's CI. Measured for the follow-up; this PR does not move it. ## Acceptance notes - `check:spec-parsed-alias` recognises a bare alias only in the spelling `z.input` of `typeof` the schema. Its population drops 1443 → 1441 bare aliases (paired 657 → 655), because `InlineAction` and `AssembledViewArtifact` now use member derivations, as `ViewMetadata` has since objectstack-ai#19919. Both `…Parsed` siblings still exist; the gate just no longer sees the pairs. Noted, not filed. Carrier: none. - Same family, nested: `viewItemArmShape(viewKind, config: z.ZodTypeAny)` makes `config` `unknown` on both members of `ViewItem` and `ViewItemWire` (measured). So it is `unknown` on the `viewItem` member of every union above too. Reported to the dispatching seat to fold into this family's closing card; not changed here. - Zone 3's "a value missing a required key is a type error" pin cannot be expressed for `InlineAction`: every key of its input is optional (`type` has a default; `name` and `label` are `.partial()`). Its pins are `unknown`, the two legacy spellings, and a scalar. --- _Generated by [Claude Code](https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…tstack-ai#20448) Fixes objectstack-ai#19920 Clause-②: no (narrowing) This PR takes items 1 and 3 of the remainder that seat 4's release on objectstack-ai#19920 (comment 5865019059) names: `ApiError.code` and the flattened list overlay's legacy `options` bag. Item 2, `ViewFilterRule.operator`, is not changed: its input type is a contract choice, so it is analysed as a fork (below) for the seat to take to triage. ## What changed Only types change. No schema's parse, no value, and no export moves; no export is added. The FROM column was read by a compiler-API census at the base `0283cb924` and by probes against the source; the TO column is also probed against the built `dist`. | item | FROM | TO | |:--|:--|:--| | 1. `ApiError.code` (`api/error-code-ledger.zod.ts`, `api/contract.zod.ts`) | `unknown`. `ErrorCode` was cast to `z.ZodType` naming only its OUTPUT type parameter, and `z.ZodType`'s INPUT parameter defaults to `unknown`, so the input type of `ApiErrorSchema` typed `code` as `unknown`: `{ code: 42, message: 'x' }` compiled as an `ApiError` while the schema refuses it at `code`. The same `unknown` reached the `error.code` of every response type built on `BaseResponseSchema` (58 input aliases, measured) and each `ApiError` row of a batch result. `makeApiErrorSchema` repeated the one-parameter cast for a caller-supplied vocabulary. | `ErrorCode`: the cast names both parameters, each spelled with the existing `ErrorCode` type alias. `makeApiErrorSchema`: both parameters named, the standard catalogue plus the caller's codes. The `…Parsed` types do not move: their `code` was already typed. | | 3. The list overlay's `options` bag (`ui/view.zod.ts`) | A string-keyed record of `unknown`, on the list overlay member and so on `ViewMetadata`, `ViewMetadataParsed`, `AssembledViewArtifact` and `AssembledViewArtifactParsed`, because `listViewKindBlocks()` returned a record of string to `z.ZodTypeAny`. `options: { foo: 1, kanban: 42 }` type-checked as all four while the member refuses both keys. | One optional entry per list kind that names a block (`calendar`, `chart`, `gallery`, `gantt`, `kanban`, `map`, `timeline`, `tree`), each the kind's own block with every key optional. The return type is a mapped type derived by the function's own rule (a value of the list shape's `type` enum that is also a key of the shape), each entry typed by zod's own `.partial()` answer through a typed helper, never a hand-written copy. The runtime loop is byte-identical; one assertion on its result states what the two derivations share, and the new pin file holds the runtime key set equal to the type's. | ### A change beyond the order's route, forced by a measurement The dispatch suggested dropping `makeApiErrorSchema`'s cast. It stays: its vocabulary is caller-supplied and spread into a `string[]`, so the cast is what carries the caller's codes into the type; dropping it would also change the returned schema class (to `ZodEnum`), a public-type change beyond this item. The defect was the missing input parameter, and that is what changed. The `ErrorCode` cast exists because the spread erases the members to `string`, not to dodge declaration size (mechanism assumption A1). But naming the input parameter DID hit declaration size, measured, and that fixed the spelling: - **First spelling, inline union in both parameters** (`74a132da1`): the built declarations grew 30,647,033 to 34,006,427 B (+3.36 MB, +11.0%); `api/index.d.ts` alone +1,262,450 B. Declaration emit prints an inline union literal by literal wherever a schema embeds `ApiErrorSchema` (78 sites in the `api` entry), and the input parameter doubled those prints. - **Landed spelling, the `ErrorCode` alias** (`539295c9e`): the emitter prints the alias by name, including for the output half the base already printed inline. The declarations SHRINK instead (table below). The bundler emits one new shared chunk, `error-code-ledger.zod` (31,949 B `.d.ts`, 31,950 B `.d.mts`), for the name to be imported from. ## Measurements (spec build, base `0283cb924` against head code `539295c9e`) - **TS7056**: 0 in every spec build of this round (base, `74a132da1`, `539295c9e`). - **Declaration files**: 128 at base, 130 at head; the build's own `check-dts-references` resolves 394/394 relative references across the 130 (382/382 across 128 at base). | declaration file | base | head | delta | |:--|--:|--:|--:| | `api/index.d.ts` (`.d.mts` the same, within 2 B) | 2,532,113 | 1,274,615 | -1,257,498 | | `automation-api.zod` chunk `.d.ts` (and `.d.mts`) | 535,440 | 193,663 | -341,777 | | `api-assembled/index.d.ts` (and `.d.mts`) | 184,582 | 86,529 | -98,053 | | `contracts/index.d.ts` (and `.d.mts`) | 505,045 | 505,100 | +55 | | `view.zod` chunk `.d.ts` (and `.d.mts`) — item 3 | 498,393 | 505,886 | +7,493 | | `error-code-ledger.zod` chunk, new (`.d.ts`) | 0 | 31,949 | +31,949 | | all `.d.ts` / `.d.mts` files | 30,647,033 | 27,331,377 | -3,315,656 (-10.82%) | - **Item 3 against the order's size rule** (A2: implement only if within the same order as PR objectstack-ai#20369's remainder 5, +2,729 B per chunk, 0 TS7056): item 3 moves only the `view.zod` chunk, +7,493 B per chunk (2.7 times that figure, the same order of magnitude, +1.5% of the chunk), 0 TS7056, no `any` anywhere (`check:exported-any` green). Taken on that reading; the ratio is stated so the seat can hold the rule to a tighter reading if it meant one. ## Reverse verification (from committed state `539295c9e`, on disk, through `scripts/ablation-replace.mjs`) Each pin file compiled under `tsconfig.test.json`'s options. The pins import `./contract.zod` / `./view.zod` relatively, so the subject is `src` and no `dist` is on the resolution path. | leg | reverted to (the base spelling) | pin file | result | |:--|:--|:--|:--| | control | nothing | all three pin files | 0 diagnostics | | A | `ErrorCode` cast naming the output parameter only | `api/api-error-code-type.test.ts` | 2 x TS2322, 3 x TS2578 | | B | `makeApiErrorSchema`'s cast naming the output parameter only | `api/api-error-code-type.test.ts` | 1 x TS2322, 2 x TS2578 | | C | `listViewKindBlocks()` returning a record of string to `z.ZodTypeAny` | `ui/view-overlay-options-type.test.ts` | 2 x TS2322, 9 x TS2578 | Every leg: the tool reports the anchor hit once and the mutation landed (blob changed), then the restore proven (blob equals the HEAD blob, `git diff HEAD` empty); an independent `git hash-object` check of all three files after the legs matches HEAD, and `git status --porcelain` is empty. ## Tests and gates Code is identical at `539295c9e` and `4358d1a33` (`4358d1a33` adds the changeset only). - **Spec**: build exit 0 (TS7056 x0); `typecheck` exit 0, `check:test-typecheck: OK — ... 53 file(s) / 251 error(s) / 138 pinned signature(s) held`, and `--listFilesOnly` puts both new pin files in its 540-test-file program; `vitest run --project local` at `4358d1a33`: 565 files passed, 16,604 tests passed, 1 todo; `check:generated`: all 15 generated artifacts up to date, with no tracked file moved by any build. - **Consumers** (after building spec and the 12-package closure of `metadata-protocol`): `@objectstack/metadata-protocol` typecheck exit 0 (192 test files in its program) and tests 189 files passed, 3 skipped, 2,745 tests passed, 19 skipped; `@objectstack/types` typecheck exit 0 (23 of 23 test files in its program) and tests 22 files, 685 passed; `@objectstack/client` `tsc --noEmit` over `src` exit 0. - **Probe against the built `dist`**, from a consumer program importing `dist/api` and `dist/ui`: 0 diagnostics, where every `@ts-expect-error` (a numeric `code` on `ApiError`, an invented one on `BaseResponse`, a numeric `options.kanban` on `ViewMetadata`, an unknown kind on `AssembledViewArtifact`) is consumed and a tuple compiles only if `ApiError.code` is neither `unknown` nor `any`. - **Gates**: `dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `4358d1a33` derived 86 commands (the dispatch's 75 plus 11); all 86 run, exit codes written to disk first: 83 exit 0 (`check:lean-entry-closure` after building `objectql`), 2 exit 3 PREREQUISITE NOT MET (`check:dual-build-cjs-loads`, `check:type-check-debt`: both need the whole-packages build). `--ran`: 86 derived, 84 run, 2 NOT-MEASURED, 0 UNRUN. Readings of note: `check-adr-0087-registration` reads `[BREAKING+clause-②-narrowing] not-required (no-migration-prescription)`; `check:api-surface` "public API surface + factory signatures unchanged"; `check:exported-any` "no exported type resolves to any: 2384 types + 1446 schemas across 18 entry points"; `check-empty-changeset` exit 0. - **Lint, narrowed and proven**: `eslint --no-inline-config --format json` over the 5 changed `.ts` files: 0 errors, 0 warnings; the changeset is outside eslint's configuration. `eslint.config.mjs`:327 enables no type-aware linting for any file, so this diff cannot move an untouched file's verdict. Repo-wide lint is CI's. - **NOT MEASURED, left to CI**: spec `test:repo` (it held the verify lock for the whole foreground window, about 595 s, without finishing, twice); the `@objectstack/client` test-layer typecheck (its 12 dev dependencies include `runtime` and `rest`, a 33-package build); the two exit-3 gates above; the objectui and cloud builds. ## Consumer census - **Item 1 in this repo.** 116 exported spec aliases carry `ApiErrorSchema`'s shape (58 input names; their `…Parsed` twins were already typed), found by walking each alias's properties, arrays and union members. Outside spec, code names them in `@objectstack/client` (return annotations, `as unknown as` casts and `['data']` reads), `@objectstack/metadata-protocol` (`toRowApiError`'s cast from `any` after a `safeParse` guard, and `as BatchUpdateResponse` casts) and `@objectstack/types` (`Pick` of `ApiError`'s optional fields, not `code`). All three typechecks are green above; neither named consumer file needed an edit. - **Item 3 in this repo.** Outside spec, `ViewMetadataSchema` and `AssembledViewArtifactSchema` are called with `safeParse` in `objectql`, `rest` and `metadata-protocol` tests and `objectql`'s `engine.ts`; both schemas' own static types are the erased unions, so no typed `options` read exists outside spec. - **objectui at the pin `f8a9d0fb`.** `ApiError` appears only as `Pick` of `userMessage` (two files); none of the four view types is named. Neither narrowing reaches it (from reading, not compiling). - **cloud**: no checkout in this container, NOT MEASURED. ## Item 2, `ViewFilterRule.operator`: not changed, a fork for triage `operator` is `z.preprocess(normalizeFilterOperator, z.enum(VIEW_FILTER_OPERATORS))`. zod types a preprocess's input as its function's parameter type, and `normalizeFilterOperator` takes `unknown`, so `{ field: 'status', operator: 42 }` compiles as a `ViewFilterRule` (and as a rule on every carrier: `ListView.filter`, tab filters, `Page.filterBy`) while the door refuses it. Who writes the legacy spellings the fold accepts, measured: - `examples/`: 0 legacy spellings on a view-filter carrier, 19 canonical ones in 8 files. (The one legacy-looking hit, `operator: 'ne'` in `app-showcase`'s `invoice.object.ts`, is a field's `lookupFilters`, a separate closed dialect.) - In-repo non-test code: 0 (every other hit is another dialect: lookup filters, auth `where`, skill trigger conditions, analytics). - objectui at the pin `f8a9d0fb`: the filter builder emits camelCase ids (13 of its 22 option values are alias-table keys: `notEquals`, `greaterThan`, `notIn`, `isNull`, …). Its two producers typed against spec's `ViewFilterRule` (`viewFilterFold.ts`, `ObjectDataPage.tsx`) fold through `normalizeFilterOperator` before typing, so the canonical id is what reaches the type. objectui at `9f0c84a44` (its current head) emits the 20 canonical ids only. - Stored `sys_metadata` rows: the alias table exists for them; they are read through the runtime parse, whose input is `unknown` whatever the type says. The three options, the four axes and the recommendation are in the `os-dev-report` on objectstack-ai#19920 (`open_questions`). In short: A, canonical enum only (type the preprocess function's parameter; the runtime fold is untouched); B, the enum plus the alias-table spellings (needs the table's keys typed as literals, and still cannot express the case-folded variants the fold also accepts); C, leave `unknown` with a declared reason. The recommendation is A. ## What stays on objectstack-ai#19920 A compiler-API census of the 2,337 non-generic exported aliases of `packages/spec/src` (tests excluded, 11 generic skipped), with an injected control module that must read lit (it did, at base and head): - **Alias level**: 6 aliases resolve to `unknown` at base and at head, and none belongs to this family: `FlowValueSlot`, `AssignmentValue` and their `Parsed` (value slots), `GetPublishedMetaItemResponse` and its `Parsed` (opaque by ruling). - **Top-level keys**: 194 at base, 193 at head; the one that left is `ApiError.code`, and none entered. The only family site left is `ViewFilterRule.operator` (item 2). Every other key the census reads is declared `z.unknown()` / `z.any()` (the door accepts anything, so the type is honest), a third-party or zod type, a service map or a fixture; `GetMetaItemLayeredResponse.code` and `ViewMetadata.defaults` were checked by hand and are both declared `z.unknown()`. - **Index signatures one level below a top-level key**: 391 at base, 387 at head; the four that left are `options` on `ViewMetadata`, `ViewMetadataParsed`, `AssembledViewArtifact` and `AssembledViewArtifactParsed`, and none entered. Blind spot, declared: deeper nesting is not walked. ## Clause-② Line 2 and the changeset (`b26d6506b`) both read `Clause-②: no (narrowing)`. The changeset keeps its BREAKING banner and the ADR-0087 marker `not-required (no-migration-prescription)`, so `check-adr-0087-registration` still reads the narrowing. The value is `no` because this diff adds no export and moves no accept set; it only narrows published types. That is the PR objectstack-ai#19919 / PR objectstack-ai#20260 shape for this defect class. ## Acceptance notes - `makeApiErrorSchema`'s generic return type still prints the standard catalogue inline: 4 prints in its one declaration (a 4,311 B line in the emitted `contract.zod` declaration), where the base printed 2. A local generic alias would name it; not done here, being one bounded declaration. - A field's `lookupFilters` is its own closed operator dialect (`eq`, `ne`, `gt`, `lt`, `gte`, `lte`, `contains`, `in`, `notIn`), whose members are spellings `ViewFilterRule` treats as deprecated aliases. Both are enforced; noted for item 2's triage, not filed. - The two new pin files follow the two-program shape of `view-overlay-viewkind-type.test.ts`: tsc judges the type half, vitest the runtime half; the refusal cases assert the issue `code` and `path`, not a bare failure. Line 1 was changed from the partial-landing marker to this closing keyword by the `domain:spec` seat 1 (`session_01B3TqpoQbTAfG7G74GMDWNW`): item 2 (`ViewFilterRule.operator`'s input type) now has its own card, objectstack-ai#20450, for triage, which is the dispatch order's A4 condition for closing objectstack-ai#19920 with this PR. Line 2 and the `## Clause-②` section were amended by the same seat after the at-tier record 5871015216 found the value `yes` wrong for this diff; the changeset line moved with them in `b26d6506b`, and the claim on objectstack-ai#19920 was amended in place. The stale `Part of` paragraph was removed. --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #19871
Clause-②: no (narrowing)
What changed
packages/spec/src/ui/view.zod.ts, the type export region only:ViewMetadatawasz.inputoftypeof ViewMetadataSchema, which is exactlyunknown. It is nowz.inputover(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.ViewMetadataSchema, its members and itsz.preprocessare untouched, and a 44-body parse probe answers byte-identically before and after (below).packages/spec/src/ui/view-metadata-type.test.ts.packages/metadata/src/serializers/typescript-serializer.ts: the comment that documented theunknownnow says whyviewstays unannotated. The reason is the table's own rule (the spec type must equal the bound schema'sz.input), and it holds for a real reason: the door accepts bodiesViewMetadatarefuses. One word in the sibling test's docblock ("is" to "once was") keeps it true..changeset/view-metadata-type-not-unknown.md:@objectstack/specminor, BREAKING banner,Clause-②: no (narrowing), ADR-0087 dispositionnot-required (no-migration-prescription).Why this type: the reading
ViewMetadataParsedis the output shape. So the fix is the members' INPUT union.z.preprocessinput type isunknown, and where the union is built its members are cast toz.ZodTypeAny, so bothz.inputandz.inferofViewMetadataSchemaareunknown.VIEW_METADATA_MEMBERSis the union's member list by construction, so it is read there instead.git grep -w ViewMetadatafinds no annotation consumer in this repo. objectui has zero hits at the pinned sha62597c5880and at itsorigin/main544ecba. The control grep forViewMetadataSchema/VIEW_METADATA_MEMBERSat the pin answers exit 0 with 9 files, so the negative reading is valid.Measurements: before
8490127962(origin/main), after2f378ec5cfType probe, compiled against
srcAND against the BUILTdist/ui/index.d.mts, with the same results on both:unknown extends ViewMetadataunknown, assigned toViewMetadatanotAViewKey42name,type: 'grid',object,columns), which the parse rejectsViewMetadataParsedisunknownProbe liveness control: at base, a
ListViewliteral with an undeclared key reports TS2353 in both programs.--listFilesshows 11 specdistfiles and 36 specsrcfiles in the two programs.Runtime acceptance is byte-identical. The probe runs 44 bodies through
ViewMetadataSchema.safeParse: theview-union-diagnosticsbattery 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). Onsrcand ondist, before and after, it gives 20 accepted and 24 refused, report sha256f652fb343387bf490eda6fd7efdde6330d6103db811f246c8234ded9ee4993d7all four times.Type against door over the same 44 bodies. Before, all 44 compile (
unknown). After:idonsort/filterrows, which the preprocess strips. The third is the undeclared key, which a strip-mode member drops.{}, name-only and stamped-identity-only bodies;listViews;configasunknown(see Acceptance notes), so a bad config or a decorated config type-checks;TypeScriptSerializer.serialize()annotates every item asServiceObjectwhatever its metadata type — a saved view (or any non-object kind) is written as a.tsfile that failstscwith TS2353 #19852 repro andcontainer.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 oncetypeandcolumnsare found on another member,name+type+object+columnsis assignable to the container member. Measured: the same union without the container member refuses the body (TS2741,viewKindmissing). 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.pinAndOrderandoverlay.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.mjsthen put the declaration back to today's spelling (anchor hit 1 to 0, blobbb6384c31aebtoa74e16786768).pnpm --filter @objectstack/spec check:test-typecheckthen 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 TS2578Unused '@ts-expect-error' directiveat lines 61, 63 and 65. Restored: blob equals HEAD, andgit diff HEADis empty. On the fixed tree the same gate is green: 53 files, 255 errors and 142 signatures, held exactly by the unchangedtest-typecheck-debt.json.Generated artifacts, and what
check:api-surfaceseesgen:api-surface,gen:export-originsandgen:declaration-mapall ran on the rebuilt dist and wrote zero diff ("0 shard(s) rewritten").check:api-surfaceanswers "public API surface + factory signatures unchanged". The snapshot records that an export EXISTS and its kind, not what a type alias resolves to (see thebuild-api-surface.tsheader), so this type change is invisible to it. The change is stated here instead:ViewMetadatanarrows fromunknownto the union of the four members' input types. The dispatch expectedcheck:api-surfaceto report it. Measured, it does not.Clause-② reading
no. The runtime accept set does not move (the probe above), and no export is added or removed.(narrowing). A published TYPE narrows fromunknown: code that assigns a non-view value to aViewMetadataslot stops compiling. ADR-0087's 2026-08-30 addendum reads a published type-surface narrowing as BREAKING, truthfully.minor. This follows the launch-window convention in thecheck-changeset-no-major.mjsheader: breaking-ness is carried by the banner and the disposition, not by the level.not-required (no-migration-prescription).type-surface-onlyis closed to this diff by its predicate 2 (it touchespackages/spec/**) and predicate 3 (a*.zod.tsmoved). Nothing authorable is removed or renamed, and no stored row moves.check-adr-0087-registrationanswers "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition".check-changeset-no-majoranswers "nomajorbump".Clause-②: nois superseded byno (narrowing).Local verification at
2f378ec5cfpnpm --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 typecheckandtest: exit 0, 54 files, 821 passed.pnpm --filter @objectstack/spec check:generated: all 15 artifacts up to date. It ran against a dist built fromaa5156d9de; 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-loadsandcheck: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/mainmoved 2 commits since the base (beac798026,a34c27cbe5). Neither touchespackages/specorpackages/metadata, so they are not merged here.Acceptance notes
Sibling census: every exported type alias in
packages/spec/srcthat resolves tounknown, compiled with the package's own tsconfig at2f378ec5cf. 2359 aliases were scanned. ⛔ None of these is changed here; they are listed for the seat.InlineAction(src/ui/action.zod.ts:2161) isz.inputof alazySchemawrapping az.preprocess, so it isunknown. It has the same shape as this card's type, in another file. (InlineActionParsedresolves to a concrete type.)ViewMetadataParsed(src/ui/view.zod.ts:6329) isunknown: itsz.inferreads the union whose members are cast toz.ZodTypeAny. Narrowing it the same way is not a one-line change.diagnoseViewMetadatareturnsdata: parsed.data, andparsed.dataisunknown, which is not assignable to the members' output union (TS2322, measured). So the fix also needs an edit in that function, outside this region.AssembledViewArtifactandAssembledViewArtifactParsed(src/ui/assembled-views.zod.ts:102/104) areunknown. The cause is a union ofz.ZodTypeAny-cast members (the sameVIEW_METADATA_MEMBERSvalues), not a preprocess. The TSDoc says "One assembledviewItems:entry (input shape)".JoinedReportBlock(src/ui/report.zod.ts:407) isunknownbecauseJoinedReportBlockSchemais annotatedz.ZodTypeAny.AssignmentValue(Parsed)andGetPublishedMetaItemResponse(Parsed)areunknownby design: their schemas arez.unknown().configis typedunknown, becauseviewItemArmShapetakesconfig: z.ZodTypeAny. So at the type levelViewItem,ViewItemWireand this union's viewItem arm accept anyconfig.check:exported-anycatches aZodTypeofany, not an exported alias that resolves tounknown. 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 FAIL5808373817Written by the
domain:specseat 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 saidViewMetadataisunknown, which this PR makes false. It now reads:ViewMetadataSchemais az.preprocess, whosez.inputtype isunknown;ViewMetadatais declared instead as the union of the input types of the members that schema's union runs, so it is not that schema'sz.inputtype. 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-*.mdsentence (ViewMetadataisunknown) as superseded. That file is not edited, because editing it is the deliberate-correction class.@objectstack/metadata. The README does publish (files[]), but the two packages sit in onefixedgroup, so the corrected README ships in the same release, and the false clause never shipped. At-tier re-review5815765984: PASS.Generated by Claude Code