fix(spec): type ApiError.code and the list overlay options bag - #20448
Conversation
ErrorCode and makeApiErrorSchema's code cast named only z.ZodType's output parameter, so ApiError.code (an input type) was unknown. Both now name Output and Input. listViewKindBlocks() returned Record<string, z.ZodTypeAny>, so the flattened list overlay's legacy options bag was a string-keyed record of unknown. Its return type is now a mapped type over the kinds that name a block, each entry zod's own partial type of the kind block. The runtime loop is unchanged. Two type-pin files hold both. Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW Co-authored-by: Claude <noreply@anthropic.com>
Naming Input as the inline union doubled the union's prints at every schema embedding ApiErrorSchema (+3.36 MB of declarations). The alias is printed by name, so the built declarations shrink by 3.32 MB instead. Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW Co-authored-by: Claude <noreply@anthropic.com>
…rrowing Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check6 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 137 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 f24453daaa3950c4074a4c55ad586f4a46e49acd && git checkout f24453daaa3950c4074a4c55ad586f4a46e49acd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7fa3e3e07cc67877ae2f5d49eac42665c0515fda b26d6506bfd199bbc04421aae8a9b38081ef05bc && git checkout -B drift-repro 7fa3e3e07cc67877ae2f5d49eac42665c0515fda && git merge --no-ff b26d6506bfd199bbc04421aae8a9b38081ef05bc
node scripts/docs-audit/affected-docs.mjs --json 7fa3e3e07cc67877ae2f5d49eac42665c0515fda |
Contract reviewServed-tier: Inputs: card #19920 (body and all 18 comments), card #20450 (body), PR #20448 (body, the 6-file list and the per-file patches, which are the whole net diff: 6 files, +281/−4, three commits), the at-tier record 5864698088 on PR #20369 (its ③5 is what routed the three sites to this round), and the 53 check-runs on the head, re-polled until none was in progress. Context for unchanged code came from GitHub's patch context and from this repository's checkout in the reviewer's container (not a git checkout, and its ① Derived judgmentsAccept set: none moves — RIGHT. All three source hunks change types only and leave every value expression as it was:
Public surface, each change named:
Nothing derived is wrong or missing. ② Semver level
③ Boundary flagsFrom the dev report 5870687948 (three open questions, nine deviations, two out-of-scope findings), the PR body's own flags, and the seat's ACCEPT 5870763898.
Implemented-by: VERDICT: FAIL Generated by Claude Code |
…#19920) Claude-Session: https://claude.ai/code/session_01B3TqpoQbTAfG7G74GMDWNW Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #19920 (body and all 18 comments, among them the claim 5868438648 as amended in place and the three dev reports), card #20450 (body; no comments), PR #20448 (its body as edited, the 6-file list, the net diff against This is a patch round on the prior head ① Derived judgmentsA. Accept set: none moves — RIGHT. Every value expression in the three source hunks is as it was:
B. Public surface, each change named and judged:
Nothing derived is wrong or missing. ② Semver level
③ Boundary flagsFrom the dev report 5870687948 (three open questions, nine deviations, two out-of-scope findings), the seat's ACCEPT 5870763898, the prior record 5871015216, and what this patch round introduced.
Implemented-by: VERDICT: PASS Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 36434919545 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
…ewFilterOperator (objectstack-ai#20450) (objectstack-ai#20503) Fixes objectstack-ai#20450 Clause-②: no (narrowing) ## What `ViewFilterRuleSchema.operator` is a `z.preprocess` over the alias fold, and zod 4.6.1 types a preprocess's INPUT from its function's parameter (`preprocess(fn: (arg: B, ctx) => A, schema): ZodPreprocess(U, B)`, with `B = unknown` by default). The function was `normalizeFilterOperator(op: unknown)`, so `ViewFilterRule['operator']` was `unknown`, and `{ field: 'status', operator: 42 }` compiled as a rule on every carrier. This PR lands triage's direction A: the typed input of `operator` is the canonical `ViewFilterOperator`. The runtime fold is untouched, so stored `sys_metadata` rows and plain-JS producers that carry an alias still parse and fold. - `packages/spec/src/ui/view.zod.ts`: the preprocess now runs through a module-private wrapper, `foldAuthoredViewFilterOperator(op: ViewFilterOperator): string`, which returns `normalizeFilterOperator(op)`. The wrapper is not exported, so no export is added and `api-surface/` is byte-identical. - `packages/spec/src/ui/view-filter-operator-input-typed.test.ts`: the two-half pin (see the verification record below). - `packages/spec/src/migrations/entries/semantic/18.view-filter-rule-operator-input-canonical.ts` and the regenerated registry region: the ADR-0087 registration (see Acceptance notes, item 1). - `.changeset/20450-view-filter-operator-input-typed.md`: `@objectstack/spec` `minor`, a BREAKING banner, a FROM → TO table and the one-line fix, `Clause-②: no (narrowing)` (corrected in round 2, the seat's answer `5877491493`), and the `registered` disposition. ### Which site, and why not `normalizeFilterOperator`'s own parameter Triage's execution line named `normalizeFilterOperator`'s parameter. I measured that site first. It is exported (`api-surface/ui.json`), and its callers pass raw strings or `unknown` by design: - `packages/lint/src/validate-preset-comparands.ts:634` passes a stored rule's `rule.operator`. - `packages/rest/src/view-filter-rule-lowering.ts:72` passes a stored rule's `operator`. - `packages/spec/src/conversions/registry.ts` passes `op.slice(1)` (:10984), `op` (:11167) and the literal `'eq'` (:11060, :11187). - `packages/spec/src/ui/filter-rule-array.ts:152` passes the literal `'eq'`. - objectui at its pin calls it in `ObjectView.tsx`, `ListView.tsx`, `UserFilters.tsx`, `filter-converter.ts`, `filter-builder.tsx` and `viewFilterFold.ts`, each with a string or `unknown`. Narrowing that parameter would break every one of these callers. A canonical-in, canonical-out fold would also have nothing left to fold. The smallest correct site is the preprocess's own input at :923. The wrapper reaches the same result (the typed input is canonical) and leaves the exported fold's signature as it was. ## Verification record Final gate union on HEAD `bed8cabb44` (after merging `origin/main` at `fc0db22bcf` through `scripts/pm/os-regen-merge.sh`). My delta against the merged main commit is the five files above: +288 / -5. **Premise, measured at base `8e02859185` against the built `dist/*.d.ts`.** I ran a downstream probe that resolves `@objectstack/spec/ui` through the package `exports`. `tsc` gave EXIT 0 on all of these: `operator: 42`, `operator: 'eq'` and `operator: Symbol('x')`, on `ViewFilterRule` and on three carriers (`ListView['filter']`, `ViewTab['filter']`, `InterfacePageConfig['filterBy']`). **Reverse verification.** I rebuilt spec with the change and ran the same probe. `tsc` gave EXIT 2 with 9 errors, one on each of the 9 non-canonical lines. The canonical line still compiles. The runtime is byte-identical to base: a number is refused with `code: 'invalid_value'`, `path: ['operator']` and the canonical `values`; `'eq'` folds to `equals`; `'NotIn'` folds to `not_in`. **Compile-half ablation** (at `60102b7b42`, through `scripts/ablation-replace.mjs`). I widened the wrapper's parameter back to `unknown`. The anchor went 1 → 0 and the blob went `4403a5bb` → `d8324f31`. `check:test-typecheck` then reported one problem: `src/ui/view-filter-operator-input-typed.test.ts: 6 type error(s)`, which are the six `@ts-expect-error` directives (TS2578). Restored: the blob equals HEAD `4403a5bb` and `git diff HEAD` is empty. **Runtime-half ablation** (at `bed8cabb44`). I replaced the wrapper body `return normalizeFilterOperator(op);` with `return op;`, which still compiles. The pin went `3 failed | 2 passed`: the three fold tests failed; the canonical and refusal tests held. Restored: the blob equals HEAD `07956823` and `git diff HEAD` is empty. **Spec package** (post-merge, `bed8cabb44`): - `pnpm --filter @objectstack/spec typecheck` exit 0 (src, scripts and the test layer). `check:test-typecheck: OK — 53 file(s) / 251 error(s) / 138 pinned signature(s) held`, so no test file moved. - `vitest run --project local`: `Test Files 573 passed (573)`, `Tests 16794 passed | 1 todo`. - `check:generated`: all 15 generated artifacts up to date. Nothing records the input type: `api-surface/`, `export-origins/`, `declaration-map/`, `authorable-surface/` and the JSON schemas are unchanged. `spec-changes.json` and the upgrade guide hold because in-progress major-18 entries reach them at release (control: the sibling `view-filter-rule-scalar-operator-array-refused` is in neither). **Consumer typechecks.** These ran against spec `dist` built from `60102b7b42`; the narrowing is identical after the merge. They are a selection of `@objectstack/spec`'s downstream consumers (the `...@objectstack/spec` direction), not the whole 73-package closure. Every one exited 0, and each test-layer ledger held exactly: - `@objectstack/lint` (2 files / 6 errors) - `@objectstack/metadata-protocol` (plain `tsc`, tests included) - `@objectstack/rest` (0 / 0) - `@objectstack/objectql` (40 / 234) - `@objectstack/platform-objects` (1 / 3) - `@objectstack/plugin-sharing` (2 / 3) - `@objectstack/plugin-security` (0 / 0) - `@objectstack/plugin-audit` (0 / 0) - `@objectstack/plugin-approvals` (8 / 324) - all five examples: `app-showcase`, `app-todo`, `app-crm`, `app-multi-package`, `embed-objectql` The rest of the closure is CI's. **Census of typed producers.** Non-test `.ts`/`.tsx` under `packages/`, `examples/` and `apps/`: - 3 `operator:` literals carry an alias spelling. None is on a view-filter carrier: an app-showcase `lookupFilters` entry, and two doc comments in other dialects. As a control, the same grep for canonical spellings hits 113 times. - 2 non-literal `operator:` writes. Neither is a view-filter rule: a cross-field refusal record in `formula`, and a text-operator door verdict in `objectql`. - 0 non-test typed producers write an alias through `ViewFilterRule`. No test fixture needed an escape: every alias-carrying fixture already reaches the fold through `.parse` / `.safeParse`, which take `unknown`. **objectui at its pin `dd3f7e1be3`** (read-only). I exported its sources with `git archive` into a scratch directory, so nothing was written to the objectui repo. I compiled `packages/*/src` and `apps/console/src` against this branch's spec `dist`, with its root compiler options, `@object-ui/*` mapped to source, and React 19.2.8 / `@types/react` 19.2.18. There were 0 errors naming the operator union or `ViewFilterOperator`. - Positive control: a scratch-only file wrote a `string`, an alias, and a `string` through objectui's own `RecordRelatedListComponentProps.filter`. Exactly those 3 lines errored, and every other error was identical to the run without the control. - The typed producers read the same way. `viewFilterFold.ts:211` casts to `ViewFilterRule['operator']`. `ObjectDataPage.tsx` builds a record and pushes the parse OUTPUT. - objectui resolves published `@objectstack/spec` (`^17.x`), so it meets this at its next spec bump, not through the Console Pin Gate. **Gates.** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 88 families on `bed8cabb44`. I ran 87 and each exited 0. `--ran` reconciliation: `88 derived famil(ies) accounted for — 87 run, 1 NOT-MEASURED`. - `check-adr-0087-registration`: `[BREAKING+bang+clause-②-narrowing] registered view-filter-rule-operator-input-canonical (new here: …)`. - `check-changeset-no-major`: no `major`. Its level axis is PR-scoped and reads NOT APPLICABLE locally, so that half is CI's. NOT MEASURED: `check:type-check-debt`. Reason: its `--re-measure` runs a full-repo `turbo run build`, which does not fit the foreground cap. I took targeted readings instead: - The root program (`tsc -p tsconfig.json`) has 26 errors, equal to its DEBT ledger's 26, and 0 of them name the operator union. - The other three DEBT packages (`cloud-connection`, `hono`, `observability`) contain no `operator` or `ViewFilter` text. ## Acceptance notes 1. **Surface extension: one hand-written file beyond the claim's file surface.** `packages/spec/src/migrations/entries/semantic/18.view-filter-rule-operator-input-canonical.ts` sits outside the claimed surface; its generated registry region is inside it. Why: - The changeset carries the FROM → TO the dispatch asked for, and `check-adr-0087-registration` refuses `no-migration-prescription` on a body with a prescription. - `type-surface-only` is closed to any diff that touches `packages/spec` or moves a `*.zod.ts` (ADR-0087 D8, predicates 2 and 3). The ADR's reason is that for such a diff the ledger *is* a channel that can carry the change. - So the honest disposition is `registered`. The precedent is `spec-type-alias-input-suffix-retired`, a type-only spec change registered the same way. - The entry records that nothing at rest is rewritten (no D2 conversion). The family predecessor, objectstack-ai#20448, took `no-migration-prescription` without a concrete rewrite. The reviewer may prefer that route; it means dropping the FROM → TO table. 2. **`Clause-②` value.** Round 1 carried the claim's `yes`. By `scripts/pm/clause2-line.mjs`'s own definitions this diff reads `no (narrowing)`, "NOT a widening, but breaking": it adds no export and widens no accept set, as the family precedents (objectstack-ai#19514, objectstack-ai#20448) declared. The seat answered `5877491493` (Q1: A), and round 2 corrected the changeset line and this body's declaration line to `Clause-②: no (narrowing)`. The level (`minor`), the BREAKING banner and the ADR-0087 marker are unchanged. 3. **Prose that now names the old spelling (noted, not filed).** These still describe the operator as `z.preprocess(normalizeFilterOperator, …)`: `packages/metadata-protocol/src/protocol.ts:1187`, `protocol.graft-normalized-operators.test.ts:6`, `packages/spec/src/ui/view-filter-rule-wire-id.test.ts:185`, and objectui's `packages/types/src/zod/complex.zod.ts:283`. The preprocess now runs a wrapper that delegates to that fold, so each statement is still true about behaviour. Only the literal spelling drifted. 4. **Observation (not filed).** The fold's case-folded branch reaches only aliases whose lower-cased form is itself a table key. `GREATERTHANOREQUAL` does not fold: the table holds `greaterorequal` and `greaterThanOrEqual`, not `greaterthanorequal`. This is deliberate per the table's docblock ("the folds exist per measured legacy spelling"). The pin tests the case-fold branch on `GT` and `NotIn` only, for that reason. 5. **The objectui measurement has a stated blind spot.** Third-party modules (radix, lucide and others) were not installed, so the types that flow through them collapse to `any` and cannot error either way. The positive control shows the channel is live for every spec-typed position. --- _Generated by [Claude Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
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.codeand the flattened list overlay's legacyoptionsbag. 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
0283cb924and by probes against the source; the TO column is also probed against the builtdist.ApiError.code(api/error-code-ledger.zod.ts,api/contract.zod.ts)unknown.ErrorCodewas cast toz.ZodTypenaming only its OUTPUT type parameter, andz.ZodType's INPUT parameter defaults tounknown, so the input type ofApiErrorSchematypedcodeasunknown:{ code: 42, message: 'x' }compiled as anApiErrorwhile the schema refuses it atcode. The sameunknownreached theerror.codeof every response type built onBaseResponseSchema(58 input aliases, measured) and eachApiErrorrow of a batch result.makeApiErrorSchemarepeated the one-parameter cast for a caller-supplied vocabulary.ErrorCode: the cast names both parameters, each spelled with the existingErrorCodetype alias.makeApiErrorSchema: both parameters named, the standard catalogue plus the caller's codes. The…Parsedtypes do not move: theircodewas already typed.optionsbag (ui/view.zod.ts)unknown, on the list overlay member and so onViewMetadata,ViewMetadataParsed,AssembledViewArtifactandAssembledViewArtifactParsed, becauselistViewKindBlocks()returned a record of string toz.ZodTypeAny.options: { foo: 1, kanban: 42 }type-checked as all four while the member refuses both keys.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'stypeenum 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 astring[], so the cast is what carries the caller's codes into the type; dropping it would also change the returned schema class (toZodEnum), a public-type change beyond this item. The defect was the missing input parameter, and that is what changed.The
ErrorCodecast exists because the spread erases the members tostring, not to dodge declaration size (mechanism assumption A1). But naming the input parameter DID hit declaration size, measured, and that fixed the spelling:74a132da1): the built declarations grew 30,647,033 to 34,006,427 B (+3.36 MB, +11.0%);api/index.d.tsalone +1,262,450 B. Declaration emit prints an inline union literal by literal wherever a schema embedsApiErrorSchema(78 sites in theapientry), and the input parameter doubled those prints.ErrorCodealias (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
0283cb924against head code539295c9e)74a132da1,539295c9e).check-dts-referencesresolves 394/394 relative references across the 130 (382/382 across 128 at base).api/index.d.ts(.d.mtsthe same, within 2 B)automation-api.zodchunk.d.ts(and.d.mts)api-assembled/index.d.ts(and.d.mts)contracts/index.d.ts(and.d.mts)view.zodchunk.d.ts(and.d.mts) — item 3error-code-ledger.zodchunk, new (.d.ts).d.ts/.d.mtsfilesview.zodchunk, +7,493 B per chunk (2.7 times that figure, the same order of magnitude, +1.5% of the chunk), 0 TS7056, noanyanywhere (check:exported-anygreen). 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, throughscripts/ablation-replace.mjs)Each pin file compiled under
tsconfig.test.json's options. The pins import./contract.zod/./view.zodrelatively, so the subject issrcand nodistis on the resolution path.ErrorCodecast naming the output parameter onlyapi/api-error-code-type.test.tsmakeApiErrorSchema's cast naming the output parameter onlyapi/api-error-code-type.test.tslistViewKindBlocks()returning a record of string toz.ZodTypeAnyui/view-overlay-options-type.test.tsEvery 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 HEADempty); an independentgit hash-objectcheck of all three files after the legs matches HEAD, andgit status --porcelainis empty.Tests and gates
Code is identical at
539295c9eand4358d1a33(4358d1a33adds the changeset only).typecheckexit 0,check:test-typecheck: OK — ... 53 file(s) / 251 error(s) / 138 pinned signature(s) held, and--listFilesOnlyputs both new pin files in its 540-test-file program;vitest run --project localat4358d1a33: 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.metadata-protocol):@objectstack/metadata-protocoltypecheck exit 0 (192 test files in its program) and tests 189 files passed, 3 skipped, 2,745 tests passed, 19 skipped;@objectstack/typestypecheck exit 0 (23 of 23 test files in its program) and tests 22 files, 685 passed;@objectstack/clienttsc --noEmitoversrcexit 0.dist, from a consumer program importingdist/apianddist/ui: 0 diagnostics, where every@ts-expect-error(a numericcodeonApiError, an invented one onBaseResponse, a numericoptions.kanbanonViewMetadata, an unknown kind onAssembledViewArtifact) is consumed and a tuple compiles only ifApiError.codeis neitherunknownnorany.dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat4358d1a33derived 86 commands (the dispatch's 75 plus 11); all 86 run, exit codes written to disk first: 83 exit 0 (check:lean-entry-closureafter buildingobjectql), 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-registrationreads[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-changesetexit 0.eslint --no-inline-config --format jsonover the 5 changed.tsfiles: 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.test:repo(it held the verify lock for the whole foreground window, about 595 s, without finishing, twice); the@objectstack/clienttest-layer typecheck (its 12 dev dependencies includeruntimeandrest, a 33-package build); the two exit-3 gates above; the objectui and cloud builds.Consumer census
ApiErrorSchema's shape (58 input names; their…Parsedtwins 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 ascasts and['data']reads),@objectstack/metadata-protocol(toRowApiError's cast fromanyafter asafeParseguard, andas BatchUpdateResponsecasts) and@objectstack/types(PickofApiError's optional fields, notcode). All three typechecks are green above; neither named consumer file needed an edit.ViewMetadataSchemaandAssembledViewArtifactSchemaare called withsafeParseinobjectql,restandmetadata-protocoltests andobjectql'sengine.ts; both schemas' own static types are the erased unions, so no typedoptionsread exists outside spec.f8a9d0fb.ApiErrorappears only asPickofuserMessage(two files); none of the four view types is named. Neither narrowing reaches it (from reading, not compiling).Item 2,
ViewFilterRule.operator: not changed, a fork for triageoperatorisz.preprocess(normalizeFilterOperator, z.enum(VIEW_FILTER_OPERATORS)). zod types a preprocess's input as its function's parameter type, andnormalizeFilterOperatortakesunknown, so{ field: 'status', operator: 42 }compiles as aViewFilterRule(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'inapp-showcase'sinvoice.object.ts, is a field'slookupFilters, a separate closed dialect.)where, skill trigger conditions, analytics).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'sViewFilterRule(viewFilterFold.ts,ObjectDataPage.tsx) fold throughnormalizeFilterOperatorbefore typing, so the canonical id is what reaches the type. objectui at9f0c84a44(its current head) emits the 20 canonical ids only.sys_metadatarows: the alias table exists for them; they are read through the runtime parse, whose input isunknownwhatever the type says.The three options, the four axes and the recommendation are in the
os-dev-reporton #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, leaveunknownwith 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):unknownat base and at head, and none belongs to this family:FlowValueSlot,AssignmentValueand theirParsed(value slots),GetPublishedMetaItemResponseand itsParsed(opaque by ruling).ApiError.code, and none entered. The only family site left isViewFilterRule.operator(item 2). Every other key the census reads is declaredz.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.codeandViewMetadata.defaultswere checked by hand and are both declaredz.unknown().optionsonViewMetadata,ViewMetadataParsed,AssembledViewArtifactandAssembledViewArtifactParsed, and none entered. Blind spot, declared: deeper nesting is not walked.Clause-②
Line 2 and the changeset (
b26d6506b) both readClause-②: no (narrowing). The changeset keeps its BREAKING banner and the ADR-0087 markernot-required (no-migration-prescription), socheck-adr-0087-registrationstill reads the narrowing. The value isnobecause 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 emittedcontract.zoddeclaration), where the base printed 2. A local generic alias would name it; not done here, being one bounded declaration.lookupFiltersis its own closed operator dialect (eq,ne,gt,lt,gte,lte,contains,in,notIn), whose members are spellingsViewFilterRuletreats as deprecated aliases. Both are enforced; noted for item 2's triage, not filed.view-overlay-viewkind-type.test.ts: tsc judges the type half, vitest the runtime half; the refusal cases assert the issuecodeandpath, not a bare failure.Line 1 was changed from the partial-landing marker to this closing keyword by the
domain:specseat 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 valueyeswrong for this diff; the changeset line moved with them inb26d6506b, and the claim on #19920 was amended in place. The stalePart ofparagraph was removed.