feat(lint): 禁止把引擎查询选项擦成 any,存量入计数 baseline 只减不增 (#4918) - #5600
Merged
Conversation
… shrink-only baseline (#4918) `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. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GX3sL71LFq8m2usg6VqTSE
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
Author
Contributor
Author
CI 现状:
|
This was referenced Aug 5, 2026
Closed
os-zhuang
marked this pull request as ready for review
August 5, 2026 20:25
os-zhuang
enabled auto-merge
August 5, 2026 20:27
os-zhuang
pushed a commit
that referenced
this pull request
Aug 5, 2026
… the migrated tests (#4645) The 15 turso test files arrived from cloud, which predates this repo's `query-options/no-any-erasure` rule (#5600, landed on main after the first merge), and added 52 sites — test surface 267 -> 319. Every one is inside `packages/drivers/driver-turso/src/*.test.ts`; nothing outside turso moved. Both dispositions the rule names, chosen per site, with no assertion touched: - 14 sites TYPED. Ordinary on-contract reads whose only defect was the missing required `QueryAST.object` key (`driver.count('users', { where: { age: 17 } })`, the TEMPORAL_CASES/TEMPORAL_TIME_CASES conformance reads, and the `$not`/`$or` spellings the transport is asserted to ANSWER). Declaring `object` is what driver-memory's conformance suite already does, and it puts `where` back under tsc. - 38 sites `as unknown as QueryAST`. Deliberately off-contract input: values flowing through the suites' `unknown`-typed helpers (`compile`, `refusalOf`, `ids`) and inline literals whose whole point is refusal — `where: 42`, `[]`, `''`, a class instance, bare AST arrays, `{ $or: [null] }`, `{ $not: null }`, non-boolean `$null` comparands. These assert the transport REJECTS them, so the type must be bypassed by name rather than erased. Baseline untouched: 267 is met, not raised. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx
os-zhuang
pushed a commit
that referenced
this pull request
Aug 6, 2026
`check:query-options-erasure`(PR #5600 / #4918,在本分支切出之后落地) 把测试面计入只增不减的棘轮。本分支新增的正控里 `engine.find('task', {} as any)` 是其中一处擦除,使测试面 267 → 268 而红。 该调用点是**合约内**形状——`find(object, query?: EngineQueryOptions)` 的空查询——不是「断言引擎拒绝未知选项」那类刻意越契约的输入,所以按 门禁处方的第 1 条直接给它正确类型(这里等于去掉断言,签名本就能推 断),而不是写 `as unknown as EngineQueryOptions`。 基线文件未动:抬高天花板是「reviewed edit, not a remedy」,这里修的是 站点本身。 验证(合并 origin/main 后): - node scripts/check-query-options-erasure-ratchet.mjs → 267,at the ceiling,no files added - @objectstack/objectql test → 1934 passed (120 files) - @objectstack/spec + @objectstack/objectql typecheck → Done Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 6, 2026
…vers/ (objectstack-ai#4645 Phase A) (objectstack-ai#5618) * feat(drivers): migrate driver-turso back in-repo and collect all IDataDriver packages under packages/drivers/ (objectstack-ai#4645) Phase A of the maintainer-ruled two-repo migration (objectstack half). - git mv packages/plugins/driver-{memory,mongodb,sql,sqlite-wasm} -> packages/drivers/* (content unchanged; only repository.directory follows the move) - new packages/drivers/driver-turso, migrated from objectstack-ai/cloud @ bc372e3b, published Apache-2.0 / access: public. multi-tenant routing and vector-poc stay in cloud. - pnpm-workspace glob, check-driver-conformance DRIVERS_DIR, both CI workflows, liveness evidence paths, adr-anchors, docs and source comments follow the move - driver-turso enters the conformance matrix with 3 measured DEBT cells (objectstack-ai#5590) - CLI turso refusal message no longer claims the package ships elsewhere (objectstack-ai#5602) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx * fix(service-analytics): point the incoming applyLike cross-reference at packages/drivers/driver-sql (objectstack-ai#4645) `like-pattern.ts` arrived with origin/main (objectstack-ai#5567) naming `packages/plugins/driver-sql/src/sql-driver.ts` — the pre-objectstack-ai#4645 path. The merge was textually clean because the file is new on main and the move is on this branch, which is the jointly-wrong shape AGENTS.md §10 warns about: nothing conflicts, and the cross-reference the header leans on lands nowhere. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx * fix(driver-turso): satisfy the objectstack-ai#4918 query-options-erasure ratchet on the migrated tests (objectstack-ai#4645) The 15 turso test files arrived from cloud, which predates this repo's `query-options/no-any-erasure` rule (objectstack-ai#5600, landed on main after the first merge), and added 52 sites — test surface 267 -> 319. Every one is inside `packages/drivers/driver-turso/src/*.test.ts`; nothing outside turso moved. Both dispositions the rule names, chosen per site, with no assertion touched: - 14 sites TYPED. Ordinary on-contract reads whose only defect was the missing required `QueryAST.object` key (`driver.count('users', { where: { age: 17 } })`, the TEMPORAL_CASES/TEMPORAL_TIME_CASES conformance reads, and the `$not`/`$or` spellings the transport is asserted to ANSWER). Declaring `object` is what driver-memory's conformance suite already does, and it puts `where` back under tsc. - 38 sites `as unknown as QueryAST`. Deliberately off-contract input: values flowing through the suites' `unknown`-typed helpers (`compile`, `refusalOf`, `ids`) and inline literals whose whole point is refusal — `where: 42`, `[]`, `''`, a class instance, bare AST arrays, `{ $or: [null] }`, `{ $not: null }`, non-boolean `$null` comparands. These assert the transport REJECTS them, so the type must be bypassed by name rather than erased. Baseline untouched: 267 is met, not raised. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 6, 2026
…bjectstack-ai#5668) * docs(spec): HookContext.input 契约表改成引擎真正构造的形状 (objectstack-ai#5273) The `HookContext.input` table named three keys no producer sets: `ast` on bulk update AND bulk delete, and `doc` on insert. `input` is `z.record(z.string(), z.unknown())` — an open shape — so Zod validated none of it and the prose was the only contract an author could read. - Bulk writes carry no `ast`. The row-scoping predicate lives on the engine-internal `OperationContext.ast` (objectstack-ai#2982) so middleware-composed filters bind the driver call where no handler can widen them. Deleted the "the row-scoping predicate is carried in `input.ast`" sentence. - `input.id` on a bulk before-event is present but `undefined` (the engine builds `{ id, … }` with shorthand), not absent — documented as such, since `'id' in input` answers true. - Documented the post-objectstack-ai#5038 per-row after-event shape: `after*` on a bulk write dispatches once per matched row on a single-record-shaped context, so `input.id` IS bound there. - insert builds `{ data }`, not `{ doc }`. Kept: before-events still fire once per batch, and there is no `*Many` event. No engine change. The `ast` special-case in `hook-wrappers.ts` deliberately stays — it is live on the READ path, where `input.ast` is real and a handler may rewrite it. Pinned in `packages/objectql/src/hook-input-shape-contract.test.ts`: spec cannot execute a dispatch (objectql depends on spec, so a spec-side test would invert the dependency), so the facts are asserted next to the engine that produces them. `beforeFind` is the positive control (objectstack-ai#4865) — it really does carry `ast`, so "no ast on writes" is a measurement rather than a vacuous pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D * test(objectql): 正控的 find 选项不再擦成 `any`,过 objectstack-ai#4918 query-options 棘轮 `check:query-options-erasure`(PR objectstack-ai#5600 / objectstack-ai#4918,在本分支切出之后落地) 把测试面计入只增不减的棘轮。本分支新增的正控里 `engine.find('task', {} as any)` 是其中一处擦除,使测试面 267 → 268 而红。 该调用点是**合约内**形状——`find(object, query?: EngineQueryOptions)` 的空查询——不是「断言引擎拒绝未知选项」那类刻意越契约的输入,所以按 门禁处方的第 1 条直接给它正确类型(这里等于去掉断言,签名本就能推 断),而不是写 `as unknown as EngineQueryOptions`。 基线文件未动:抬高天花板是「reviewed edit, not a remedy」,这里修的是 站点本身。 验证(合并 origin/main 后): - node scripts/check-query-options-erasure-ratchet.mjs → 267,at the ceiling,no files added - @objectstack/objectql test → 1934 passed (120 files) - @objectstack/spec + @objectstack/objectql typecheck → Done Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D --------- Co-authored-by: Claude <noreply@anthropic.com>
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.
Fixes #4918
一、前提重测(派发口径的硬要求)
正文的测量基于
89d2a4e(08-03)。本分支基于e900015cd(08-05,后并入e0b2ea72d)重测,逐形状结果:find/findOne/count/aggregate的选项参数(第 1、2 位)被any断言擦掉orderBy: … as any(比参数低一层,A 的选择器看不到)const opts: any = { … }…find(o, opts)(作用域解析,非命名启发)比正文的 25+11 多,不是少 —— 前提成立,而且更强。这不是两天里长出来的:正文的 A 是按
} as any)这类文本形状数的,AST 口径把同一个类的其它拼法(标识符参数、第 2 位的hookContext.input.options as any、aggregate/count)一起算进来了。84 处的源行逐条人工核对过,零误报 —— 全部是真的 engine / driver 查询调用。存量清单进scripts/query-options-erasure-baseline.json。二、形态选择:eslint 规则 + 计数 baseline(不是
scripts/独立走查)判据是「哪种已有同形先例且能双向证红」。同形先例就在隔壁:
#4251的 slot-lookup 是同一个类(把已声明的契约擦成any),它的形态是 eslint 规则 +ignores按文件豁免 + 计数 shrink-only baseline +check-slot-lookup-ratchet.mjs复测。本单照抄这套机制,理由:packages/**TS 源码上的词法 / 作用域形状,ESLint 已经在 CI 解析这片源码(pnpm lint),独立走查要再解析一遍;pnpm lint变红,这是本地最快的信号。独立走查给不了这个。⛔ 没有碰
packages/lint(那是 metadata graph 的 linter,不是仓库自身 TS 源码的 linter),所以也没有碰互斥声明里的packages/lint/src/validate-react-page-props.ts。三、规则
新规则
query-options/no-any-erasure(eslint.config.mjs),覆盖三个形状:any断言。第 0 位在这四个签名上一律是对象名,所以从 1 起算 —— 顺带也把Array.prototype.find(cb)(同名方法、回调在第 0 位)整个排除在外,不靠白名单。orderBy:...(ast.orderBy ? { orderBy: ast.orderBy as any } : {})坐在一个本来有类型的选项字面量里面,参数位检查看不见它。orderBy按名入围,因为它就是 fix(metadata-protocol): two internalengine.findcalls sort bydirection, a key no driver reads — audit history and global search silently return ASCENDING #4674 拼错的那个键。const opts: any = { … }之后find(o, opts)—— fix(metadata-protocol): two internalengine.findcalls sort bydirection, a key no driver reads — audit history and global search silently return ASCENDING #4674 的 global search 站点用的正是这个形状。走作用域解析而不是标识符名字:按名字匹配会误伤仓里每一个const options: any(不管它有没有流向查询),又漏掉别的拼法。作用域分析不需要类型信息,所以仍然留在无类型 lint pass 里。断言链是整条走的,
{ … } as any as EngineQueryOptions也算 —— 那个拼法先把字面量对着any检查(等于不检查),再贴回契约的名字,擦除程度和裸as any一样,读起来却像有类型。as unknown as EngineQueryOptions故意不算。它点名了自己在绕过哪个契约、保留了调用其余部分的检查、而且 grep 得出来是个有意行为 —— 裸as any三条都不满足。所以它是「这个值就是故意越契约的」(例如一条断言引擎会拒绝未知选项的测试)的规范拼法。为什么是独立 plugin 规则而不是再加三条
no-restricted-syntax选择器,两个理由都是硬的:packages/**上再设一次no-restricted-syntax,会把 slot-lookup 那块的选择器列表在两块都命中的文件上整体替换掉,等于静默删掉那条规则。两个门禁还需要各自独立的ignores(未清扫集合是不同的文件),一个共享块给不了。slot-lookup/no-any-assignment存在的原因。已知残余(写出来而不是暗示):经由有类型的间接层发生的擦除 —— 形如
(o, q?: any) => engine.find(o, q)的转发 helper,或者自身返回类型被标成 any 的 wrapper —— 擦除效果一样,本规则看不到。: any形参转发进查询是个真实形状(约 50 处,几乎全是测试 double),判它需要调用图而不是单文件作用域,和 slot-lookup 自己的 KNOWN RESIDUAL 是同一条边界,同一个答案:属于有类型的 lint pass。四、存量:baseline + 只减不增,不清扫
scripts/check-query-options-erasure-ratchet.mjs(pnpm check:query-options-erasure,在 lint.yml 里紧挨 slot-lookup ratchet)是让 baseline 有意义的那一半。ignores一条会让整个文件闭嘴,所以 baseline 带逐文件计数,由脚本把豁免解除后复测 —— 列表里的文件新长一处擦除,骑不了旧条目。计数涨、跌、文件清干净没删条目、以及往 baseline 里加文件(对比与 main 的 merge base),四种都判红。engine-unknown-option.test.ts、engine-wire-alias-reject.test.ts、sqlite-wasm-out-of-contract-filter-input.test.ts这类以「越契约输入」为被测对象的测试,必须擦掉类型才能构造出tsc本来会拒绝的输入。逐文件 ratchet 会在一条正当的新拒绝测试上判红,而且没有诚实的补救动作;聚合数字有一个:把故意的那处写成as unknown as EngineQueryOptions,它点名契约、也不进这个计数。这是我在字面口径上唯一的取舍,明写在 baseline 的$comment和脚本注释里,请 PM 复核。⛔ 本 PR 不清扫那 84 处,按派发口径。其中一部分是真的跨类型边界(objectql 的
hookContext.input.options、metadata loader 的 query bag),需要的是把边界类型补上,不是删断言 —— 属后续批次。五、双向证红
方向在跑之前就定了,四组:
pnpm lint红 + ratchet 红__injected_probe.ts: NEW engine query-options erasure (1 site(s))protocol.ts)追加一处pnpm lint绿(ignores让整个文件闭嘴 —— 这正是「看不见的那一步」)+ ratchet 红erasure count grew 6 → 7pnpm lint绿(首刀不覆盖)+ ratchet 红test surface grew 267 → 268 site(s)pnpm lint绿;ratchet:84 unswept non-test site(s) in 19 file(s), none new第二行是这套机制存在的全部理由:
ignores藏得住的那一步,只有计数抓得到。--self-test把这些做成了可重跑的形式:10 个应报形状、10 个应静默的对照(含Array.prototype.find、结果位断言、第 0 位断言、被遮蔽的内层有类型绑定、as unknown as逃逸口)、豁免通道与测试 glob 通道的双向证明、以及 8 个 ratchet 比较用例(相等 / 新文件 / 涨 / 跌 / 文件清空 / baseline 加键 / 测试面涨 / 测试面跌)。ratchet 比较逻辑抽成了纯函数diffRatchet,因为那正是「在干净树上跑绿」永远碰不到的那一半。另外把 #4674 的成因原地三态复现了一遍,证明这条规则守的是真东西,而不是一个自造的形状。取
protocol.ts的 audit-history 查询(#4720 修的那处),pnpm exec tsc --noEmit -p packages/metadata-protocol/tsconfig.json:order: 'desc'direction: 'desc'error TS2353 … 'direction' does not exist in type… @ 3520,51} as any)direction: 'desc'count grew 6 → 7即:擦除就是把编译错误变成静默的那一步,而这条门禁现在抓的就是擦除。注:
@objectstack/metadata-protocol目前还在check:type-check-coverage的 DEBT 台账里、没有typecheck脚本,所以那条tsc通道今天是本地 / 编辑器级的,不是 CI 门禁 —— 这让本门禁更必要,不是更不必要。baselineKeysAddedSinceMergeBase(merge-base 单调性)这一段依赖 git,本 PR 里 baseline 在 base 上还不存在,所以它按设计报告自己没能跑(NOT verified: …)而不是静默通过;它的判定逻辑由diffRatchet的addedBaselineKeys用例覆盖。下一个 PR 起这条会真跑。六、其它
engine.findcalls sort bydirection, a key no driver reads — audit history and global search silently return ASCENDING #4674 三态复现全部还原,git status干净;pnpm --filter '@objectstack/metadata-protocol^...' build顺带重写的packages/spec/authorable-surface.base.json已 checkout 还原,不随本 PR 走。node scripts/check-nul-bytes.mjs通过,并对改动文件做了超出门禁扫描面的自扫(grep -naP全部 C0)—— 零命中。pnpm check:type-check-coverage、pnpm check:published-files绿;.github/workflows/lint.ymlYAML 可解析,新步骤落在 slot-lookup ratchet 之后。origin/main(e0b2ea72d)后重测,84 / 267 两个数字未变,pnpm lint仍绿。eslint.config.mjs/scripts// 根package.json一条脚本 / 工作流,不发布任何包,按 pr-automation 的处方走首选路线 2 ——skip-changeset标签(非空 changeset 无包可署名;空 frontmatter changeset 是 空 changeset 会静默卡死已 version 的发布:Release run 全绿,但 npm 和 Docker 什么都没发(17.0.0-rc.2 现在就卡着) #4898 的静默卡住路径,按 Check Changeset 的失败文案把「空 changeset」推荐为出路 —— 而那正是 #4898 静默卡死发布的输入 #5292 后的处方属最后手段)。package.json的脚本项基于当前 main(check:prerelease-pins之后)加入。Generated by Claude Code