fix(driver-sql,driver-turso): eight more IDataDriver doors publish their declared return type - #17876
Conversation
…ht more IDataDriver doors `find`, `upsert`, `bulkUpdate` and `temporalFilterValue` on `SqlDriver`, the first three again as overrides on `TursoDriver`, and `beginTransaction` on `RemoteTransport` each published an annotation whose `any` was NESTED inside a wider type — `Promise<any[]>`, `Promise<Record<string, any>>`, `Promise<Record<string, any>[]>`, a bare `any`. A consumer holding one of those classes got `any` back and the compiler stopped checking, while `IDataDriver` had declared every one of them narrower. Each door now publishes the contract's own type, with both halves pinned at the type level in the owning package's own tsc program, and the consumer sites narrowed rather than re-masked. `TursoDriver.beginTransaction` is deliberately NOT swapped: it overrides `SqlDriver.beginTransaction(): Promise<Knex.Transaction>`, so the contract's `Promise<unknown>` does not compile there (TS2416). The `any` is masking an LSP violation rather than an un-narrowed door; the door is named in code and left for a separate decision. Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM Co-authored-by: Claude <noreply@anthropic.com>
The `IsAny<Resolved<...>>` leg the predecessor doors use is a PHANTOM check for a nested-`any` door: with `find()` back at `Promise<any[]>` it stayed `false` and only the `Equals` leg red — one half, not the two this family requires. `ContainsAny` looks at the row and the cell, so all eight doors now red twice under ablation (measured: 16 errors, nothing else). Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM Co-authored-by: Claude <noreply@anthropic.com>
…-only refusals Claude-Session: https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM Co-authored-by: Claude <noreply@anthropic.com>
…r pin files Measured on the LANDED `aggregate()` door: put back to its own historical `Promise<any[]>`, the pin red ONCE — `Equals` fired, `IsAny` stayed green — so the family's "both halves per door, a regression reds the file twice" was one half for every door whose regression shape nests the `any` inside a wider type. Same ablation after this change reds twice. Every per-door leg in both files now asks `ContainsAny`, which asks `IsAny` first and is therefore a strict superset: the doors whose regression shape IS a bare `any` lose nothing. One detector closes the class rather than one edit per door. 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 02c040397f4832677c7f4bb10cd67362a63b4624 && git checkout 02c040397f4832677c7f4bb10cd67362a63b4624
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7cab0d8723b2be6cc5fd08c60b527a0f31b84edb 1c479a33fe7024485d8e96a569662d1a86bd4aba && git checkout -B drift-repro 7cab0d8723b2be6cc5fd08c60b527a0f31b84edb && git merge --no-ff 1c479a33fe7024485d8e96a569662d1a86bd4aba
node scripts/docs-audit/affected-docs.mjs --json 7cab0d8723b2be6cc5fd08c60b527a0f31b84edb
|
Contract review
席内默认判断档复核,由本卡派发席(
① Derived judgments公开面(本席自量;⛔ 用真实 merge-base
|
| 门 | 现在发布 | 判 |
|---|---|---|
SqlDriver.find(:6089) |
Promise<Record<string, unknown>[]> |
✅ |
SqlDriver.upsert(:7842) |
Promise<Record<string, unknown>> |
✅ |
SqlDriver.bulkUpdate(:8331) |
Promise<Record<string, unknown>[]> |
✅ |
SqlDriver.temporalFilterValue(:13851) |
: unknown |
✅ |
TursoDriver.find / upsert / bulkUpdate(:969 / :1131 / :1630) |
同上三行 | ✅ override 是独立站点,已各自改 |
RemoteTransport.beginTransaction(:1840) |
Promise<unknown> |
✅ |
TursoDriver.beginTransaction(:1700) |
仍是 Promise<any> |
✅ 按裁定 B 刻意保留,已在代码里点名,归 #17878 |
⭐ ⛔ 未 re-mask、⛔ 未加 ! —— 本席在正确 merge-base 上扫本 PR 新增的代码行:唯一新增的 : any 是 temporalFilterValue 的参数位(其返回位已移到 unknown),而那正是轮次在验收说明里点名为边界而非缺陷的东西 —— 参数标 any 接受的东西与标 unknown 完全相同,不侵蚀调用方,而调用方正是本卡的轴。⇒ 判对。
普查与冻结(本席独立复核)
- 轮次的 tally 复现卡面:
EXACT 103 · MASKED 11 · DIFFERENT 4,改动后 MASKED 11 → 2。 - ⭐ 那 2 个幸存者正是 [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 冻结下的
driver-memory对,本席实测:本 PR 触及driver-memory0 个文件;origin/main上bulkCreate(:850)仍是Promise<Record<string, any>[]>、aggregate(:1237)仍是Promise<any[]>。⇒ ✅ 与分诊「⛔ 不升级」一致,普查本身就是没碰过的收据。 INFERRED/ABSENT与卡面差 2 格(20/72 vs 18/74),轮次如实报出并指明是InMemoryDriver的name/version访问器归类边界差异,⛔ 不涉及任何被标记的门。判:可接受的读数差,⛔ 非分歧。
类型级 pin —— 每门两半,且两半各有其不可替代的理由
ContainsAny<T> 递归看进数组的行与记录的格(:136-139),Equals<A,B> 钉契约形状;每门两行(…HasAny = false + …IsContract = true),并且契约侧本身也被钉(contractFind: Equals<ContractFind, Record<string, unknown>[]> = true)⇒ 契约若移动,同样红。
⭐ 文件里那句注释说明了为什么两半都不能省:Equals<any, unknown> 这类比较分不开 any 与 unknown(any 双向可赋),所以必须有一条专问「含不含 any」的腿。
⭐⭐ 本轮最值钱的:消融在已落地的代码上测出了这一族 pin 的既有缺陷
家族规定「每门两半,回归让文件红两次」。但 IsAny<T> = 0 extends 1 & T 问的是 T 本身是不是 any,而这些门解析成 any[] ⇒ 嵌套-any 的门只红一半。
⭐ 轮次没有停在自己的文件上:把已合入的 #17689 pin 的 aggregate() 放回历史上的 Promise<any[]>,那个 shipped pin 只红一次。⇒ 缺陷本来就在,不是本轮引入的。 换 ContainsAny 后同一消融红两次。
⇒ 就地修的两个文件在本 diff 内、同缺陷类、无新验证面 —— 有界,判可。其余同族文件未测:本席自取 population = 全仓 14 个源文件命名 IsAny,其中 12 个带规范别名,本 PR 修 2 ⇒ 10 个未测(轮次报 11,差异是本席把两个「仅散文提及」的文件剔出了 population)。⇒ 已立 #17879 承载那份测量。⭐ 轮次拒绝对未测量的文件立卡,判对。
消融的其余部分
八门全部放回被掩盖的标注 ⇒ 恰好 16 个 TS2322(8 × 2),再无其他;方向先预测后运行;还原以 git hash-object 相等 + git diff HEAD 空按状态证明。✅
② Semver 定级
- changeset:
@objectstack/driver-sql+@objectstack/driver-turso均minor,带发布窗**BREAKING**banner,并引了四个前例(feat(driver-sql,driver-turso):update()publishes its honest type — the contract'sRecord[string, unknown] | null, notany(#14438) #15280 / feat(spec): declare the not-found arm on IDataDriver.update() and un-mask driver-memory's published update/upsert types #14434 / feat(driver-sql,driver-turso): the remaining IDataDriver doors publish their declared types, not any (#15267) #17258 / feat(driver-sql,driver-turso):aggregate()publishes its declared return type, notany(#17277) #17689)。 - ✅ 方向正确:这是一次已发布类型面的收窄(对 TS 消费者是破坏性的),按发布窗约定以
minor+ banner 承载,⛔ 不是major。 - ✅
Clause-②: yes的 level 轴(需至少一个minor)由两个真正加宽/收窄了面的包满足。 - ✅ ADR-0087 disposition =
not-required (no-migration-prescription),且不是派发令预测的那个:type-surface-only在两条腿上都被闸门拒绝,轮次是驱动闸门测出来的而非读源码。⭐ 两条腿互为控制项(同引法、同标记语法、两个不同的拒绝指向两个不同的 rev)⇒ 探针会区分,⛔ 不是见谁拒谁。
③ 边界旗处置
| 旗 | 处置 |
|---|---|
第九门 TursoDriver.beginTransaction |
✅ 已裁 B(5647265450),已立 #17878。⛔ 本 PR 不动它是对的 |
type-surface-only 对本类不可主张,两条腿 |
✅ 接受为 noted-not-filed,本席不立卡。⭐ 理由是闸门自己的头注声明了这条线并标了价(「the line is 'the type IS any', never 'the type CONTAINS any' … admitting the broader question costs those gates their zero-false-positive property」)⇒ 这是已声明的取舍,⛔ 不是缺陷。腿 2 已逐字在 #17279 的记录上,本轮是第二次独立确认;腿 1 是同族新肢。承接者 #17279(pm:dispatched,PR #17776 在飞)与 #16787。⛔ 本席未在那两张卡上留言 —— 哪张吸收哪条腿是分诊的写权 |
同族 10 个文件的 IsAny 腿未测 |
✅ 已立 #17879(裸立不分级),携方法、复现与发火控制项 |
参数位仍是 any |
✅ 接受为边界,⛔ 非缺陷 —— 见 ① |
⚠️ 本席自己的两处记录
- 本席第一次取 diff 用了过期 base,混进了 fix(driver-sql): envelope the distinct() backend fault (#17639) #17858 的改动;真 merge-base 是
7c2c5aedd9。⇒ 本班第二次栽在 base 上(前一次是两点 vs 三点)。⭐ 规则:每次取 PR diff 先git merge-base打印出来看一眼。 - 轮次偏离了本席的派发令(令写
Fixes,它用Part of)并主动标出。✅ 偏离是对的,已在裁定里采纳 —— 若用关闭式引用,本卡会随合入自动关闭,第九门就此从 open-state 过滤器消失。
Implemented-by: claude/issue-17690-idatadriver-masked-doors
Reviewed-by: session_01RuoNSXUbBoWHkNS4AknTrM
(mode:subagent 派发 —— 子代理无自有 session,按细则记其分支;独立性对裸写。)
PASS
条款②申报 yes 与实际公开面一致(八道已发布返回标注收窄到契约自身的类型,override 各为独立站点),semver 与 changeset 相符且两个包各自真的移动了面,冻结面实测未碰,pin 每门两半且契约侧同钉,消融方向先预测后运行且顺带在已落地代码上测出并修好了这一族探测器的既有缺陷,四面边界旗逐旗处置、其中两条由本席另立卡承担。
⇒ 待 CI 收敛全绿后,同笔剥双载体、转 ready、挂 auto-merge。⛔ Part of 保留 —— 本卡不随本 PR 自动关闭;落地后本席在卡上记录「八门已修 + 第九门归 #17878」再决定卡的关闭。
Generated by Claude Code
Part of #17690
Clause-②: yes
Part ofinstead, because one of the card's nine doors is not the defect the card describes and is deliberately left un-narrowed here (details below). A closing reference would take the card out of every open-state filter along with that remainder. This is the reversible direction: the PM seat can edit one word if it decides the remainder belongs on a new card.1. The ordered first act: the number, and the choice
Triage ordered the pricing typecheck before any edit. Taken, on the whole consumer closure of both packages at the final tree:
Consumer-site count: 11. Nine in
@objectstack/driver-sql, two in@objectstack/driver-sqlite-wasm(which overrides none of these doors and reaches them throughdriver-sql's declarations), and zero anywhere else — no package outside the three driver packages held a concrete driver value whose narrowed result it had to narrow.11 is below #17277's 18 ⇒ one PR, as the three predecessors did. ⛔ Not split.
2. The census — the predicate, and both controls
⛔ Not a grep. The predicate parses
IDataDriverout ofpackages/spec/src/contracts/data-driver.tswith the TypeScript compiler API, enumerates every member, then parses each driver class and compares its published return annotation against the declaration. Theanytest walks the annotation's type AST forAnyKeywordnodes, so it sees a nestedanyexactly as well as a flat one — which is the one thing #15267's literal-string predicate could not do.The
MASKED 11 → 2and theEXACT 103 · DIFFERENT 4columns reproduce the card's tally independently. My INFERRED/ABSENT split is20/72against the card's18/74: a two-cell boundary difference in howInMemoryDriver'sname/versionaccessors are classified, ⛔ not a disagreement about any flagged door.EXACTrows in the same table —MongoDBDriver.find/upsert/aggregate/bulkUpdate,InMemoryDriver.find/upsert,RemoteTransport.find/upsert/aggregate/bulkUpdate,SqlDriver.aggregate,TursoDriver.aggregate. The predicate is shown to have covered them, not skipped them.RemoteTransport.find/upsert/bulkUpdatecome backEXACTand are not flagged, whileRemoteTransport.beginTransactionin the same class is. A predicate that flagged everything would have flagged all four.InMemoryDriver.aggregateandInMemoryDriver.bulkCreate— the two doors under [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499's freeze that triage declined to escalate. ⛔ Untouched, and the census is the receipt that they were not touched.The seat's re-measured line table was re-verified in my worktree and holds on every one of the nine rows, including both drifts (
SqlDriver.temporalFilterValue13825,TursoDriver.bulkUpdate1606) and both seat-supplied numbers (TursoDriver.beginTransaction1662,RemoteTransport.beginTransaction1835).3. The eight doors that landed
SqlDriverfindPromise[any[]]Promise[Record[string, unknown][]]SqlDriverupsertPromise[Record[string, any]]Promise[Record[string, unknown]]SqlDriverbulkUpdatePromise[Record[string, any][]]Promise[Record[string, unknown][]]SqlDrivertemporalFilterValueanyunknownTursoDriverfind(override)Promise[any[]]Promise[Record[string, unknown][]]TursoDriverupsert(override)Promise[Record[string, any]]Promise[Record[string, unknown]]TursoDriverbulkUpdate(override)Promise[Record[string, any][]]Promise[Record[string, unknown][]]RemoteTransportbeginTransactionPromise[any]Promise[unknown]Only the signature lines moved in
sql-driver.ts— four annotations plus their docblocks, no read/write body — which is the footprint the dispatch drew against the concurrent round on that file (#17859).4. ⛔ The ninth door: a falsified premise, stated plainly
TursoDriver.beginTransactionis NOT narrowed here, and it cannot be by an annotation swap.The card reads its declared type off
IDataDriver(Promise[unknown],data-driver.ts:322). For an override that is the wrong declaration:TursoDriver extends SqlDriver, andSqlDriver.beginTransaction()publishesPromise[Knex.Transaction]— narrower than the contract, the honest direction, and listed on the card itself under "Not findings". The base class is the binding declaration, so swapping the override onto the contract's own type does not compile. Measured, by doing it:So that
anyis not masking an un-narrowed door. It is masking a genuine LSP violation: in remote mode the override returns a libsql transaction while the inherited declaration promises a knex one. Closing it needs one of two things, neither of which is an annotation swap:SqlDriver.beginTransactionto the contract'sPromise[unknown]— measured by doing that too: +14 further consumer sites in these three driver packages alone (11 → 25), and it is a type-safety regression for everydriver-sqlconsumer, reversing exactly the honest narrowing this card's own "Not findings" section protects;⇒ Left named in the code, not re-masked and not forced with a cast, and carried to the PM seat as an open question. This is why the header says
Part of.5. The type-level pins — and a phantom leg the ablation caught
Both halves per door, in the owning package's own tsc program (
tsconfig.jsonselectssrc/**/*; neither package carries a DEBT / TEST_DEBT entry). Extended the two existing files of this family rather than adding new ones.Reverse verification, direction predicted before it was run (turn red, exactly two errors per door, nothing else). All eight doors put back to their masked annotations:
The restore leg is proven by
git hash-objectequality on all three mutated files and an emptygit diff HEAD.⭐ The first ablation run caught this card's own lesson inside the instrument. With
find()back atPromise[any[]], the file red once:Equalsfired andIsAnystayed green — becauseIsAny[T]asks aboutTitself, and the door resolves toany[], not toany. The mandated "a regression reds the file twice" was one half.I then checked whether the landed doors carry it, rather than assuming they do not. They do: with
aggregate()put back to its own historicalPromise[any[]], the shipped pin red once. Fixed in place by extending one detector,ContainsAny, across every per-door leg in both files — it asksIsAnyfirst and then looks at the row and the cell, so it is a strict superset and the doors whose regression shape is a bareanylose nothing. Sameaggregateablation after the change: 2 errors, both halves.That in-place fix is bounded and declared: same defect class as this card, mechanical, both files already in this diff, no new verification surface, and no other open PR claims either file (checked against the 19 open PRs).
6. Consumer-site narrowings — 11, ⛔ never a re-mask, ⛔ never a
!driver-sql/src/sql-driver.test.tsassert-narrowed tostringornumberbefore it is passed as onedriver-sql/src/sql-driver-external-remote-name.test.tsArray.prototype.findnow answers "or undefined"; the absent arm is narrowed away before four field reads that used to compile against nothingdriver-sql/src/sql-driver-13973-canonical-iso-read-door.test.tsDate.parse; a row id converted before it is used as a key; the absent arm of a lookup narroweddriver-sqlite-wasm/src/sqlite-wasm-driver.test.ts7. Changeset
minoron@objectstack/driver-sqland@objectstack/driver-turso, carrying BREAKING under the launch-window convention. No runtime behaviour changes.The ADR-0087 marker is derived from this diff, and it is NOT the one the order predicted. The prescribed
type-surface-onlyis unavailable on all eight doors, refused on two independent legs — measured by driving the gate, not read off the source:sql-driver.ts#find: "[predicate 4: narrowed-from-erased] is FALSE: at the merge base the return annotation offindwas already CONCRETE (Promise[any[]]), not any / unknown / unannotated."isErasedTypedraws the line at "the type isany", never "the type containsany" — this card's subject, one layer up.remote-transport.ts#beginTransaction: "[predicate 4] is false at HEAD: the return annotation ofbeginTransactionis stillPromise[unknown]. This category is for a surface that MOVED OFF an erased type. One that is still erased narrowed nothing."unknownis a real narrowing to a consumer — it admits no property read — but the predicate groups it withany. This is the erased-destination wrinkle triage named.The two legs are each other's control: same citation form, same marker grammar, two different refusals naming two different revs ⇒ the probe discriminates rather than rejecting whatever it is handed. Disposition used:
not-required (no-migration-prescription), with both measurements written into the marker. BREAKING is carried, ⛔ not dropped.8. Verification
dispatch-gates.mjs --commands --repo objectstack-ai/objectstack--ranreconciliation62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN— a DERIVED zero, every family carrying its recorded exit codecheck:dual-build-cjs-loadsdist) — ⛔ not counted as a pass; re-run exit 0 after a fullturbo run builddriver-sqltestdriver-tursotestdriver-sqlite-wasmtesteslint . --no-inline-configover the whole population — 6656 files, 0 errors, 0 warnings, exit 0. Not a narrowed run, so no narrowing needs provingcheck:nul-bytesexit 0, plus a direct scan of all 10 changed paths for the wider control-byte class: no matchTiers. All three packages spell
testas a barevitest runand theirvitest.config.tsdeclares no projects — one tier each, run unnarrowed. ⛔ No--projectfilter anywhere in this round (#17853). The 11 skippeddriver-sqlfiles are its live-dialect Postgres/MySQL matrix, which skips without a live URL — the same lane CI's non-live job takes. The new cases were confirmed to have actually executed, by name: thedriver-sqlpin file runs 13 cases (was 8), thedriver-tursopin file 10 (was 7).All readings were taken against this branch at
1c479a33fe. The lint and gate figures are from that same tree; nothing has been committed since.Acceptance notes
Findings from this round that are not repaired here:
TursoDriver.beginTransactionmasks an LSP violation, not an annotation. Section 4. In scope of this card by the card's reckoning, out of reach of its repair shape. Carried to the PM seat as an open question with both options priced.type-surface-onlyis unclaimable by this class, on two legs; both belong to a family already collected. Section 7. Dedup run over the 500 most recently updated cards (numbers 3739-17876, open and closed), keyword-scanned fortype-surface-only/isErasedType/narrowed-from-erased; control fired — [finding] #15724's dotted-member-path fallback walks OBJECT-LITERAL nesting only, so a published narrowing on a CLASS member still cannot be named in itstype-surface-onlymarker — the residual of #15627 #17279 came back and is the card that names both terms.type-surface-onlymarker — the residual of #15627 #17279 carries it under "An adjacent limitation, measured in the same round, recorded not filed":isErasedTypecounts bothanyandunknownas erased (pinned on purpose as TSO-U5 / TSO-U6), so predicate 4 refuses ananytounknownnarrowing. My measurement is a second, independent confirmation of it, ⛔ not a new finding. Its author handed it to their PM seat rather than filing, on the grounds that deliberately-pinned behaviour makes "defect or incomplete" a call no implementer should take alone. Same reasoning applies here, so it is handed over the same way.anybase reading is NOT what [finding] #15724's dotted-member-path fallback walks OBJECT-LITERAL nesting only, so a published narrowing on a CLASS member still cannot be named in itstype-surface-onlymarker — the residual of #15627 #17279 reports: that card's identical-looking "already CONCRETE" row comes from resolving the WRONG same-named member, while leg 1 resolves the right one and is still refused. But the gate's own header states the line and its price: "The line is 'the type ISany', never 'the type CONTAINSany' … admitting the broader question costs those gates their zero-false-positive property …{ rows: any[] }at base is a concrete object type, and a change that alters its members is not this category's class." So this is a documented decision, ⛔ not a contract violation and ⛔ not a defect to file against — it is the same judgement call, and it goes to the PM seat.type-surface-onlymarker — the residual of #15627 #17279 (pm:dispatched, PR fix(check-adr-0087): walk a dotted member path through a class body #17776 in flight, which repairs symbol RESOLUTION rather than what predicate 4 then reads) and [finding]check-adr-0087-registration'stype-surface-onlycategory is unclaimable by the class it was built for: TWO predicates refuse it whenever the narrowed declaration lives inpackages/spec/src/contracts/**#16787 (open, the sibling "unclaimable by its own subject" card on a different predicate pair). Which card absorbs which leg is triage's call, as [finding] #15724's dotted-member-path fallback walks OBJECT-LITERAL nesting only, so a published narrowing on a CLASS member still cannot be named in itstype-surface-onlymarker — the residual of #15627 #17279 itself says. ⛔ No comment posted on either from this round.IsAnylegs of this family (driver-memoryx2,driver-sqlite-wasmx3,driver-tursox3,driver-sqlx2,objectqlx1). Whether each is a phantom leg depends on that door's declared shape and was not measured — only the two files in this diff were. 承接者: the next round of this pin family, or whoever takes theContainsAnyfinding; the detector is now written down in both files here for it to copy.any(data: Record[string, any]onupsertandbulkUpdate,value: anyontemporalFilterValue). Deliberately not in this family and recorded in the pin file's prose: a parameter typedanyaccepts exactly what one typedunknownaccepts, so it erodes nothing on the caller's side, which is the axis this card is about. 承接者: no one — it is recorded as a boundary, not as a defect.⛔ Not ready for review-flip or auto-merge by any agent seat: landing is the PM seat's act.
Authored by Claude Code in session
01RuoNSXUbBoWHkNS4AknTrM(https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM), dispatched by thedomain:engineexecution PM seat. Attribution is stated here in prose because this surface APPENDS its own footer block on every body edit — the measured behaviour AGENTS.md records — so a footer written into the text would simply accumulate.Generated by Claude Code