You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(lint): ban erasing engine query options to any, with a counted shrink-only baseline (#4918) (#5600)
`IDataEngine.find/findOne/count/aggregate` declare their options as
`EngineQueryOptions` / `EngineCountOptions` / `EngineAggregateOptions`, and
`IDataDriver` declares the same slots as `QueryAST` + `DriverOptions`. For an
INTERNAL caller `tsc` is the only channel enforcing those keys: the protocol's
ingress normalizer never runs on a direct engine call, and the options schemas
are not `.strict()`, so an unknown key is silently DROPPED rather than
rejected. One `as any` on the options argument switches that off for the call
site while looking identical to code that has it.
That is #4674: two internal queries spelled their sort
`{ field, direction: 'desc' }` where the QueryAST shape is `SortNodeSchema` =
`{ field, order }`. Both drivers normalize off `.order` with no fallback, so
both ran ASCENDING, and because both carried a `limit` the wrong direction
changed WHICH ROWS came back — audit history returned the oldest events and
global search the stalest matches. #4720 restored the two sites, #4721 closed
the external (REST/RPC) callers with a strict schema plus an ingress
normalizer; this is the third leg, and it stops the shape regrowing internally.
New rule `query-options/no-any-erasure` (eslint.config.mjs), three shapes:
- an `any` assertion at argument 1 or 2 of a query method — argument 0 is the
object NAME on every one of these signatures, which is also what keeps
`Array.prototype.find(cb)` out of the rule entirely;
- `orderBy: … as any`, which sits one level below the argument and so is
invisible to the argument-position check;
- the split form (`const opts: any = { … }` … `find(o, opts)`) — the shape
#4674's global-search site actually used. Scope analysis, not a name
heuristic; needs no type information, so it stays in the untyped lint pass.
The assertion chain is walked, so `{ … } as any as EngineQueryOptions` — which
checks the literal against nothing and then re-labels it with the contract —
is caught too. `as unknown as EngineQueryOptions` is deliberately NOT matched:
it names the contract being bypassed, keeps the rest of the call checked, and
greps as an intentional act, so it is the sanctioned spelling for input that is
deliberately off-contract (a test asserting the engine REJECTS an option).
It is a dedicated plugin rule rather than three more `no-restricted-syntax`
selectors because flat config does not MERGE rule options: a second block
setting `no-restricted-syntax` over `packages/**` would REPLACE the
slot-lookup block's selector list for every file both match, silently deleting
that rule. The two guards also need independent `ignores`.
Residual re-measured on the branch point (the issue's numbers were taken at
89d2a4e, two days and ~40 merges earlier): 84 non-test sites in 19 files and
267 in test code, none of them a false positive. Both go into
`scripts/query-options-erasure-baseline.json` and neither is swept here — part
of the residual is a real type boundary (objectql's `hookContext.input.options`,
the metadata loader's query bag) that needs the boundary type written, not the
assertion deleted.
`scripts/check-query-options-erasure-ratchet.mjs` (`pnpm
check:query-options-erasure`, wired into lint.yml next to the slot-lookup
ratchet) is what makes the baseline mean something. Non-test files are
grandfathered by path, and an `ignores` entry silences the WHOLE file, so the
baseline carries per-file counts measured with the grandfathering lifted — a
new erasure in a listed file cannot ride the old entry. Test code is held by
one aggregate decrease-only number instead of per-file counts, because an
unknown share of those sites are legitimate and a per-file ratchet would go red
on a new rejection test with no honest remedy.
Claude-Session: https://claude.ai/code/session_01GX3sL71LFq8m2usg6VqTSE
Co-authored-by: Claude <noreply@anthropic.com>
0 commit comments