fix(spec): InlineAction, ViewMetadataParsed and AssembledViewArtifact(Parsed) name their shapes, not unknown (#19920) - #20260
Conversation
…(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 <noreply@anthropic.com>
… 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 <noreply@anthropic.com>
📓 Docs Drift Check7 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 136 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 a83d2033447179094cf428aae5b3a556faa115a5 && git checkout a83d2033447179094cf428aae5b3a556faa115a5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a9fb83ef06a079a938c97d1ea5e364705e1bc13b f8792c931bf34e113314e158f2edde1b3cfcbecd && git checkout -B drift-repro a9fb83ef06a079a938c97d1ea5e364705e1bc13b && git merge --no-ff f8792c931bf34e113314e158f2edde1b3cfcbecd
node scripts/docs-audit/affected-docs.mjs --json a9fb83ef06a079a938c97d1ea5e364705e1bc13b |
… 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>
|
Confirmed: the deliberate correction of a pending release note. What was corrected
⛔ Do not restore the base text, and ⛔ do not apply The red check
|
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…ind carry the shapes their doors accept (objectstack-ai#19920) (objectstack-ai#20369) Part of objectstack-ai#19920 Clause-②: yes (narrowing) This PR takes the four remainder items that seat 1's release on objectstack-ai#19920 (comment 5858219255) lists, plus remainder 5 (the flattened list overlay's `type` and `columns`), on `main` after PR objectstack-ai#20286. objectstack-ai#19920 remains open because this round's census and probes found three more sites of the same family, outside this PR's surface. See "What stays on objectstack-ai#19920" below. ## What changed Only types change. The runtime accept set does not move: no schema's parse, no value, and no removed or renamed export. The FROM column was probed on the base `c5dcb3ba0`, both against the source (compiler API, `tsconfig.test.json` options) and against the built `dist`. | item | FROM | TO | |:--|:--|:--| | 1. `JoinedReportBlock` (`ui/report.zod.ts`) | `unknown`, because `JoinedReportBlockSchema` was annotated `z.ZodTypeAny`. Each `blocks[]` element of `Report` / `ReportParsed` (and so of `defineReport`'s parameter) was `unknown` for the same reason. | The schema's own input type: the annotation is removed. `blocks[]` elements are blocks. | | 2. A ViewItem's `config` (`ui/view.zod.ts`) | `unknown` on both arms of `ViewItem` and `ViewItemWire`, because `viewItemArmShape(viewKind, config)` typed `config` as `z.ZodTypeAny`. The same `unknown` reached `defineViewItem` and the `viewItem` member of `ViewMetadata`, `ViewMetadataParsed`, `AssembledViewArtifact` and `AssembledViewArtifactParsed`. | The arm's config type: a `ListView` config under `viewKind: 'list'`, a `FormView` config under `viewKind: 'form'`. `config` is a generic parameter now, beside `viewKind`. | | 3. The flattened overlay members' `viewKind` | The union of `'list'` and `'form'` on both members, because `flattenedViewOverlayFields(kind)` was not generic, so `z.enum([kind])` widened. `{ type: 'grid', columns: ['name'], object, viewKind: 'form' }` type-checked, through the list member, as all four union types above, while both doors refuse it. | `'list'` on the list overlay member, `'form'` on the form overlay member: the function is generic in the kind. | | 4. The list overlay's `type` default | Not named anywhere. | One TSDoc paragraph on `ViewMetadataParsed`, and the same on `AssembledViewArtifactParsed`. It says, from the code, that the member declares `type` without `.default('grid')` and re-applies it in `.overwrite(applyListOverlayTypeDefault)`. An `.overwrite()` returns the member's own output type, so `type` stays optional in the output type while every parse through that member returns it (`'grid'` when the body named none). On that member `type` is typed as the list shape's `type` enum. | | 5. The flattened list overlay's `type` and `columns` (`ui/view.zod.ts`) | `unknown` on the list overlay member of `ViewMetadata`, `ViewMetadataParsed`, `AssembledViewArtifact` and `AssembledViewArtifactParsed`, on input and on output, because `listOverlayPatchFields()` read both keys off `ListViewShapeSchema` through a cast of its shape to a record of `z.ZodTypeAny`. `{ object, viewKind: 'list', columns: 42 }` type-checked as all four while that member refuses it. | The list shape's own types, both optional: `type` the list view type enum, `columns` the field list. The shape is read as typed; the schemas are the same objects, so no parse moves. | ### Changes beyond the five items, each forced, with what forced it 1. **Declaration size (item 2).** With only the generic parameter, `view.zod.d.ts` grows by +170,655 B (500,535 to 671,190, +34%), with 0 TS7056. The cause is that `ViewItemSchema`, `ViewItemWireSchema` and `VIEW_METADATA_MEMBERS.viewItem` each spell both config types out in full, at 58,764 B, 59,326 B and +60,108 B. - **What this PR does instead.** Both schemas carry an explicit type annotation through two non-exported aliases read off `viewItemArmShape` itself. `ViewItemArmShape` is the ReturnType of `typeof viewItemArmShape`, instantiated. `ViewItemWireArmShape` is that plus `viewItemWireFields()`, written as one mapped type. The `viewItem` entry of `VIEW_METADATA_MEMBERS` is spelled `ViewItemWireSchema as typeof ViewItemWireSchema`, an assertion to the schema's own type, so the emitter names the schema instead of copying it. - **Result.** `view.zod.d.ts` is 496,008 B, 4,527 B smaller than on the base. - **Identity proof.** Measured with an `Equal` probe (the mutual-conditional form) against in-memory inferred twins of both schemas. It holds for each schema, for its z.input and z.output, and for the member. A control pair answers false. - **A spelling I measured and did not take.** A first spelling of the wire annotation used an intersection. It was assignable but not identical (Equal false), so it was replaced by the mapped form. - **The deviation.** No TS error forced this assertion. Declaration size forced it. So it deviates from the dispatch's mechanism assumption 2, which allowed assertions only where a TS error forces them. 2. **ADR-0122 (items 1 and 2).** Three isomorphism pins in `type-alias-convention.pin.test.ts` turned into TS2344 ×3: `Iso_ui_report__JoinedReportBlockSchema`, `Iso_ui_view__ViewItemSchema` and `Iso_ui_view__ViewItemWireSchema`. They had held only because input and infer were the same erased `unknown`; typed, each schema carries defaults. The pins are deleted. Per `check:spec-parsed-alias` rule 2, three parsed-state aliases are added: `JoinedReportBlockParsed`, `ViewItemParsed` and `ViewItemWireParsed`. These are three new type exports on the `ui` entry; `api-surface/ui.json` and `export-origins/ui.json` were regenerated. The pin count is restated from 786 to 783 in the file's two prose places and its count history. 3. **`test-typecheck-debt.json`.** The `report.test.ts` entry shrinks by two signatures (TS18046 "'b' is of type 'unknown'" and TS2571), because `blocks[]` is typed now. It was regenerated with `gen:test-typecheck-debt`. 4. **A pending release note corrected on purpose.** See the next section. ## Confirmation needed: a pending release note is corrected on purpose (`Check Changeset` stays red) - **The note.** `.changeset/19920-exported-types-not-unknown.md` is PR objectstack-ai#20260's pending entry for this card. It ended "`JoinedReportBlock` is not changed by this change, and still resolves to `unknown`." - **What this PR changes under it.** Item 1 makes the second half of that sentence false in any release that carries both entries. - **The rewrite.** The sentence now reads "`JoinedReportBlock` is not changed by this change. It stops resolving to `unknown` in its own entry (objectstack-ai#19920)." That holds whichever release carries either entry. Nothing else in the note moves. - **What the gate says.** `node scripts/check-empty-changeset.mjs --base origin/main` exits 1, naming exactly this file, in its DELIBERATE CORRECTION class. Following the gate's prescription, the file is not restored from the base, which would put the false sentence back. - **What happens next.** It stays red until someone confirms the correction here. If a release consumes that entry before this PR lands, the merge keeps `main`'s deletion and the correction is moot. The new entry is `.changeset/19920-exported-types-remainder.md`: `@objectstack/spec` `minor`, `Clause-②: yes (narrowing)` (the diff narrows published types and also adds three exported type names, per the at-tier record 5863387565), a BREAKING banner naming every type that narrows, FROM and TO per item (four bullets, remainder 5 included), a banner and ADR-0087 reason saying that no EXISTING export changes, and the ADR-0087 marker `not-required (no-migration-prescription)`. ## Measurements - **TS7056**: 0 in every spec build of this round: origin/main `5049a3cf`, the pre-remainder-5 head `406335af` and the head `777b5097`. - **Remainder 5's cost** (`406335af` to `777b5097`): the `view.zod` chunk grows 495,664 to 498,393 B (+2,729, the same for `.d.mts`); the `page.zod` chunk moves -3 B; every other declaration file is byte-identical; all 128 files +5,452 B. - **The whole PR** (`5049a3cf` to `777b5097`): | module | base | head | delta | |:--|--:|--:|--:| | `view.zod` chunk (`.d.ts`, and the same for `.d.mts`) | 500,191 | 498,393 | -1,798 | | `page.zod` chunk (report and assembled views) | 306,012 | 311,003 | +4,991 | | `index.d.ts` | 3,277,798 | 3,283,480 | +5,682 | | `system/index.d.ts` | 3,644,976 | 3,647,050 | +2,074 | | `ui/index.d.ts` | 524,900 | 524,978 | +78 | | all 128 `.d.ts` / `.d.mts` files | 30,888,091 | 30,910,145 | +22,054 (+0.071%) | - Item 1's in-memory emit figures (+3,542 B of `report.zod`) are the previous run's and were not re-measured; the chunk table above supersedes them. ## Reverse verification (on disk, from committed state, via `scripts/ablation-replace.mjs`) All five legs re-run at `777b5097`, from committed state, through `scripts/ablation-replace.mjs` in wrap mode; each pin file compiled under `tsconfig.test.json`'s options: | leg | reverted to | pin file | result | |:--|:--|:--|:--| | item 1 | `JoinedReportBlockSchema: z.ZodTypeAny` | `joined-report-block-type.test.ts` | 7 x TS2578, 2 x TS18046 | | item 2 | `config: z.ZodTypeAny` in `viewItemArmShape` | `view-item-config-type.test.ts` | 8 x TS2578, 1 x TS18046 | | item 3 | non-generic `flattenedViewOverlayFields` | `view-overlay-viewkind-type.test.ts` | 11 x TS2578 | | item 4 | the list overlay's `type` keeping the list default | `view-overlay-viewkind-type.test.ts` | 1 x TS2322 | | remainder 5 | the cast of the shape to a record of `z.ZodTypeAny` | `view-overlay-viewkind-type.test.ts` | 6 x TS2578, 4 x TS2322 | Every leg: the tool reports the mutation landed and the restore proven (blob after restore equals the HEAD blob, `git diff HEAD` empty), and a second check re-reads `git hash-object`. Control, no mutation: the three pin files compile with 0 diagnostics; `git status --porcelain` empty. ## Tests and gates, at `777b5097` At `777b5097`: spec build exit 0 (TS7056 x0); spec typecheck exit 0 (check:test-typecheck OK, 53 files / 253 errors / 140 signatures); spec check:generated exit 0; spec vitest local 560 files, 16,493 passed, 1 todo; spec test:repo 34 files, 620 passed; consumer typechecks exit 0 for lint, metadata, metadata-protocol and objectql after building their 12-package closure (rest NOT MEASURED locally, 26-package closure). dispatch-gates derived 86: 83 exit 0, check-empty-changeset exit 1 (the deliberate correction), 2 NOT MEASURED (check:dual-build-cjs-loads, check:type-check-debt, exit 3); `--ran`: 86 derived, 84 run, 2 NOT-MEASURED, 0 UNRUN. Lint narrowed and proven: 7 changed `.ts` files, 0 errors, 0 warnings; the 5 other changed files are outside eslint's configuration; no type-aware linting (`eslint.config.mjs`:327). ## Consumer census - **This repo.** Outside `packages/spec`, the code that imports any narrowed name is: - `defineReport` in the two example report files above; - `Report` in `qa/downstream-contract/src/pipeline.report.ts`; - `ReportSchema` in one platform-objects test and one downstream-contract test; - `ViewMetadataSchema` in two metadata-protocol tests; - `AssembledViewArtifactSchema` in objectql's `engine.ts` and one objectql test (its own static type does not move). - Nothing outside spec names `viewItemArmShape` or `flattenedViewOverlayFields`, which are not exported. All consumer typechecks above are green. - **objectui at the pin `f8a9d0fb`: one break, by design, for the seat to carry.** `packages/types/src/__tests__/report-chart-query-spec-parity.test.ts`:725 is the inverted tripwire `true satisfies IsUnknown` of the spec's `JoinedReportBlock`. Compiled with that file's own two type definitions against this head's dist, it gives TS1360 ("Type 'true' does not satisfy the expected type 'false'"); line 726 (`IsAny`) still holds. - Its docblock says this failure is the instruction to re-run the triage and burn the local interface down, together with the ledger entry at `scripts/check-spec-symbol-derivation.mjs`:636. - It reds objectui's own `type-check` (`tsc -p tsconfig.test.json`) on its next spec bump. It does not red this repo's Console Pin Gate, which builds objectui without `__tests__`. - **objectui's other hits.** Five other objectui files reference `ViewItemSchema` / `ViewMetadataSchema` in code; every other census hit is a comment or objectui's own local `JoinedReportBlock` interface. From reading them, not from compiling them: they read `.success` / `.error` of a `safeParse`, or cast the schema `as unknown as` a local interface. None reads a typed `config` off a parse result. - **Remainder 5 at objectui's pin `f8a9d0fb`.** Six calls in four test files parse a body with `ViewMetadataSchema`, and none reads `.data` off the verdict, so the now-typed list-overlay `type` / `columns` reach no objectui read (from reading, not compiling). ## What stays on objectstack-ai#19920 A compiler-API census of the 2,337 non-generic exported aliases of `packages/spec/src`, excluding tests, with a control that must read lit: 6 aliases resolve to `unknown` before and after this PR, and none belongs to this family (`FlowValueSlot`, `AssignmentValue` and their `Parsed`, value slots; `GetPublishedMetaItemResponse` and its `Parsed`, opaque by ruling). At top-level-key level it counts 210 keys typed `unknown` before remainder 5 and 202 after; the 8 that left are remainder 5. Three sites of the family remain, outside this PR's surface: - `ApiError.code` (input): `ErrorCode` is cast to a one-argument `z.ZodType`, whose input defaults to `unknown`, and `makeApiErrorSchema` repeats the cast. `{ code: 42, message: 'x' }` compiles as `ApiError` while `ApiErrorSchema` refuses it at `code`. - `ViewFilterRule.operator` (input): a `z.preprocess`, whose input is `unknown` (the cause `InlineAction` had). `{ field: 'status', operator: 42 }` compiles while the door refuses it. - The list overlay's `options` bag: `listViewKindBlocks()` returns a record of `z.ZodTypeAny`, so the bag is typed as a string-keyed record of `unknown`. `options: { foo: 1, kanban: 42 }` compiles as `ViewMetadata` while the door refuses it. The census does not scan nested keys; a probe found this one. ## Acceptance notes - **The pin count holds at 783** (`check:spec-parsed-alias`: 1,442 bare `z.input` aliases, 783 pinned isomorphic, 659 paired). - **objectui tripwire, carried by the seat.** `packages/types/src/__tests__/report-chart-query-spec-parity.test.ts` at objectui pin `f8a9d0fb` asserts that `JoinedReportBlock` is `unknown` (line 725). It goes red at the next objectui spec-pin bump, and its docblock says that failure is the instruction to re-run the triage and burn down the local interface. Carrier: the next objectui spec-pin bump. It cannot red this repo: the Console Pin Gate builds objectui without its `__tests__`. - **Fence held.** `FormViewSchema.layout` (objectstack-ai#20221), `ListViewShapeSchema.tabs` and the view container's body `name` (objectstack-ai#20301) are untouched. `origin/main` `5049a3cf` was merged once, at `1161545d`, with no conflict; commits that landed on `main` later touch none of this PR's files. --- _Generated by [Claude Code](https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ)_ --------- 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>
Part of #19920
Clause-②: no
Three of the four sites #19920 names now resolve to the shape their TSDoc promises.
JoinedReportBlockis the remainder: its region ofreport.zod.tsis held by the open PR #20238 (#20161), and the dispatch put this site after that PR merges. #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 #19919 re-derivedViewMetadata.unknownate0f17a37)InlineAction(ui/action.zod.ts)z.inputoftypeof InlineActionSchema. The schema is az.preprocess, whose input type is the preprocess function'sunknownparameter.z.inputof(typeof InlineActionSchema)['out']: the pipe'soutmember, the.pick()ed action object.ViewMetadataParsed(ui/view.zod.ts)z.inferoftypeof ViewMetadataSchema. The union's members are cast toz.ZodTypeAnywhere it is built.z.inferover(typeof VIEW_METADATA_MEMBERS)[ViewMetadataBranch]: the members' OUTPUT union, the same recordViewMetadatareads its input types from.AssembledViewArtifact(ui/assembled-views.zod.ts)z.inputoftypeof AssembledViewArtifactSchema. Same cast.z.inputover theVIEW_METADATA_MEMBERSentries minuscontainer: the three members that schema's union is mapped from.AssembledViewArtifactParsedz.inferof the same schema. Same cast.z.inferover the same three members.diagnoseViewMetadata: the one edit the new type forces. Its success branch returneddata: parsed.data, andparsed.dataisunknownfor the same member cast. Without an edit that line is TS2322 (measured below). It now assertsparsed.datatoViewMetadataParsed, 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 assertsdiagnosis.datadeep-equals the member's own parse output for every member.InlineAction, the legacytype: 'navigation'andtospellings are refused by the type while the door still folds them. That is pinned in both directions.JoinedReportBlockSchemaz.ZodTypeAnyannotation and the member casts insideViewMetadataSchema/AssembledViewArtifactSchemaare all untouched..changeset/19920-exported-types-not-unknown.md:minoron@objectstack/spec,Clause-②: no (narrowing)with the BREAKING banner (a narrowing of published TYPES; the runtime accept set does not move) and the ADR-0087not-required (no-migration-prescription)disposition, matching the fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown #19919 precedent; FROM and TO per name, plus the "if your code stops compiling" instruction..changeset/view-metadata-type-not-unknown.mdis the unreleased fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown #19919 entry. It said "ViewMetadataParsedis not changed by this release: it is stillunknown", which this branch makes false if both entries ship in one release. It now reads "ViewMetadataParsedis not changed by this change. It is re-derived from the same members, as their output types, by its own entry ([finding] four more exported spec types resolve tounknownwhile their TSDoc promises a shape —ViewMetadataParsed,InlineAction,AssembledViewArtifact,JoinedReportBlock(the #19871 class, other sites) #19920)." That holds whichever release carries either entry. My own entry'sJoinedReportBlocksentence is worded the same way.Confirmation needed: a pending release note is corrected on purpose (
Check Changesetstays red)check-empty-changesetrefuses 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:ViewMetadata.ViewMetadataParsed, so the note's sentence "ViewMetadataParsedis not changed by this release: it is stillunknown" would ship false in any release that carries both entries.ViewMetadataParsedis not changed by this change. It is re-derived from the same members, as their output types, by its own entry ([finding] four more exported spec types resolve tounknownwhile their TSDoc promises a shape —ViewMetadataParsed,InlineAction,AssembledViewArtifact,JoinedReportBlock(the #19871 class, other sites) #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 Changesetstays red until the correction is confirmed here in writing. It is not a required context. If a release consumes the #19919 entry before this PR lands, the correction becomes moot: the merge resolves by keepingmain's deletion.Measurements
All readings are TypeScript compiler-API reads of the package's own
tsconfig.json/tsconfig.test.json, unless named otherwise.The premise holds. At base
e0f17a37:ViewMetadataParsed,InlineAction,AssembledViewArtifact,AssembledViewArtifactParsedandJoinedReportBlockall have type flagUnknown. The controlViewMetadata(re-derived in PR fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown #19919) is NOT unknown, andInlineActionParsedwas never unknown. At head, the four changed names are not unknown, andJoinedReportBlockstill is.Declaration cost. No cast is removed; each alias is emitted verbatim.
pnpm --filter @objectstack/spec buildwas run on both trees in one lock turn:.d.tsbytes: 30,830,702 ate0f17a37→ 30,836,475 at61b382d9(+5,773, +0.019%, TSDoc and alias text);view.zod499,735 → 500,541,action.zod76,621 → 77,606,page.zod304,750 → 305,845;TS7056occurrences 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.ZodTypeAnytuple 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
diagnoseViewMetadataassertion, so that assertion is the cheaper of the two ways to typedata.What the type change forces. Census, whole tree,
git grep -wexcluding.md/.mdx: outsidepackages/spec, nothing names the four types ordiagnoseViewMetadata. Inside, onlyview.zod.tsitself and six test files do. An in-memory ablation removing the assertion yields exactly one diagnostic:view.zod.tsTS2322 "Type 'unknown' is not assignable to type 'ViewMetadataParsed'". The six census test files carry 8 diagnostics undertsconfig.test.json, identical on base and head apart from line numbers, all in the ledgeredview.test.tsdebt.Consumer compile.
f8a9d0fbnames none of the three changed types. It namesJoinedReportBlock, which this PR leaves alone.48d7066) names none of the four. The control leg (defineStack) hits 18 files.@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-errorpin turns red:InlineActioninline-action-type.test.tsViewMetadataParsedview-metadata-type.test.tsAssembledViewArtifactassembled-view-artifact-type.test.tsAssembledViewArtifactParsedassembled-view-artifact-type.test.tsWith the fix in place the three pin files compile with 0 diagnostics.
tsc -p tsconfig.test.json --listFilesOnlylists all three, among 523 test files.Tests
Final head
f8792c93. Its code is identical to61b382d9;cf123662andf8792c93touch only.changeset/. Round 2 (f8792c93, the changeset level / arm / banner / marker only) re-ran the 17 derived families that read.changesetpluscheck:spec-changes:check-adr-0087-registrationexit 0 ([BREAKING+clause-②-narrowing] not-required (no-migration-prescription)),check-changeset-no-majorexit 0 (including the level axis driven with this PR's payload), andcheck-empty-changesetexit 1 on the deliberate correction only. The other 66 stand at theircf123662reading. Heavy runs went throughscripts/pm/os-verify-lock.sh, and each exit code was written to disk before it was read.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)".cf123662(lock turn 2):pnpm --filter @objectstack/spec check:generated: exit 0, "All 15 generated artifacts are up to date";vitest run --project local --maxWorkers=2: "Test Files 3 passed (3)", "Tests 17 passed (17)".cf123662:node scripts/pm/dispatch-gates.mjs --commands, 83 commands, all run.check:api-surface: "public API surface + factory signatures unchanged";check:exported-any: "no exported type resolves toany: 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-closureandcheck: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-loadsandcheck:type-check-debt: exit 3, PREREQUISITE NOT MET. NOT MEASURED: both need the whole./packages/*build closure, which CI builds.dispatch-gates.mjs --ranover the exit-coded record: "83 derived, 81 run, 2 NOT-MEASURED, 0 UNRUN".pnpm lintis CI's. I raneslint --no-inline-config --format jsonover the six changed.tsfiles:eslint.config.mjs's own TS/JS globs, so the two.changeset/*.mdfiles are outside it;parserOptions.project, no typed@typescript-eslintrules) for ANY file" (eslint.config.mjs:327), so this diff cannot move the verdict on any untouched file.@objectstack/specimports the four types ordiagnoseViewMetadata(census above).The remainder:
JoinedReportBlockWhy it is not here. PR fix(spec): a joined report draws no chart — retire blocks[].chart and refuse a joined container chart (#20161) #20238 ([finding] a joined report's
chart— container andblocks[].chart— parses and is never drawn, while the liveness ledger names the joined branch as its reader #20161, +77/−14 inreport.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 mergedmain.Measured fork, for whoever takes it.
JoinedReportBlockSchemais annotatedz.ZodTypeAny. I did an in-memory declaration emit ofreport.zod.tson currentmain, with the annotation removed. It produced no TS7056 and no other diagnostic.report.zod.d.tsgrows 18,608 → 34,390 bytes (×1.85): the block schema's declaration grows to 7,582 bytes, andReportSchemadoubles (8,677 → 16,937) becauseblocks:now inlines the block type. So the annotation buys declaration size, not an escape from TS7056. The two options are:Both need re-measuring on the post-fix(spec): a joined report draws no chart — retire blocks[].chart and refuse a joined container chart (#20161) #20238 tree.
objectui tripwire. objectui at the pin holds an inverted pin,
true satisfies IsUnknownof the spec'sJoinedReportBlock, inpackages/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 (thetypesbuild config excludes__tests__/), so that pin reds objectui's owntype-checkon 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-aliasrecognises a bare alias only in the spellingz.inputoftypeofthe schema. Its population drops 1443 → 1441 bare aliases (paired 657 → 655), becauseInlineActionandAssembledViewArtifactnow use member derivations, asViewMetadatahas since fix(spec): ViewMetadata names a view body (the union of its members’ input types), not unknown #19919. Both…Parsedsiblings still exist; the gate just no longer sees the pairs. Noted, not filed. Carrier: none.viewItemArmShape(viewKind, config: z.ZodTypeAny)makesconfigunknownon both members ofViewItemandViewItemWire(measured). So it isunknownon theviewItemmember of every union above too. Reported to the dispatching seat to fold into this family's closing card; not changed here.InlineAction: every key of its input is optional (typehas a default;nameandlabelare.partial()). Its pins areunknown, the two legacy spellings, and a scalar.Generated by Claude Code