feat(driver-sql,driver-turso): aggregate() publishes its declared return type, not any (#17277) - #17689
Conversation
…urn type, not any (#17277) `IDataDriver` declares `aggregate?(object, query, options?)` as `Promise<Record<string, unknown>[]>`; `SqlDriver.aggregate` published `Promise<any>` over it and `TursoDriver` overrode the door with its own `Promise<any>`, so both packages' emitted `.d.ts` erased the declaration. #15267 repaired five sibling doors on these same two files and left this one alone, because `SqlDriver.aggregate`'s own code comment asserted `aggregate` was not on the contract. It is, and it was — that comment is the reason the census missed the door, and it is corrected here at the source site. Each annotation is replaced with the type the contract already declares, each is pinned at the type level in its own package's tsc program, and the nine consumer dereferences the narrowing surfaced — all in driver-sql's own tests, all reads of a `.find()` result with no `undefined` check — narrow with vitest's `assert()`. No runtime behaviour changes. Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM Co-authored-by: Claude <noreply@anthropic.com>
…7277) `SqliteWasmDriver` does not override `aggregate()` and inherits it, so the driver-sql narrowing reaches its callers through that package's `.d.ts`. The same nine dereferences appear here: reads of a `.find()` result over an aggregate row with no `undefined` check. Each narrows with vitest's `assert()` — a narrowing assertion, not a `!` and not a cast. No source change in this package. Also adds the two changesets declaring the type-surface narrowing. Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d221a9160277678084d7af41a8e18ea5478d312c && git checkout d221a9160277678084d7af41a8e18ea5478d312c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6465cc0a7c83ba60503bfd4ddd907d35220e4244 d1d6952e2679db1b2edd14c9d1b9bb6f2f35ecbc && git checkout -B drift-repro 6465cc0a7c83ba60503bfd4ddd907d35220e4244 && git merge --no-ff d1d6952e2679db1b2edd14c9d1b9bb6f2f35ecbc
node scripts/docs-audit/affected-docs.mjs --json 6465cc0a7c83ba60503bfd4ddd907d35220e4244
|
Contract reviewHead reviewed: ① Derived judgments — the accept-set / public-surface changes, each named and judged
② Semver grading — consistent with the changeset declaration
⭐ Precedent verified by this seat, ⛔ not taken from the report: landed commit
③ Boundary flags and
|
Fixes #17277
Clause-②: yes
What this changes
IDataDriverdeclares the door as(
packages/spec/src/contracts/data-driver.ts).SqlDriver.aggregatepublishedPromise<any>over it, andTursoDriveroverrides the door with its ownPromise<any>— two sites, not one, because an override re-declares the door in its own package's.d.tsand picks up nothing from a driver-sql-only fix.Both annotations are now the contract's own. Proven at the artifact level, in the emitted declarations after
pnpm --filter @objectstack/driver-sql --filter @objectstack/driver-turso build:Each door is pinned at the type level inside its own package's tsc program, extending the family files #15267 landed (
sql-driver-doors-declared-types.test.ts,turso-driver-doors-declared-types.test.ts) rather than starting new ones — both halves per door, so putting an annotation back toPromise<any>reds the file twice (IsAnyflips totrue,Equalstofalse). No runtime behaviour changes.Optionality is not material.
aggregateis declaredaggregate?where #15267's five doors are required. The?governs whether the member EXISTS, not what it returns once it does — the engine's own dispatch (typeof driver.aggregate === 'function') hands the caller a function whose published return wasanyand is now the contract's record array. The contract half of each pin reads the member throughNonNullable, exactly asexplainalready did.Why #15267's census missed this door
SqlDriver.aggregatecarried its own code comment asserting the opposite:That sentence was false, and it is the entire mechanism: #15267's census asked "is
aggregateon the contract?" and answered from this comment instead of frompackages/spec/src/contracts/data-driver.ts. The reading then travelled — into the census, into the card, and into the dispatch order for #15267, which repeated it as a ruling. The implementer followed the order exactly and was right to; nothing in the chain re-measured it against the contract.The comment is corrected at the source site in this PR, and the mechanism is recorded in both pin files, where the next person extending this family will read it. A comment is not the contract.
The re-run census (triage ordered this; it is the real deliverable)
Predicate built from
data-driver.tsitself, never from a code comment: parse theIDataDriverinterface, enumerate every member it declares, then compare each driver class's own published return-type annotation against the declaration.IDataDriver(32 methods, 3 properties).InMemoryDriver,MongoDBDriver,SqlDriver,SqliteWasmDriver,TursoDriver,RemoteTransport. Test-only probe subclasses excluded.EXACT101 ·MASKED13 ·DIFFERENT4 ·INFERRED18 ·ABSENT74.EXACT103 ·MASKED11 ·DIFFERENT4 ·INFERRED18 ·ABSENT74.MASKED= the published annotation containsanywhere the declaration does not. Control: the predicate returns non-zero (13 before, 11 after) and independently re-derivesaggregateon both SQL drivers, the two doors this card was filed for; the zero-cases are visible in the same table (EXACTrows), so the predicate is shown to have covered the honest doors rather than skipped them.SqlDriverfindPromise<Record<string, unknown>[]>Promise<any[]>driver-sql/src/sql-driver.tsSqlDriverupsertPromise<Record<string, unknown>>Promise<Record<string, any>>driver-sql/src/sql-driver.tsSqlDriverbulkUpdatePromise<Record<string, unknown>[]>Promise<Record<string, any>[]>driver-sql/src/sql-driver.tsSqlDrivertemporalFilterValueunknownanydriver-sql/src/sql-driver.tsTursoDriverfind(override)Promise<Record<string, unknown>[]>Promise<any[]>driver-turso/src/turso-driver.tsTursoDriverupsert(override)Promise<Record<string, unknown>>Promise<Record<string, any>>driver-turso/src/turso-driver.tsTursoDriverbulkUpdate(override)Promise<Record<string, unknown>[]>Promise<Record<string, any>[]>driver-turso/src/turso-driver.tsTursoDriverbeginTransaction(override)Promise<unknown>Promise<any>driver-turso/src/turso-driver.tsRemoteTransportbeginTransactionPromise<unknown>Promise<any>driver-turso/src/remote-transport.tsInMemoryDriveraggregatePromise<Record<string, unknown>[]>Promise<any[]>driver-memory/src/memory-driver.tsInMemoryDriverbulkCreatePromise<Record<string, unknown>[]>Promise<Record<string, any>[]>driver-memory/src/memory-driver.tsThe
driver-memoryrows are named as findings only. That package is under an investment freeze whose exception channel is a triage naming, so escalating them is triage's act and neither this PR's nor its dispatching seat's.find,bulkUpdateandtemporalFilterValueare named in neither #15267's repaired set nor its deliberately-excluded set — they were never seen.upsert,aggregateandbeginTransactionwere seen and excluded by name. The spelling correlates exactly: every door #15267 repaired is annotated with the literal stringPromise<any>, while every door it never named carries theanynested inside a wider type (Promise<any[]>,Promise<Record<string, any>>,Promise<Record<string, any>[]>, bareany) — which a literal-string predicate does not match. Reported, deliberately not widened into: whether that remainder is one card or a bigger PR is the dispatching seat's call, not this PR's.Two rows the predicate flags as
DIFFERENTrather thanMASKEDare not findings, recorded so the table is readable:SqlDriver.beginTransactionpublishesPromise<Knex.Transaction>andMongoDBDriver.beginTransactionpublishesPromise<ClientSession>— both narrower than the declaredPromise<unknown>, which is the honest direction.Blast radius of the un-masking
18 dereferences, every one in the drivers' own tests, none in shipped source:
@objectstack/driver-sql(sql-driver-advanced.test.ts,sql-driver-queryast.test.ts)@objectstack/driver-sqlite-wasm(sqlite-wasm-driver-advanced.test.ts,sqlite-wasm-driver-queryast.test.ts) — that package does not override the door and inherits it, so the narrowing reaches its callers through driver-sql's.d.ts@objectstack/driver-tursoAll 18 are the identical shape and the identical defect:
result.find((r: any) => …)returnsT | undefined, and throughPromise<any>the whole expression came backany, so a cell read off the row compiled with no check at all.expect(x).toBeDefined()does not narrow. Each site now asserts the row arm with vitest'sassert()— a narrowing assertion, not a!and not a cast — matching the idiom #15267 established for the same class.Nothing was re-masked to quiet
tsc.Verification
All readings below are against
d1d6952e26, the final commit on this branch.pnpm --filter @objectstack/driver-sql --filter @objectstack/driver-turso typecheckTS18048before the consumer sites were narrowed (the defect appearing), 0 afterpnpm --workspace-concurrency=2 --filter '...@objectstack/driver-sql' --filter '...@objectstack/driver-turso' run typecheckTS18048indriver-sqlite-wasmand nothing else; exit 0 after narrowingpnpm --filter @objectstack/driver-sql --filter @objectstack/driver-turso --filter @objectstack/driver-sqlite-wasm run testpnpm lint(eslint . --no-inline-config, whole repo, no narrowing)node scripts/pm/dispatch-gates.mjs --rannode scripts/pm/check-clause2-carriers.mjs --pair 17689Three gates first answered exit 3 — PREREQUISITE NOT MET (
check:dual-build-cjs-loads,check:lean-entry-closure,check:type-check-debt), which is not a pass. Each was re-run afterturbo run buildover the package closure and then exited 0; the reconciliation above records the 0s.Reverse verification
Direction predicted before either leg ran: putting one annotation back to
Promise<any>should red exactly two consts in that package —IsAnyflips totrueandEqualstofalse— and nothing else. Both legs ran from the committed state, each mutation proved on disk by agit hash-objectchange against theHEADblob before the run, each restored withgit checkout HEAD -- <path>and proved back by blob equality plus an emptygit status --porcelain.Lines 121/122 are
sqlAggregateIsAny/sqlAggregateIsContract; 111/112 aretursoAggregateIsAny/tursoAggregateIsContract. Both pins fail when the defect returns, in the direction predicted, and no other assertion in either package moves.Acceptance notes
analyzeQuery()remains correctly out of scope — it has no hit anywhere indata-driver.tsand is a public helper behindexplain(), not a door. That half of [finding] driver-sql / driver-turso still publishPromise[any]on fiveIDataDriverdoors after #14438 —findOne,create,bulkCreate,execute,explainonSqlDriver, andTursoDriver.create()'s own override #15267's original claim survives re-measurement.SqlDriver.registerExternalObject/registerObjectMetadatapublishvoidagainst a declaredvoid | Promise<void>. Narrower than declared, so the honest direction; recorded only because the census surfaced them.Authored by the
os-devseat undersession_01RuoNSXUbBoWHkNS4AknTrM, dispatched by thedomain:engineexecution PM seat on #17277.Generated by Claude Code