Commit cf55914
fix(spec): type ApiError.code and the list overlay options bag (#20448)
Fixes #19920
Clause-②: no (narrowing)
This PR takes items 1 and 3 of the remainder that seat 4's release on
#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 #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 #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 #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 #19919 / PR #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, #20450,
for triage, which is the dispatch order's A4 condition for closing
#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
#19920 was amended in place. The stale `Part of` paragraph was removed.
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 35e549b commit cf55914
6 files changed
Lines changed: 281 additions & 4 deletions
File tree
- .changeset
- packages/spec/src
- api
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
284 | 284 | | |
285 | 285 | | |
286 | 286 | | |
287 | | - | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
288 | 290 | | |
289 | 291 | | |
290 | 292 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1390 | 1390 | | |
1391 | 1391 | | |
1392 | 1392 | | |
| 1393 | + | |
| 1394 | + | |
| 1395 | + | |
| 1396 | + | |
| 1397 | + | |
| 1398 | + | |
| 1399 | + | |
| 1400 | + | |
| 1401 | + | |
| 1402 | + | |
| 1403 | + | |
| 1404 | + | |
| 1405 | + | |
| 1406 | + | |
| 1407 | + | |
1393 | 1408 | | |
1394 | 1409 | | |
1395 | 1410 | | |
1396 | 1411 | | |
1397 | 1412 | | |
1398 | 1413 | | |
1399 | 1414 | | |
1400 | | - | |
| 1415 | + | |
1401 | 1416 | | |
1402 | 1417 | | |
1403 | 1418 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
0 commit comments