fix(metadata,objectql): drop the redundant object key from driver queries and the casts carrying it (#6231) - #7182
Conversation
…ueries and the casts carrying it (#6231) `DriverQuery` (`Omit<QueryAST, 'object'>`) exists since #6076 and five drivers followed in #6075, but five call sites stayed as they were because they sat behind a cast the compiler could not see through. The key itself is inert -- `git grep 'query\.object' -- 'packages/drivers/*/src'` is zero, so no driver reads it. The cast was the cost: `as any` on a query argument switches off checking for `where` / `orderBy` / `fields` too, the account #5181's changeset opened (cloud#1030's `$like` reached runtime through exactly this hole). - metadata `DatabaseLoader._find/._findOne/._count`: declare `query: DriverQuery` and pass it to the driver unchanged and uncast (9 call sites re-checked). The ENGINE branch keeps its `as any` and says why in a comment: it is blocked by a spec-level divergence, not vestigial. - objectql `resolveSecret`: the `sys_secret` read loses both the key and the `as QueryAST` that existed only to satisfy it. - objectql `LifecycleService` governance counter: passes argument one only, and its hand-written driver shape becomes the named `CountCapableDriver` typed with `DriverQuery` instead of `Record<string, unknown>`. Pins added at all three sites assert the shape the driver is actually handed; re-adding the key without a cast is now `TS2353`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6bLax4KMrSfnE1ydFU8Dw
…g issue (#7178) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6bLax4KMrSfnE1ydFU8Dw
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 17 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also reference the affected code. These are read-only:
|
|
ACCEPT — PM step-7 review ( CI on The fix is deeper than the card asked for, in the right direction. The card reads as "delete a redundant key at five sites". What actually made the casts unnecessary was retyping the three read helpers in My assumption 1 was handled exactly as intended. I offered the back-reference pattern (PR #6210's method) as a starting point and said five careful edits beat one clever regex if the sites are not uniform. They are not — one site had no cast at all and needed an interface changed — so the pattern was used as a finder and equivalence-prover, never as a rewriter, and the five edits are by hand. Using the clever tool for the part it is good at and not for the part it is not is the judgement I wanted. The self-defence check is the best thing in this report. Beyond "does it go red without the fix" (3/3, with the exact predicted causes), the dev ran a second prediction: re-add the key without a cast and predict a compile error. Measured The STOP boundary held under pressure, and that matters more than the extra cleanup would have. Three adjacent engine-branch casts (not among the five sites) turned out to be blocked by a real pre-existing divergence: Two more results worth having:
Honest non-measurements: the ratcheted Landmine and forbidden keys confirmed intact. The Cross-seat note: #7178 lands in Generated by Claude Code |
Fixes #6231
What this is
DriverQuery(Omit< QueryAST, 'object' >) has existed since #6076 and five drivers followed in #6075, but five call sites stayed as they were — because they sat behind a cast the compiler could not see through.The redundant key was never the expensive half.
git grep 'query\.object' -- 'packages/drivers/*/src'is still zero onmain: no driver reads it, so the key is inert and nothing disagrees at runtime. The cast is the cost.as anyon a query argument does not suppress one key — it switches off checking forwhere/orderBy/fieldsin the same literal. That is the account #5181's changeset opened (cloud#1053 measured 20 such sites; cloud#1030's$like, an operator the filter dialect does not have, survived compilation and reached the runtime through exactly this hole).DatabaseLoaderis the metadata main read path, so it was the worst place to be running unchecked.The five sites, as found
Located by content, not by line —
engine.tshad drifted ~800 lines since the card was filed. All five were exactly as the drivers seat's handoff described them.origin/main@3e8e669c0packages/metadata/src/loaders/database-loader.ts:232this.driver!.find(table, { object: table, ...query } as any)packages/metadata/src/loaders/database-loader.ts:239this.driver!.findOne(table, { object: table, ...query } as any)packages/metadata/src/loaders/database-loader.ts:246this.driver!.count(table, { object: table, ...query } as any)packages/objectql/src/engine.ts:4135secretDriver.find('sys_secret', { object: 'sys_secret', where: { id } } as QueryAST)packages/objectql/src/lifecycle/lifecycle-service.ts:791driver.count(obj.name, { object: obj.name })File-by-file, including the declared cross-surface touches
packages/metadata/src/loaders/database-loader.ts(this card's own surface) — the three read helpers_find/_findOne/_countdeclaredquery: Record< string, unknown >, which is why a cast was needed at all. They now declarequery: DriverQueryand hand it to the driver unchanged and uncast. That re-checkswhere/orderBy/fieldsat all nine of their call sites, not just the three edited lines.packages/objectql/src/engine.ts— declared cross-surface touch (domain:engine-core), authorized by the PM on this card. One line: thesys_secretread inresolveSecret. Theas QueryASTexisted only to satisfy the AST's then-requiredobject; with the key gone the cast is gone too. #5574, the in-flight engine-core claim on this file, closed completed on 2026-08-08.packages/objectql/src/lifecycle/lifecycle-service.ts— declared cross-surface touch (domain:engine-core). This site carried no cast; the redundant key was admitted by a hand-written driver shape whosequerywasRecord< string, unknown >— which would equally have admitted awherethe dialect does not have. Following the file's existing idiom (ReclaimCapableDriver,RotationCapableDriver), that inline shape becomes a namedCountCapableDrivertyped withDriverQuery, and the call passes argument one only.Tests — a pin at each of the three sites (details below).
scripts/query-options-erasure-baseline.json— not in the declared surface; declaring it here. Removing the three driver-branch casts lowered that file's erasure count 6 → 3, and the #4918 ratchet requires the baseline committed (ratchet DOWN: run --update and commit). One line changed, mine only. Caught locally; it would otherwise have been a red ESLint job..changeset/driver-query-redundant-object-callers.md— patch/patch.Not touched, as instructed:
packages/metadata/src/plugin.ts,repository.ts(#7000),packages/metadata-protocol/src/protocol.ts(#6992),packages/spec(#6298),content/docs/releases/.Pattern or hand-edits?
Both, in that order — the pattern as a finder, hand-edits as the fix.
The card's suggested back-reference pattern works and I used it, but only to enumerate and to prove equivalence. It is not usable as a rewrite here because the five sites are not uniform: three spread a variable (
{ object: table, ...query }), one is a literal with awhere, and one has no cast at all and needed an interface changed rather than a call. Five careful edits, one pattern to prove the set was complete:The back-reference is what makes equivalence a property of the pattern rather than of my reading: it matches only when the value is character-for-character the first argument.
Landmine and protected keys — verified intact, and verified by the pattern.
packages/objectql/src/engine-unknown-option.test.ts:183still readsengine.find('task', { object: 'person' } as any)— untouched. The pattern does not match it, because'person'is not'task'; that is precisely the reason to use a back-reference instead of grepping forobject:. Both other keys are equally unmatched and untouched:objectinside anexpandentry (:191,:198) names the related object, andsyncSchemasBatch([{ object, schema }])'sobjectis genuinely read.The pattern also found that the card's measurement was source-only. Ten more sites of the identical shape live in driver test files (
driver-mongodb×9,driver-sql×1) — one of them spelling the castas never, which anas anygrep misses. Those are the drivers seat's surface and not in this card's authorized file list, so they are filed, not fixed: #7177.What the removed casts exposed (the interesting part)
Assumption 3 of the dispatch was right, and it landed somewhere more useful than expected.
The driver calls — the five sites — compile with no cast at all, and exposed zero new errors.
Record< string, unknown >frombaseFiltersatisfiesFilterCondition, and every call shape (where,where+fields,where+orderBy+limit+offset) satisfiesDriverQuery. So the query bag at those nine call sites was genuinely well-typed all along; the cast was buying nothing and hiding everything.I also tried to remove the three engine-branch casts in the same helpers (
this.engine.find(table, query as any)). Those are not among the five sites and were pre-existing onmain. They do not compile, and the reason is a real pre-existing spec divergence:BaseQuerySchema.searchisz.union([ z.string(), FullTextSearchSchema ]), and its doc says the bare string is the canonical Tier-1 contract per ADR-0061 D1. Its siblingEngineQueryOptionsSchema.searchisFullTextSearchSchema.optional()— structured form only. SoDriverQueryis not assignable toEngineQueryOptionsParsed, purely because ofsearch. The runtime serves the string, and objectql's own 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.Fixing that means editing
packages/spec, which this card's dispatch declared a STOP boundary. So per the instruction not to paper over a surfaced error with a narrower cast, the three engine-branch casts are left byte-identical tomain, with a comment naming the cause and the tracking issue. Filed as #7178.Tests
A pin at each of the three sites, asserting the shape the driver is actually handed — not that the source text lacks a key:
packages/metadata/src/loaders/database-loader.test.ts— wraps the mock driver'sfind/findOne/count, drives every read path the loader owns (load,loadMany,exists,list,stat,save), asserts the object name arrives as argument one and the AST has noobject.packages/objectql/src/lifecycle/lifecycle-service.test.ts— the governance counter is called with argument one only.packages/objectql/src/secret-fields.test.ts—resolveSecret'ssys_secretread carrieswhereand noobject. (buildEnginenow also returns its stub driver so the AST can be observed.)I did not add type-level pins for
DriverQueryitself:packages/spec/src/contracts/data-driver.test.ts:203already carries the@ts-expect-errorthat a redundantobjectis rejected (#5181/#6076), inside a package tsc really compiles. Duplicating it here would add a second, weaker copy.Reverse verification
Direction predicted before running: reverting each source file to my pinned base restores both the key and the cast, so all three pins should go red. Reverted via a saved patch and
git checkout --, nevergit stash. Base is my own pinned3e8e669c0, not a movingorigin/main.Predicted red, went red — all three, each with the predicted cause:
Second prediction — is the fix self-defending, or only test-pinned? Re-added
object: 'sys_secret'to the engine.ts call without a cast. Predicted a compile error; got exactly one:So a re-add is caught by the CI-gated
TypeScript Type Checkjob, not merely by my tests. Only a deliberate new cast could re-open the hole, and the three pins catch that.Guards, not evidence (green in both directions, reported as such): the metadata suite (589) and full objectql suite (2851) pass on both sides of the revert. They cannot go red on this change, because the removed key was inert on every path — which is the card's own premise. Their value here is confirming no behaviour moved, and that is all I claim for them.
Left unmeasured, deliberately: the ratcheted
@objectstack/metadataDEBT count could not be re-measured throughcheck:type-check-debt, which refuses to run without the whole 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 — debt-neutral, and below its recorded 92. So this change restores checking at nine call sites without adding a single type error.Verification
pnpm --filter @objectstack/objectql typechecktsc --noEmit, no output)pnpm --filter @objectstack/objectql testpnpm --filter @objectstack/metadata testpnpm --filter '@objectstack/metadata' --filter '@objectstack/objectql' buildpnpm lintpnpm check:query-options-erasurepnpm check:slot-lookuppnpm check:verify-stand-inpnpm check:nul-bytes@objectstack/metadatahas notypecheckscript (DEBT ledger), so its type errors are not gated by theTypeScript Type Checkjob — but it is gated by the build:tsup's dts step compiles it, and it is a dependency ofobjectql, so the first version of this change failed the build rather than sailing through. That is how thesearchdivergence above was found.Out of scope, filed not fixed
object-in-query sweep of #6231 was source-only — 10 more sites of the same shape live in driver test files #7177 — 10 more sites of this exact shape in driver test files; the card's measurement was source-only.finding, unlabeled for queueing, drivers surface.EngineQueryOptionsSchema.searchrejects the bare query string that ADR-0061 D1 calls canonical — so every engine caller that wants it mustas any, losing the whole query's checking #7178 —EngineQueryOptionsSchema.searchrejects the ADR-0061 D1 canonical bare string, forcingas anyat every engine caller. Unlabeled for triage. This is what blocks removing the three surviving engine-branch casts.🤖 Generated with Claude Code
https://claude.ai/code/session_01W6bLax4KMrSfnE1ydFU8Dw
Generated by Claude Code