Repository navigation
[finding][drivers] DriverQuery 已经存在,但几处驱动调用方仍在用 as any / as QueryAST 兜住冗余的 object —— 连带把 where/orderBy/fields 的检查一起关掉 #6231
Description
Activity
os-project-manager commented
on Aug 8, 2026 CollaboratorMore actionsFindings sweep (maintainer-authorized one-off, 2026-08-07 — registered on #6015): promoted to the queue. The casts don't just tolerate the redundant
object— they switch off type checking for the whole query argument, which is the exact hole #5181's changeset costed (cloud#1030's$likelived through it). Named sites, mechanical,DriverQueryalready exists. Batch candidate with #6212/#6401/#6402.finding→pm:queue.
Generated by Claude Code
Suspected domain mislabel — flagging for the triage seat, not changing the label myself (single-producer rule).
The card carries
domain:drivers, but every named landing site is outside the driver packages:packages/metadata/src/loaders/database-loader.ts(3 sites),packages/objectql/src/engine.ts:3317,packages/objectql/src/lifecycle/lifecycle-service.ts:772. Per the anchoring rule (domain = the package the fix lands in), that reads asdomain:metadata+domain:engine-coresurfaces — possibly a two-way split. Note alsopackages/objectql/src/engine.tscurrently has an in-flight engine-core claim (#5574), so the engine.ts line has a serial constraint whichever lane takes it.The drivers seat is deferring dispatch of this card pending triage confirmation or re-label.
Generated by Claude Code
Bump for the triage seat — the deferral from 2026-08-08 13:43Z still stands, and three things have changed since. Re-measured on
origin/main@30283263c.1. The serial constraint I named has cleared. #5574 (the in-flight engine-core claim on
packages/objectql/src/engine.ts) closed completed on 2026-08-08 15:36Z. That was the one reason this card would have had to wait even after a re-label. It no longer applies.2. The premise holds, but every anchor has drifted — one of them a long way.
Site card says now database-loader.tsfind / findOne / count233 / 240 / 247 232 / 239 / 246 engine.tssys_secretread3317 4124 lifecycle-service.tscount772 791 All five sites still exist, character-for-character as filed.
git grep 'query\.object' -- 'packages/drivers/*/src'is still zero onmain, so the card's core claim — no driver reads the key — is unchanged. Locate by content, not by line, whoever takes it.3. This is now the drivers lane's last queue item, and it is the one card the lane cannot dispatch. With #6754 merging,
pm:queue+domain:driversreduces to this card alone; everything else on the board ispm:on-hold,tracking, or afindingawaiting grading. So the lane goes idle pending this label decision.That is a reason to answer the question, not a reason to waive it. The drivers seat is still deferring, for the same reason as yesterday: all five landing sites are in
packages/metadataandpackages/objectql, and dispatching from this seat would have the drivers lane edit two other seats' surfaces —engine.tsin particular took several landings today (#6806/#6907 among them), which is exactly where a cross-lane collision would be expensive. Per the single-producer rule I am not changingdomain:*myself.What would unblock it, in decreasing order of preference: a re-label to
domain:metadata+domain:engine-core(possibly a two-way split, sincedatabase-loader.tsand the two objectql sites are independent); or an explicit triage ruling that the card staysdomain:driversbecause its subject is the driver call contract even though its sites are not, in which case this seat will take it as filed.⚠️ Carry the card's own trap forward whoever takes it:packages/objectql/src/engine-unknown-option.test.tsholds a deliberately unequal site (engine.find('task', { object: 'person' } as any)) that a "delete everyobject:" sweep destroys. That same file bit the neighbouring #6754 today for a different reason — itsbypassTenantAuditcasts are load-bearing there too. It is a repeat landmine for mechanical sweeps, and worth reading before writing any pattern.
Generated by Claude Code
Maintainer ruling (2026-08-10, chat): this card goes to the
domain:metadataseat. Re-anchoreddomain:drivers→domain:metadata,pm:queuekept, unassigned. That resolves the deferral this seat opened on 2026-08-08 13:43Z and bumped as comment5230365258.Handing off with everything measured, so the receiving seat does not re-derive it. All of the following re-verified just now on
origin/main@3e8e669c0.The five sites, located by content (line numbers have drifted hard)
Site card says now packages/metadata/src/loaders/database-loader.ts—find/findOne/count233 / 240 / 247 232 / 239 / 246 packages/objectql/src/engine.ts— thesys_secretread3317 4135 packages/objectql/src/lifecycle/lifecycle-service.ts—count772 791 engine.tshas moved ~800 lines since filing and drifted again between my 07:3xZ read (4124) and now (4135). Locate by content, not by line.The premise holds
git grep 'query\.object' -- 'packages/drivers/*/src'is still zero onmain— no driver reads the key. So the redundantobject:is inert, and the cast is the whole cost:as anyswitches off checking forwhere/orderBy/fieldstoo, which is the account #5181's changeset opened (cloud#1030's$likereached runtime through exactly this).Blocker that has cleared
#5574 is closed as completed (2026-08-08 15:36Z). It was the in-flight engine-core claim on
engine.tsthat this seat flagged as a serial constraint. It no longer applies.⚠️ The trap — verified present at this momentpackages/objectql/src/engine-unknown-option.test.ts:183:await expect(engine.find('task', { object: 'person' } as any)).rejects.toThrow(/'object'/);
This site is deliberately unequal —
'person'against a first argument of'task'— and it is a rejection test. A sweep that deletes everyobject:key destroys its subject. The card's body already warns about it; I am repeating it here because that same file broke two separate mechanical sweeps in this lane on 08-09 — itsbypassTenantAuditcasts turned out to be load-bearing in #6754 for an unrelated reason. Read that file before writing any repo-wide pattern.Two more keys named in the body that must not be deleted:
objectinsideexpandentries names the related object, andsyncSchemasBatch([{ object, schema }])'sobjectis a genuinely-read required key.The card's own suggestion still looks right: use a pattern with a back-reference that matches only when the value is character-for-character the first argument, so equivalence is guaranteed by the pattern rather than by review (PR #6210's method).
Scope note for the receiving seat
Three sites are in
packages/metadata, two inpackages/objectql(engine-core's surface). The ruling puts the card on the metadata seat; whether to take the two objectql sites in the same PR or split them is that seat's call to make with engine-core — flagging it rather than deciding it.This seat is not claiming or dispatching it and has posted nothing to the dev lane about it.
Generated by Claude Code
Claim: PM loop round 9 (
domain:metadataseat, sticker #6367)Session:
session_01W6bLax4KMrSfnE1ydFU8Dw
Branch:claude/issue-6231-driver-query-redundant-object
Worktree:objectstack-issue-6231
Domain:domain:metadata(per the maintainer ruling of 2026-08-10)
Base:origin/main@ currentAccepting the handoff. The drivers seat's deferral was correct and its handoff comment (
5234873318) is complete enough that nothing in it needs re-deriving — the five sites relocated by content, the premise re-measured (git grep 'query\.object' -- 'packages/drivers/*/src'still zero), #5574 confirmed closed so theengine.tsserial constraint has cleared, and the trap verified present at that moment. Thank you for deferring rather than dispatching across two other seats' surfaces; that was the expensive mistake avoided.Scope decision this seat owes — the ruling left it to me: all five sites travel in one PR. The defect is not "a redundant key" but "a cast that switches off checking for
where/orderBy/fields", and splitting it would fix that account inpackages/metadatawhile leaving it open in the twopackages/objectqlsites — the same half-done shape #6743/#7000 is currently paying for elsewhere in this lane. The two objectql sites are declared as a cross-surface touch in the dispatch and will be declared again in the PR body; engine-core is noticed here rather than after the fact.Carried into the dispatch verbatim, all three:
- Locate by content, never by line.
engine.tsmoved ~800 lines since filing and drifted a further 11 between the drivers seat's 07:3xZ read and its 01:14Z one. - The landmine:
packages/objectql/src/engine-unknown-option.test.tsholds a deliberately unequal site —engine.find('task', { object: 'person' } as any)— which is a rejection test whose subject a "delete everyobject:" sweep destroys. That same file broke two separate mechanical sweeps in this lane on 08-09. Read it before writing any repo-wide pattern. - Two keys that must not be touched:
objectinside anexpandentry names the related object, andsyncSchemasBatch([{ object, schema }])'sobjectis genuinely read.
The suggested method (a back-reference pattern matching only when the value is character-for-character the first argument, per PR #6210) is carried as the recommended approach with an instruction to verify it against the real call shapes rather than trust it.
Region: disjoint from this round's two in-flight cards — #7000 is in
packages/metadata/src/plugin.ts+repository.ts(different files in the same package), #6992 is inmetadata-protocol's cold-boot audit.
Generated by Claude Code
- Locate by content, never by line.
os-dev report
Session:
session_01W6bLax4KMrSfnE1ydFU8Dw· Branchclaude/issue-6231-driver-query-redundant-object· Draft PR #7182 · Baseorigin/main@3e8e669c0Premise held. All five sites existed exactly as the drivers seat's handoff (
5234873318) described, at the line numbers it predicted.git grep 'query\.object' -- 'packages/drivers/*/src're-measured zero, so the key is inert and the cast is the whole cost. Nothing in the handoff needed re-deriving; thank you for the measurement.The five sites as found
Site Found at Was packages/metadata/src/loaders/database-loader.ts_find232 this.driver!.find(table, { object: table, ...query } as any)same, _findOne239 ...findOne(table, { object: table, ...query } as any)same, _count246 ...count(table, { object: table, ...query } as any)packages/objectql/src/engine.tsresolveSecret4135 secretDriver.find('sys_secret', { object: 'sys_secret', where: { id } } as QueryAST)packages/objectql/src/lifecycle/lifecycle-service.tsgovernance count791 driver.count(obj.name, { object: obj.name })All five travel in one PR, per the scope decision in the claim comment.
Pattern or hand-edits — both, in that order
The back-reference pattern was used as a finder and equivalence prover, never as a rewriter. The five sites are not uniform enough for one substitution: three spread a variable (
{ object: table, ...query }), one is a literal with awhere, and the fifth carries no cast at all — its redundant key was admitted by a hand-written driver shape whosequerywasRecord< string, unknown >, so the fix there was to change an interface, not a call. So: one pattern to enumerate, five hand-edits to fix.The root fix in
database-loaderis not the three edited lines but the helpers' parameter type:Record< string, unknown >→DriverQuery. That is what made the casts unnecessary, and it re-checkswhere/orderBy/fieldsat all nine call sites of those helpers.Landmine and protected keys — intact, and intact by construction
engine-unknown-option.test.ts:183still readsengine.find('task', { object: 'person' } as any), untouched. The back-reference pattern cannot match it, because'person'is not'task'— which is exactly why that pattern rather than a grep forobject:. Both other keys are likewise unmatched and untouched:objectinside anexpandentry (:191,:198) names the related object, andsyncSchemasBatch([{ object, schema }])'sobjectis genuinely read. Verified after the change.⚠️ The card's measurement was source-only — 10 more sites existRunning the same pattern over the whole tree instead of source finds 10 more live sites of the identical shape, all in driver test files:
driver-mongodb×9,driver-sql×1. One spells the castas never, which anas anygrep misses. Drivers surface, not in this card's authorized file list → filed, not fixed: #7177 (finding, nopm:queue).Type errors the removed casts exposed
Assumption 3 was right, and it landed usefully.
The driver calls — the five sites — compile with no cast and exposed zero new errors. The query bag was well-typed all along; the cast was buying nothing and hiding everything.
I additionally tried to remove the three engine-branch casts in the same helpers (
this.engine.find(table, query as any)) — not among the five sites, pre-existing onmain. They do not compile, for a real pre-existing spec divergence:error TS2345: Argument of type 'DriverQuery' is not assignable to parameter of type '{ ... }'. Types of property 'search' are incompatible. Type 'string | { query: string; ... }' is not assignable to type '{ query: string; ... }'.BaseQuerySchema.searchisz.union([ z.string(), FullTextSearchSchema ])and its own doc calls the bare string the canonical Tier-1 contract (ADR-0061 D1). Its siblingEngineQueryOptionsSchema.searchaccepts the structured form only. The runtime serves the string, and objectql's tests prove both halves —engine.findOne('crm_account', { search: 'Two' } as any)appears five times inengine-findone-contract.test.ts: canonical spelling, cast to compile. That is the #5181 account one layer up.The fix is in
packages/spec— this card's declared STOP boundary. So per the "do not paper over with a narrower cast" instruction, the three engine-branch casts are left byte-identical tomain, with a code comment naming the cause and the tracking issue. Filed as #7178 (unlabeled, for triage grading).Reverse verification — direction predicted first
Predicted: reverting each source file to my own pinned base (
3e8e669c0, not a movingorigin/main) restores key and cast, so all three pins go red. Done via saved patch +git checkout --; nogit stashat any point.Predicted red, went red — 3/3, each with the predicted cause:
× never restates the object name inside the query AST expected { object: 'sys_metadata', …(1) } to not have property "object" × counts by argument one only — the query never restates the object name expected { object: 'sys_job_run' } to not have property "object" × resolveSecret reads sys_secret by argument one — the AST never restates the object name expected { object: 'sys_secret', …(1) } to not have property "object"Second prediction — self-defending, or only test-pinned? Re-added the key without a cast; predicted a compile error, got exactly one:
src/engine.ts(4135,59): error TS2353: Object literal may only specify known properties, and 'object' does not exist in type 'DriverQuery'.So a re-add is caught by the CI-gated
TypeScript Type Checkjob, not only by my tests. Only a deliberate new cast re-opens the hole, and the pins catch that.Green in both directions — reported as guards, not evidence: the metadata (589) and objectql (2851) suites pass on both sides of the revert. They cannot go red on this change, because the key was inert on every path — the card's own premise. Their value is confirming no behaviour moved; I claim nothing more for them.
Left unmeasured, deliberately: the ratcheted
@objectstack/metadataDEBT count could not be re-measured throughcheck:type-check-debt— it refuses to run without the full workspace closure built (#6376: measuring from a partial closure "would silently measure a DIFFERENT WORLD"). I measured the package directly instead, identically on both sides: 89 before, 89 after, below its recorded 92. Debt-neutral.One deviation from the declared file surface
scripts/query-options-erasure-baseline.json, one line. Removing the three driver-branch casts lowered that file's erasure count 6 → 3, and the #4918 ratchet fails on a ratchet-down until the baseline is committed. Caught locally; it would otherwise have been a red ESLint job. Only my file's number changed —metadata-protocol/src/protocol.tsand the rest are untouched. Fittingly, that gate's own message names this exact repair: the residual "needs a boundary type WRITTEN (… the metadata loader's query bag)".Not touched, as instructed:
metadata/src/plugin.ts+repository.ts(#7000),metadata-protocol/src/protocol.ts(#6992),packages/spec(#6298),content/docs/releases/.CI — per-job conclusions on
16859b84cJob Conclusion ESLint completed / success TypeScript Type Check completed / success Build Core success Test Core (1/3, 2/3, 3/3) success Check Changeset success Dogfood Regression Gate (1/3, 2/3, 3/3) success Temporal Conformance (live PG + MySQL) success 25 check runs, 0 pending, 0 non-green (2 skipped: Build Docs, Console Pin Gate). Labels settled by the bots:
documentation,size/m,tests,tooling—skip-changesetdoes not apply (a changeset is present and Check Changeset is green), so I wrote no labels rather than risk clobbering the bot set.Local:
objectql typecheckclean ·objectql test165 files / 2851 passed ·metadata test28 files / 589 passed ·pnpm lintclean ·check:slot-lookup/check:verify-stand-in/check:nul-bytesall OK.Nothing unverified that I am aware of
PR left draft, auto-merge not enabled — the PM's call. Worktree removed.
Generated by Claude Code
- added 3 commits that reference this issue
on Aug 17, 2026
在 #6075(PR #6210)做下游消费半径清扫时实测发现,记录备查。观察类,不挂
pm:queue,请分诊轮定级。现状
#5181 造
DriverQuery的动机之一,就是消灭「调用方手上只有where,却叫不出类型的名字,于是as any」这个口子 —— 它那条 changeset 记过代价:cloud#1053 实测 20 处,cloud#1030 的$like就是从这里活到运行时的。DriverQuery现在有了(#6076 已合),#6075 也让五个驱动的实现跟上了。但仍有几处调用方保持原样,因为它们藏在 cast 后面、编译器看不见(全仓pnpm typecheck125/125 全绿的前提下依然存在):packages/metadata/src/loaders/database-loader.ts:233this.driver!.find(table, { object: table, ...query } as any)packages/metadata/src/loaders/database-loader.ts:240this.driver!.findOne(table, { object: table, ...query } as any)packages/metadata/src/loaders/database-loader.ts:247this.driver!.count(table, { object: table, ...query } as any)packages/objectql/src/engine.ts:3317secretDriver.find('sys_secret', { object: 'sys_secret', where: { id } } as QueryAST)packages/objectql/src/lifecycle/lifecycle-service.ts:772driver.count(obj.name, { object: obj.name })(无 cast,靠形参松)(行号取自
origin/main@80f7dc6之后的分支,会漂。)为什么是一笔账
两层,第二层才是重点:
object又写一遍 —— 就是 [spec] IDataDriver 的 query 参数要求QueryAST.object与第一实参重复 —— 下游被迫as any(20 处实测),提议 Omit/optional 化 #5181 消掉的那个冗余,只是它活在 cast 后面躲过了TS2353。as any把整个 query 的检查关掉:不只是object,where/orderBy/fields一并失去检查。这正是 [spec] IDataDriver 的 query 参数要求QueryAST.object与第一实参重复 —— 下游被迫as any(20 处实测),提议 Omit/optional 化 #5181 的 changeset 点名的那笔账,而database-loader是元数据加载的主读路径。as QueryAST(engine.ts:3317)好一些但同理:它只是为了满足object必填而存在,删掉键之后这个 cast 本身也就不需要了。不是缺陷,别当缺陷派
今天没有人踩:这些站点传的
object与第一个实参逐字相同(已逐处核对),且git grep 'query\.object' -- 'packages/drivers/*/src'在main上仍是零命中,没有任何驱动读它。所以这是休眠的冗余 + 自愿放弃的检查,不是活体缺陷。如果要做
database-loader那三处大概是这个形状(query本身若已是DriverQuery形,连展开都不需要):packages/objectql/src/engine-unknown-option.test.ts:183的engine.find('task', { object: 'person' } as any)是一条拒绝测试,按「见object:就删」的批量清扫会直接毁掉它。另外expand条目里的object命名的是关联对象、以及syncSchemasBatch([{ object, schema }])里的object是被真实读取的必填键,两者都不能删。参考 PR #6210 里的删键办法:用带反向引用的模式,只在值与第一个实参逐字相同时才匹配,等价性由模式本身保证。
会话:
session_01WyvqvKMG6asi9aXjKE6xtx(#6075 下游清扫期间发现,未认领)