refactor(types): the casts outside src/ and workers/ - #11368
Merged
Conversation
…ebuilding a typed query
Four casts, in the two places outside src/ and workers/ that ship code.
## The client's paginators (2)
Both generators widened the caller's typed query into a
`Record<string, unknown>` so they could write `cursor` onto it, then
asserted it back with `as unknown as QueryParams<Path>` at every
request. Two casts per page, around a value whose type never changed --
and between them the query was unchecked for the whole loop.
The cursor was never the caller's field. It belongs to the PAGINATOR,
and `metagraphedFetch` already treats `query` as an untyped bag when it
builds the URL, so it now takes `cursor` as its own option and appends
it after the query. The caller's `QueryParams<Path>` passes through
untouched, and there is nothing left to construct.
That also removed the reason `next_cursor` was read through
`page as { meta?: { pagination?: { next_cursor?: unknown } } }` and
carried straight back into the next request as `unknown`. `nextCursorOf`
narrows it, and treats a non-scalar token as no token -- the API's
cursors are opaque strings, and stringifying an object would page
forever on "[object Object]".
(I tried two smaller fixes first, and the client's own tsconfig -- which
the root one excludes -- rejected both: `QueryParams<Path>` resolves to
`never` for a route with no query, so the generic cannot be spread, and
`{}` is not assignable to it either. The construction had to go, not the
cast around it.)
## The non-empty tuples (2)
`z.enum` wants `readonly [string, ...string[]]`.
`sectionsOf` already threw on an empty list and then asserted the proof
into the type; destructuring the head narrows it with no assertion at
all. `VALIDATOR_ECONOMICS_SORTS` was declared `as const` and cast to the
MUTABLE tuple form, which `sortSchema` never asked for -- it takes
`readonly`, so passing it directly was always enough.
Verified: root tsc clean, packages/client tsc clean (checked separately
-- the root config excludes it), eslint clean, prettier clean,
validate:double-assertions / contract-drift / types green, 237 tests
across 18 suites passing.
24 casts across five validator scripts, all the same shape:
`createLocalArtifactEnv() as unknown as Env` and its multi-line sibling,
each script inventing its own way to hand a Worker handler an env.
The suites had already solved this: `mockEnv` in tests/row-type.ts wraps
the one honest assertion -- a stub standing in for live platform
bindings a unit process cannot construct -- behind a named helper. The
scripts drive the SAME handlers and were doing it by hand.
`scripts/lib/worker-env.ts` (moved from tests/helpers, which now
re-exports it) serves both. Its parameter is keyed on the real env with
`unknown` values, so a binding NAME the Worker does not have is still an
error while a hand-rolled stub is allowed.
I first tried making `createLocalArtifactEnv` RETURN an `ApiWorkerEnv`,
which is the more obviously correct fix. It is not the better one: the
suites build an env and then mutate it (`env.DATA_API = { fetch }`), and
a real `Fetcher` also declares `connect`, so a typed return pushed
platform-type strictness into ~30 fixtures to remove 24 casts from
scripts. Wrapping at the call sites gets the same result with no test
churn.
That attempt did surface four more fixtures still setting the retired
`METAGRAPH_HEALTH_DB`, and a tripwire -- "rejects unsupported query
parameters before the store" -- spying on it, which therefore cannot
fire. Worth fixing on their own; not in this commit.
scripts: 54 -> 30.
Verified: tsc clean, eslint clean (uncached), prettier clean,
validate:api green (it drives the real handlers through this change),
plus double-assertions / types / boundary-casts / unreferenced-exports.
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
metagraphed-wss-lb | 412cda0 | Aug 16 2026, 03:18 AM |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #11368 +/- ##
=======================================
Coverage 95.57% 95.57%
=======================================
Files 762 762
Lines 46076 46081 +5
Branches 16942 16943 +1
=======================================
+ Hits 44039 44044 +5
Misses 526 526
Partials 1511 1511
🚀 New features to boost your workflow:
|
Six scripts each carried the same paragraph of apology:
// ajv-formats has no named export to sidestep the NodeNext/esModuleInterop
// resolution -- cast to its real callable signature.
const addFormats = addFormatsPlugin as unknown as (i: Ajv2020) => void;
The comment is wrong about the cause and the cast is wrong about the fix.
ajv-formats ships CJS, so under NodeNext the namespace import resolves to
`module.exports` -- an object whose `default` is the callable -- and printing
its keys says so plainly: `['__esModule', 'default', 'module.exports']`.
Nothing about it is unreachable; `addFormatsPlugin.default(ajv)` type-checks
today. The cast was not routing around a missing export, it was restating a
signature that already existed one property deeper.
Restating it six times is the part that costs something. The assertion pins
the plugin's arity and parameter type at each site, so the day ajv-formats
takes an options argument, or narrows what it accepts, six files keep
compiling and start failing at runtime instead. scripts/lib/ajv-formats.ts
now owns the interop in one place and takes a real `Ajv2020`, so the compiler
checks the call again -- and a future change to the plugin breaks one file.
check-response-conformance.ts had a second, larger copy of the same mistake.
Its five siblings import `{ Ajv2020 }` by name; it alone imported the default
and then hand-wrote a type restating the entire constructor -- `addKeyword`,
`compile`, `addSchema`, the generic parameters -- to assert the class into.
That type had already drifted from ajv's own. It uses the named export now,
and the restatement is gone with it.
scripts/lib/ is the right home rather than a shared script: worker-env.ts
established that a per-concern helper under lib/ is what stops a fixture
pattern from being copied a seventh time.
validate:double-assertions shipped in #11361 scanning `src` and `workers` at a flat budget of zero. That was the whole population at the time, so the gate was honest -- but it left `scripts`, `schemas-src` and `packages` unwatched, and those are not empty. They held 54 double assertions between them when this branch started. A gate whose scope stops short of where the casts are is a gate that reports OK on the wrong thing. The budget is now per top-level directory: src: 0 workers: 0 schemas-src: 0 packages: 0 scripts: 21 Four are at zero because they are genuinely at zero -- #11339 and #11361 swept src and workers, and this branch took schemas-src and packages there. A regression in any of them is a plain failure, not a number to renegotiate. `scripts` gets a ratchet rather than an exemption. What is left there is third-party shape -- a Durable Object's state, GraphQL's type unions, coverage-report JSON -- where each fix is its own small piece of work, and pretending otherwise gets the directory added to an exemption list instead. The repo already knows how that ends: validate-untyped-db-reads.ts records that a declared exemption list stops being read the moment it outgrows a screen, and then hides exactly what it names. A number cannot hide anything. Both halves of the ratchet are enforced, which is the part worth stating: a budget ABOVE the real count also fails. A ceiling nobody is holding is not a ceiling -- the next addition slides in underneath it and the gate stays green. So the failure text names the direction and the fix ("Lower BUDGETS[...] to 19 in the same change that removed them"), and the number tracks reality rather than intent. Verified by mutation in all four directions: +1 in a zero area fails, +1 in packages fails, a budget one under reality fails, a budget one over reality fails, clean exits 0. `tests` is still not scanned, and that stays a judgement rather than an oversight. A unit process cannot construct a `KVNamespace`, `Hyperdrive`, `ExecutionContext` or `R2Bucket`; there the assertion IS the mechanism, and counting it would only teach people to read past the number. The discipline that works for fixtures is centralising them behind a key-checked helper -- which is how scripts/lib/worker-env.ts caught four fixtures still setting a binding retired two releases ago.
`as never` is the strongest thing you can say in this language and it is
almost always said by accident: every value is assignable to `never`, so the
assertion does not widen a type, it deletes the check. Three remained in
scripts/, and each was covering something different.
**scripts/lib.ts — the DNS pin.** `createPinnedLookup` is an SSRF control: it
resolves one hostname to the one address already vetted and refuses every
other lookup, closing the DNS-rebinding window between the safety check and
the socket. It was handed to undici as `lookup: createPinnedLookup(...) as
never`, which meant nothing checked that the pin still matched the shape
undici calls. It types cleanly as Node's own `LookupFunction`; the only real
mismatch was that the refusal path called `callback(err)` with no address
where the contract declares one. It now passes `[]` -- an empty result is the
truthful answer for a lookup that resolved nothing, and it is the safe one:
a caller that ignored the error gets no route to the host the pin exists to
refuse. The test dropped three assertions of its own along the way. It had
declared the callback as `address?: string` where the contract says
`string | LookupAddress[]`, so the `{ all: true }` case was type-checked
against the single-answer shape and agreed with itself.
**scripts/review-treasury-readings.ts — a write path.** `PROMOTABLE_STATES
.includes(state as never)` tested membership and then threw the answer away:
`state` stayed `string` afterwards, and that string goes into
`SET review_state = $3` and is compared against the literal `"reviewed"` ten
lines later. Both are now checked, because the value is PARSED --
`PromotableTreasuryReviewStateSchema`, derived in schemas-src/treasury.ts by
excluding `candidate` from the existing enum rather than filtering a copy at
the call site. A fourth state added to the vocabulary lands in the promotable
set automatically, and `.options` still prints the list for the usage message.
**scripts/refresh-og-image.ts — a genuine gap between two packages.**
satori-html returns satori's `VNode`; satori's published signature says
`ReactNode`. They describe the same runtime object and neither package
declares the other, so this one keeps a single assertion -- but `as ReactNode`,
not `as never`. The difference is that `as never` also accepted `undefined`,
which is what a `renderMarkup` returning nothing would have handed over.
Budget 21 -> 18.
Three Durable Object hubs declared their constructors as taking the whole
`DurableObjectState`, so every double outside the runtime had to be asserted
into it -- `{ storage: inMemoryDoStorage() } as unknown as DurableObjectState`,
three times in validate-mcp.ts. That assertion claimed the object had
`blockConcurrencyWhile`, `abort`, `id`, `props` and a fifteen-member
`DurableObjectStorage`. It had one key. Nothing checked the claim, so nothing
noticed when it drifted from the truth:
- all three hubs call `state.waitUntil` for usage telemetry, inside a `try`
that swallows the resulting TypeError. No double supplied it. Every
`validate:mcp` run emitted no telemetry at all and reported success.
- `ChainFirehoseHub` reads and writes `state.storage` on the head-poll path.
Its double was `{ getWebSockets: () => [] }` -- no storage whatsoever.
- tests/head-poller.test.ts had built the right shape by hand but wrote
`new ChainFirehoseHub(state as never, env as never)`, so the fixture was
unchecked too -- and it was missing `acceptWebSocket`.
workers/do-state.ts names what each hub actually uses, following
`WaitUntilLike` and `HyperdriveLike` in src/pg-sql.ts. A real
`DurableObjectState` satisfies all three structurally, so the runtime is
untouched; a double now has to implement what the hub calls, and a hub that
starts calling something new breaks the build instead of throwing into a
`catch`.
## The bug that fell out
Narrowing the storage surface meant deciding what `get` returns, and the
answer is `unknown`. `DurableObjectStorage.get` is generic and the hubs were
naming the type they expected -- `get<number>("head:last_seen")` -- about a
value written by a PREVIOUS deploy. That is a claim, not a check, and
mcp-session-hub.ts already says so at length in its own `hydrate()` before
parsing with zod.
For the head cursor the claim was load-bearing. `heightsToEmit` computes
`lastSeen + 1`, which CONCATENATES for a string: `"14" + 1` is `"141"`, the
emit loop starts past the head and never runs. Measured, not reasoned --
`heightsToEmit("14", 16)` and `heightsToEmit("14", 99)` both return `[]`.
Nothing is emitted, so nothing is written back, so the corrupt cursor is never
overwritten. The lane goes quiet at every head, permanently, and raises
nothing: no exception, no failed verdict, no alarm. It is precisely the shape
of silence this repo has been bitten by before, and it was one deploy writing
one bad value away.
The cursor is now checked. The regression test asserts the cursor MOVED --
a still-`"14"` value IS the wedged lane -- and fails without the fix.
Budget 18 -> 15. All 55 CI validators pass; validate:mcp still completes its
242 tools and both round trips, now with telemetry that actually runs.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
metagraphed-data-api | 502ceb7 | Aug 16 2026, 02:55 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
metagraphed-registry-sync-api | 502ceb7 | Aug 16 2026, 02:54 AM |
Nine assertions across the changelog path, and pulling on them found that two
of the three shapes they claimed were wrong in opposite directions.
**CoverageSnapshot was over-strict, and nothing wanted it.** It declared four
required numbers, so all four call sites had to assert their way past it --
including `(currentCoverage || {}) as unknown as CoverageSnapshot`, which
claims four required numbers about `{}`. The only consumer is `delta`, which
already takes `unknown` and checks with `Number.isFinite`, because the value
is read from a document a PREVIOUS publish wrote. The checking was always the
right half; the declaration was ceremony that cost four assertions. It is now
`Row`, and `delta` narrows with `typeof` first so its own three assertions go
too -- `Number.isFinite` is a guard, not a narrowing, which is precisely why
they were there.
**SubnetEntry was load-bearing, and nothing enforced it.** `netuid` is the Map
key the entire diff turns on. Rows arrive as `Record<string, unknown>` from
JSON and were asserted in wholesale, so an entry without a `netuid` keys
`previousByNetuid` under `undefined` -- and every other unidentifiable entry
collides with it, leaving one arbitrary subnet standing in for all of them.
A netuid that survived a round trip as `"7"` instead of `7` is a different Map
key again, so the same subnet reports as both added and removed on every
publish, forever, in a document whose whole job is to say what changed.
The check now lives INSIDE `diffSubnets` rather than at its callers. A
signature promising `SubnetEntry[]` only moves the assertion up one frame,
which is where it had been; taking `Row[]` and narrowing internally means
there is no version of this that skips it. The new tests needed no casts to
construct their bad input, which is the tell.
**One hole was invisible.** `previousSubnets?: { subnets?: SubnetEntry[] }`
made the property optional, and TypeScript does not check a source index
signature against an optional target property -- so a bare
`Record<string, unknown>` straight from JSON satisfied that parameter with no
cast and no complaint, and `subnets` was trusted as `SubnetEntry[]` all the
way down. `subnets` is required now, so it cannot.
Also: the three artifact-digest collectors build `{ path, hash }` themselves
and now say so, which removes three more assertions at no runtime cost, and
validate-schema-vocabularies.ts stops asserting two module namespaces into
`Record<string, readonly string[]>` -- a claim about every export in both
modules, made by the script whose job is to check whether those exports are
still vocabularies. Its `Array.isArray` check left elements `any`, so an enum
that had picked up a number was sorted and joined into the comparison as
though it were a name; both sides now go through one guard that checks the
elements, and the failure message distinguishes "gone" from "not a
vocabulary". Verified by mutation: real drift is still caught, and a number
in schemas/entity.schema.json's category enum is now reported as malformed
rather than compared.
Budget 15 -> 6. Build re-run end to end: byte-identical artifacts.
The last six, and none of them needed an escape hatch.
**check-graphql-conformance.ts** was reimplementing graphql-js. `namedTypeOf`
walked `ofType` by hand through `Row`, `isLeaf` compared
`constructor.name` against `"GraphQLScalarType"`, and `objectFieldsOf` did the
same for `"GraphQLObjectType"` before asserting `getFields()` back out. The
library exports `getNamedType`, `isScalarType`, `isEnumType` and
`isObjectType`, and the last three are type PREDICATES -- so the narrowing is
real and both assertions go with them. Reading a class name is also the kind
of check that breaks silently under any bundler that mangles names, and this
is the script that runs against production.
**probes-smoke.ts** asserted a registry row into `ProbeSurface`, claiming
`url: string` and `kind: string` about a `Record<string, unknown>`. What
happens without them is worse than a crash: an undefined url probes as a
FAILURE, so the surface is published DOWN rather than unprobeable, and its
subnet's health falls for a reason nothing in the artifact explains.
`isProbeSurface` now lives beside `ProbeSurface` in src/health-probe-core.ts,
so the Worker cron prober can use the same predicate, and probes-smoke throws
naming the offending surface ids -- validate:surface already requires both
fields, so a row failing here means something upstream broke and silence is
the wrong answer.
**load-alpha-price-history.ts** declared `PgLike.connect(): Promise<void>`.
A real `pg.Client.connect()` resolves to the client, so the interface the
module wrote to describe pg did not match pg, and the assertion that hid that
also stopped the compiler checking `query` and `end` at the same call. The
module awaits `connect()` for the side effect; the contract says
`Promise<unknown>` now, and `new pg.Client(...)` satisfies it directly.
**build-artifacts.ts** had two. `stdout as unknown as Buffer` was simply
stale -- `promisify(execFile)` with `encoding: "buffer"` has returned `Buffer`
for as long as these types have existed. And `collectArtifactSizes` returned
`Row[]` while `evaluateArtifactBudgets` takes `ArtifactSize[]`, so the call
asserted through `Parameters<typeof ...>[0]`, which restates a signature by
reference and reads as though it were checking something. The collector names
its own shape now -- as a `type` and not an `interface`, deliberately: an
interface has no implicit index signature, so it would not be assignable to
the `Row[]` the tier-counting helpers take, and naming the shape would have
cost an assertion at each of them. That is roughly how the original one came
to exist.
## Zero
src 0/0, workers 0/0, schemas-src 0/0, packages 0/0, scripts 0/0
`scripts` went 54 -> 21 -> 15 -> 6 -> 0 across this PR, the budget falling in
the same commit as each cluster it belonged to, because the gate fails a PR
that removes one and forgets to lower the ceiling just as it fails one that
adds one. The per-area map stays now that every entry is zero: it is the
mechanism to reach for if an area ever needs a ratchet again, and reaching for
it is a deliberate edit that this commit's test will make you make on purpose.
…ounted
Two findings, and the second explains why the first survived this long.
## The gate was blind to the UI workspace
`walkTypeScript` filtered on `entry.endsWith(".ts")`. Every route and every
component in apps/ui is `.tsx`. So the area carrying more double assertions
than the other five combined was the one area this gate never counted -- and
it reported OK the whole time, which is the failure mode a gate is supposed to
be immune to.
File discovery is `git ls-files` now, which fixes it in the way that stays
fixed. It carries `.tsx` because it does not filter by extension guesswork,
and it excludes build output (apps/ui/.output, .vinxi, .tanstack, dist)
because those are untracked -- not because a hardcoded skip list happens to
name them today. Git already knows what is in the repo; a skip list is one
more thing that rots quietly.
`apps/ui` enters as a RATCHET at 101, its real authored count (162 more live
in tests and e2e harnesses, which are not scanned; no generated file carries
one). `areaOf` picks the longest matching prefix so a nested area files under
itself, with a test for the case where a shorter prefix would otherwise
absorb it. Mutation-verified in all three directions: a cast in a `.tsx` now
fails, an UNTRACKED file does not count, and a test file does not count.
## @polkadot/api: seven assertions, and none of them were checking
apps/ui/src/lib/metagraphed/chain-connection.ts asserted the whole
`ApiPromise` into a hand-written `SubtensorQueryApi` naming five storage
entries -- once per call site. subtensor has no api-augment package, so that
gap is real; restating the pallet surface in TypeScript and then trusting the
restatement is not how to cross it. A renamed or removed entry still compiled,
and `undefined()` is a TypeError raised inside a wallet flow at the point the
user has already reviewed an amount. The assertion also claimed `.toNumber()`
on every RESULT, so an entry whose on-chain type stopped being numeric
compiled just as happily.
`api.query` carries a real index signature, so every entry reads with no
assertion at all and comes back as `Codec` -- which is the truth. The codec is
then narrowed by predicates whose bodies perform the check they claim
(`"toNumber" in value && typeof value.toNumber === "function"`), so a mismatch
raises a named error instead of a bare TypeError. `getMinStake` and
`getFreeBalance` get the same treatment; the latter used to read
`account.data.free.toBigInt()` through an assertion that would have thrown two
properties deep.
Three tests pin it: a missing entry, a retyped codec, and an AccountInfo with
no free balance each fail by name.
apps/ui 108 -> 101. Its typecheck and all 1401 workspace tests pass; all 55
CI validators pass.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
metagraphed-ui | 412cda0 | Aug 16 2026, 03:21 AM |
The commit that moved this interop into scripts/lib/ajv-formats.ts repointed all eight call sites and left all eight imports behind. CI's lint caught it; my local runs did not, because I linted the files I had just edited rather than the tree. `npm run lint` over everything is the check that would have seen it, and it is cheap. All seven ajv-dependent validators still execute, not merely compile.
…ed bug
apps/ui 101 -> 93, in the clusters that do not depend on the router.
**The idle-callback pair, twice.** -providers-index-page.tsx and
-subnets-index-page.tsx each carried the same eight lines and two assertions:
`window as unknown as { requestIdleCallback?: (cb: () => void) => number }`.
The doubt is legitimate -- lib.dom declares the method as always present and
Safari only shipped it in 16.4 -- but an assertion is the wrong way to express
it, because it also restates the SIGNATURE. The day the callback gains its
`IdleDeadline` argument, that assertion keeps compiling and the call is wrong.
`Partial<Pick<Window, "requestIdleCallback" | "cancelIdleCallback">>` says
"maybe" without claiming anything: a real `Window` is assignable to it with no
cast, and the signatures stay lib.dom's rather than a copy. Both pages now
call one `src/lib/metagraphed/idle.ts`, whose tests cover both branches and
pin the pairing -- cancelling a `setTimeout` handle with `cancelIdleCallback`
throws nothing and simply never cancels, which is the failure mode of two
copies drifting apart.
**The endpoint drawer was asserting a field onto its own contract.**
`(endpoint as unknown as { pool_id?: string; pool?: string })`, twice in one
expression, on a type that already declares `pool?: string` and carries an
index signature. So `pool` needed no help at all, and `pool_id` -- which is
NOT on the contract -- was being declared into existence rather than read.
`String(endpoint.pool_id ?? endpoint.pool ?? "")` type-checks as written and
behaves identically.
**Two library seams, one hop instead of two.** apps/ui/scripts/
render-og-preview.ts had the same satori/satori-html gap as the CI renderer
and gets the same narrow `as ReactNode`. vite.config.ts wrote
`satisfies NitroPluginConfig as unknown as LovableViteTanstackOptions["nitro"]`
-- the `satisfies` is the real check and validates the object against nitro's
own type, `hooks` and all, but `as unknown as` then discarded that guarantee
on the way to the wrapper's option, which is declared as a three-key subset
that does not admit `hooks`. One hop keeps the check.
Not touched here: the ~40 remaining `as never` on TanStack Router search and
params updates. Those are not per-site mistakes -- `useSearch({ from: "/chain/
blocks" })` resolves to `{}`, so the route search types are inert across the
whole workspace and every caller is working around the same thing. That is one
investigation, not forty edits, and it belongs in its own change.
apps/ui: typecheck clean, 2321 tests pass. Root: lint, format, and all 55 CI
validators pass.
apps/ui 93 -> 83. Nine places wrote `x as unknown as Record<string, unknown>` to look up a key computed at runtime -- a sort column, a parameter name, a "try these four fields in order" fallback. The refusal they were working around is narrow: an INTERFACE has no implicit index signature (a `type` alias for the same shape does), which is a soundness concession about interfaces being open to declaration merging, not a statement that the read is dangerous. The workaround was much wider than the refusal. Aliasing the object as `Record<string, unknown>` erases its real type, so every OTHER read through that alias also stops being checked -- and several of these then read a KNOWN field through the alias, got `unknown` back, and cast again to fix it. schema-drift-detail.tsx and -gaps-page.tsx each had that second cast twice over, on array entries. `Reflect.get` is the language's own answer and needs no assertion: it takes `object` and returns `any`, which narrows to `unknown` on the way out. No cast, no copy, and the argument keeps its real type so every static read at the call site is still checked. src/lib/metagraphed/read-key.ts wraps it with `readString` and `readNumber`; `readNumber` requires a FINITE number, because these values come from JSON and a NaN reaching `.toFixed()` renders "NaN" into a table cell instead of the em-dash the absent case is meant to show. Two of the nine needed nothing at all. `Subnet` already declares an index signature, so -subnets-index-page.tsx was asserting its way past a restriction that did not apply -- and `network-parameters-panel.tsx` immediately called `Object.keys()` on the alias, which works on the real type just as well. apps/ui: typecheck clean, 2321 tests pass, lint clean (8 pre-existing react-refresh warnings in files this does not touch). Root: lint, format and the gate all pass.
JSONbored
added a commit
that referenced
this pull request
Aug 16, 2026
…`{}` (#11374)
#11368 left 83 double assertions in apps/ui and said ~64 of them were one
problem rather than 64. They were, and this is it.
## The chain
`@tanstack/zod-adapter@1.167.0` declares `peerDependencies: { zod: "^3.23.8" }`.
This repo is on zod 4.4.3. The adapter's `fallback()` returns
`z.ZodPipeline<z.ZodType<..., z.ZodTypeDef, ...>, z.ZodCatch<TSchema>>` — and
neither `ZodPipeline` nor `ZodTypeDef` exists in zod 4. So `fallback()`
returned `any`.
Measured, not reasoned. `z.infer` on a schema with no `fallback` is
`{ limit: number; q: string }`. Add ONE `fallback()`-wrapped field and the
whole object becomes `{ [x: string]: any }` — a single poisoned property
collapses the object type. Every route search schema used `fallback` on every
field, so every route's search type was `{}`, so `useSearch()` returned `{}`,
so ~64 call sites wrote `as never` to get anything done.
Somebody had already found the edge of this. -health-page.tsx carried a
comment saying `fallback().default()` "loses its literal-union output type
under this repo's zod v4 — a pre-existing gap shared by every other route's
search schema". It was diagnosed correctly and worked around locally, one cast
at a time, because the shared cause was one dependency away.
## The fix
`fallback(A, B)` is `z.custom().pipe(A.catch(B))` — read from the adapter's
published dist, not guessed. The `z.custom()` prefix is a runtime no-op that
exists only to widen the pipeline's input type, which is the thing that broke.
So `A.catch(B)` is the same parse, and zod 4 implements Standard Schema, which
TanStack Router 1.170 accepts directly. 111 call sites converted by AST
codemod (their arguments contain commas and parens and are formatter-wrapped;
a regex would have eaten them), `zodValidator()` unwrapped at 17, and the
dependency is gone.
The equivalence is pinned, not asserted: search-schema-catch-equivalence.
test.ts reimplements the adapter's real `fallback` and compares both parses
across eight input classes — valid, below min, above max, wrong type, null,
missing, NaN, numeric string — for number, string and enum fields, with
`.default()` on top.
## What working types then caught
**The compare drawer's URL param has never worked.** `SubnetCompareDrawer`
writes `?compare=`, and its doc comment says the comparison is "shareable and
survives page reloads". `/subnets/$netuid`'s `validateSearch` REPLACES the
search object rather than patching it, and `compare` was not among the five
keys it returns. Proven at runtime:
`validateSearch({ compare: 5, tab: "overview" })` returns `{"tab":"overview"}`.
The param was discarded on the very next parse, every time. The drawer's own
read path defends against `compare` being a number OR a numeric string, which
is what not being able to tell looks like.
**A sortable column that silently did nothing.** `SortHeader` took
`field: string` and `onSort: (field: string) => void`, so a typo compiled — and
then the route's search schema `.catch()`ed the unknown sort back to its
default, so the column just never sorted and nothing said why. It is generic
over the table's own field union now, and `-revenue-page.tsx` binds it to
`CoverageSortField`.
**Nine copies of one test helper, each asserting past its own signature.** The
`queries.*.test.ts` files each declared their `runQuery` helper's context
parameter as `never` and then asserted the argument into it. Consolidated into
`run-query.ts` typed with `QueryFunctionContext<TKey>` — which immediately
showed the fixture was missing `client`, required since v5, so a `queryFn`
that read it worked in the app and got `undefined` in all nine tests.
Also: `alphaUsdCoverage` and `NUMERIC_FIELDS` now take the untrusted input
their bodies were already written for (their tests fed them `42`, `"x"`, `[]`
and `"featured"` through assertions, so the real contract lived in the test);
`window.__mgPaletteAnalytics` is a module augmentation rather than an assertion,
so it is discoverable; the chain-alpha volume distribution and the MCP surface
call result are parsed rather than claimed — the latter is a third-party
subnet's answer, and a missing `status_code` painted the response amber as a
failure while a missing `latency_ms` rendered "undefined ms" beside it.
apps/ui 83 -> 0.
src 0/0 workers 0/0 schemas-src 0/0 packages 0/0 scripts 0/0 apps/ui 0/0
Every area the gate scans is now at zero. apps/ui typecheck clean, 2324 tests
pass; root lint, format and all 55 CI validators pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#11361 drove
as unknown asto zero insrc/andworkers/and gated itthere. This starts on the code the gate does not cover.
Measured with the same AST scanner the gate uses, so comments and string
literals don't inflate it:
src/workersscriptspackages/clientschemas-srcapps/uitests/The published client (2)
Both paginators widened the caller's typed query into a
Record<string, unknown>to writecursoronto it, then asserted itback with
as unknown as QueryParams<Path>at every request — two castsper page around a value whose type never changed, and between them the
query went unchecked for the whole loop.
The cursor was never the caller's field.
metagraphedFetchalreadytreats
queryas an untyped bag when it builds the URL, so it takescursoras its own option now and appends it after. Nothing isconstructed, so nothing needs asserting.
That also removed the reason
next_cursorwas read throughpage as { meta?: { pagination?: { next_cursor?: unknown } } }andcarried back into the next request as
unknown.nextCursorOfnarrowsit, and treats a non-scalar token as no token — the API's cursors are
opaque strings, and stringifying an object would page forever on
"[object Object]".I tried two smaller fixes first; the client's own tsconfig — which the
root config excludes — rejected both.
QueryParams<Path>resolves toneverfor a route with no query, so the generic can't be spread, and{}isn't assignable to it either. The construction had to go.The validator scripts (24)
Five scripts each invented their own way to hand a Worker handler an
env:
createLocalArtifactEnv() as unknown as Envand its multi-linesibling. The suites had already solved this —
mockEnvwraps the onehonest assertion (a stub standing in for platform bindings a unit
process cannot construct) behind a named helper. That helper moved to
scripts/lib/so both sides share it;tests/helpers/worker-env.tsre-exports it.
I tried the more obviously correct fix first and backed it out.
Making
createLocalArtifactEnvreturn anApiWorkerEnvis bettertyping, but the suites build an env and then mutate it
(
env.DATA_API = { fetch }), and a realFetcheralso declaresconnect— so it pushed platform-type strictness into ~30 fixtures toremove 24 casts from scripts. Wrapping at the call sites gets the same
result with no test churn.
That attempt was not wasted: it surfaced four more fixtures still
setting the retired
METAGRAPH_HEALTH_DB, and a tripwire — "rejectsunsupported query parameters before the store" — spying on that dead
binding, so it could never fire. Both worth fixing on their own.
The non-empty tuples (2)
z.enumwantsreadonly [string, ...string[]].sectionsOfalreadythrew on an empty list and then asserted the proof into the type;
destructuring the head narrows it with none.
VALIDATOR_ECONOMICS_SORTSwas
as constand cast to the mutable tuple form thatsortSchemanever asked for.
Verification
tscclean;packages/clienttscclean (checked separately —the root config excludes it); eslint clean (uncached); prettier clean
validate:apigreen — that script drives the real handlers throughthe env change
validate:double-assertions,types,boundary-casts,unreferenced-exports,contract-driftgreenStill to do
scripts30,apps/ui108. Theapps/uiwork crosses into a separateworkspace whose typecheck needs built workspaces and whose route and
component files carry their own PR rules, so it wants its own change.
The ~6,000 in
tests/are a different question, not a smaller one: aunit process cannot construct a
KVNamespace,Hyperdrive,ExecutionContextorR2Bucket, so there the cast is the mechanismrather than a mistake. The useful move is the one this PR makes —
centralise it in a named, key-checked helper — not to delete six
thousand of them.