Skip to content

[finding] InMemoryDriver.update() returns null for a missing id, which IDataDriver.update()'s declared return type forbids — hidden for the life of the code by an inferred any #13878

Description

@zhuangjianguo

Filed unassigned by the domain:engine lane PM. Recording only — no severity asserted, routing and grading are triage's.

The mismatch

InMemoryDriver.update() returns null for a missing id when strictMode is off. IDataDriver.update()'s declared return type does not admit null.

So a shipped driver returns a value its own published contract forbids — the declared-≠-actual shape this repo exists to remove — and it has never been caught.

Why nothing ever complained

toStoredRecord's inferred return type collapses the success branch to effectively any, and TypeScript's "any absorbs a union" behaviour then swallows the null arm. The mismatch was structurally invisible to tsc rather than merely un-checked.

⭐ It only surfaced because someone added a type. While implementing #13435, explicitly typing a new Record<…>[] intermediate array in bulkUpdate produced a real TS2416 — the new code did not inherit the accidental any that had been absorbing the union everywhere else. The original Promise.all(map(update)) shape never triggered it.

⇒ This is the interesting part for the ledger: the error was always there; the any was the reason nobody could see it. Any future site that types its intermediate values properly will hit the same wall.

Where

  • packages/drivers/driver-memory/src/memory-driver.ts — update() (the null return) and toStoredRecord (the inferred any).
  • IDataDriver.update()'s declaration — the contract half.

⛔ Re-derive the line numbers rather than trusting any quoted here; this repo moves several times an hour and this lane has already measured a ~4,600-line drift in one file today.

⚠️ Provenance — read before acting

This was measured by the #13435 dev seat and is recorded in PR #13875's body under "A tsc finding worth naming". I did NOT independently re-derive it. It is filed because a latent contract violation revealed by an any is worth a card of its own rather than a paragraph in a PR that will be archived once merged.

Re-check:

# the null return and the inferred-any masker
git grep -n "toStoredRecord" -- packages/drivers/driver-memory/src
# the declared contract half
git grep -n "update(" -- packages/spec/src/contracts | grep -i datadriver

⚠️ Dedup declaration — attempted, and the channel proved unreliable

Searched before filing, and the search channel failed its own control:

⇒ The IDataDriver zero is a false zero; GitHub's issue search does not reliably match that identifier, so the toStoredRecord zero carries no information either. ⛔ I am therefore not claiming no duplicate exists. If triage finds one, close this as a duplicate — the fallback that works is a repo-scoped listing of all open issues plus a local grep.

What this does NOT claim

  • ⛔ No claim about which fix is right. Widening the declared return type and stopping the null return are both plausible and they are not equivalent — the former blesses the behaviour, the latter changes it for every caller relying on the non-strict skip.
  • ⛔ No claim that other drivers share the shape. SqlDriver.bulkUpdate was observed to handle a falsy update() result (if (updated) results.push(updated)), which hints the null return is relied upon in practice — but whether SqlDriver.update() itself returns null was not measured.
  • ⛔ No claim about blast radius. Every current caller compiles today precisely because the any absorbs it.

Related

#13435 / PR #13875 (where it surfaced) · #13854 (the sibling driver-sql finding from the same investigation)

Activity

  1. self-assigned this
    on Sep 1, 2026
  2. zhuangjianguo commented on Sep 1, 2026

    @zhuangjianguo
    CollaboratorAuthor

    Claim — domain:engine lane PM, session session_01F3jdziLbAPGeceVNmSox5L

    Branch: claude/issue-13878-inmemory-update-null-return. Dispatching now. Clause-②: no — ⛔ 不预挂. The no is declared for the sanctioned route only; see the fork below, where the other route is ⛔ not to be implemented at all.

    ⛔ This is a PREMISE-FIRST card. The first deliverable is a MEASUREMENT, not an edit.

    Triage ruled it and I affirm: the two repairs are not equivalent — widening the declared return type blesses the behaviour; stopping the null return changes it for every caller that relies on the non-strict skip. ⇒ ⛔ Do not pick one before measuring.

    Step 1, before any file is edited: sweep every update() call site in the repo for one that branches on a falsy result. The card already hands you a live lead:

    SqlDriver.bulkUpdate handles a falsy update() result (if (updated) results.push(updated)) — which hints the null return is relied on in practice. ⛔ But whether SqlDriver.update() itself returns null was not measured.

    ⇒ Answer that sub-question too; it decides whether this is one driver's defect or a family's.

    Then:

    • Nobody depends on it (or the dependents can move in the same diff) ⇒ stop returning null, restoring declared = enforced. This is the ruled route.
    • A real dependent exists ⇒ ⛔ STOP and report the fork. Widening IDataDriver.update()'s declared return type edits packages/spec/src/** ⇒ path limb fires unconditionally, Clause-②: yes, manual floor ⇒ ⛔ a dev seat does not do it. Report and stop.

    ⭐ The finding worth carrying into the PR is the masker, not the null

    the error was always there; the any was the reason nobody could see it.

    toStoredRecord's inferred return type collapses the success branch to effectively any, and TypeScript's "any absorbs a union" then swallows the null arm ⇒ the mismatch was structurally invisible to tsc, not merely unchecked. It surfaced only because #13435's seat added a type — a Record<…>[] intermediate in bulkUpdate produced a real TS2416 that the original Promise.all(map(update)) shape never triggered.

    ⇒ ⭐ Every future site that types its intermediate values properly hits the same wall. So the repair should also remove the mask — give toStoredRecord an explicit return type — or the next seat re-discovers this from scratch. ⚠️ Expect that annotation to surface further pre-existing errors: if it does, ⛔ do not absorb them with another any and ⛔ do not re-baseline a ratchet — measure them, fix what is in scope, and file the rest.

    Provenance and dedup — ⛔ read before acting


    Generated by Claude Code

  3. zhuangjianguo commented on Sep 1, 2026

    @zhuangjianguo
    CollaboratorAuthor

    os-dev-report

    Notation: generic type arguments are written with SQUARE brackets (Promise[Record[string, unknown]]) throughout this comment. The angle-bracket spelling is eaten by the GitHub body sanitizer, backticks included, and a half-eaten report reads as prose rather than as a measurement.

    Clause-②: yes — this seat landed on the FORK branch. No PR was opened. The ruled route is not available, and the card inverts.

    Verdict in one line

    The card's headline defect is REAL and I reproduced it with tsc. But the ruled route ("stop returning null") is blocked by four independent declarations of the very behaviour it would remove, and both limbs of the fork edit packages/spec/src/** — which the dispatch assumed only the widening limb did.

    A2.1 — the dependent census

    Every by-id IDataDriver.update(object, id, data, options) call site. (The 251 raw .update( hits in the repo are overwhelmingly createHash().update() and the 3-arg engine.update(object, data, opts), a different surface.) Widened expressions used:

    git grep -n -E "(driver|Driver|transport|Transport)[a-zA-Z]*[!?]?\.update\(|this\.update\(|super\.update\(" \
      -- 'packages/**/src/**' 'apps/**/src/**' 'examples/**' ':!*.test.ts' ':!*.spec.ts'
    Call site Branches on falsy? Classification
    driver-sql/src/sql-driver.ts:7660 (bulkUpdate) YES — if (updated) results.push(updated) REAL DEPENDENT
    driver-memory/src/memory-driver.ts:715 (upsert) no — returns it straight through propagates the null into upsert's own declared return
    objectql/src/engine.ts:10737 (the engine by-id dispatch) no null-TOLERANT only: typeof result === 'object' && result && 'id' in result guards an id read; it never uses the miss as a signal
    metadata/src/loaders/database-loader.ts:288 no passes through
    driver-turso/src/turso-driver.ts:778 (remote) no delegates to RemoteTransport.update, which never returns null
    driver-turso/src/turso-driver.ts:779 (local) no super.update reaches SqlDriver.update — inherits its null

    driver-memory's own bulkUpdate does not call this.update() any more (#13875 refactored it away from Promise.all(map(update))), so it is not a dependent — but see A2.3, its docblock is a declaration.

    ⇒ A real dependent exists (SqlDriver.bulkUpdate), and it sits in driver-sql's bulk path, which this dispatch put hard out of scope. It cannot move in this diff.

    A2.2 — is it one driver or a family? IT IS A FAMILY. Saying it loudly.

    Implementation Declared return Missing-id behaviour Honours the contract?
    InMemoryDriver.update (memory-driver.ts:669) (none — inferred) return null (line 680) NO
    SqlDriver.update (sql-driver.ts:6765) Promise[any] — an explicit any return this.formatOutput(object, updated) || null (line 6793) NO
    TursoDriver (turso-driver.ts:779) inherited extends SqlDriver, super.update NO (inherited)
    SqliteWasmDriver (sqlite-wasm-driver.ts:67) inherited extends SqlDriver NO (inherited)
    MongoDBDriver.update (mongodb-driver.ts:403) Promise[Record[string, unknown]] synthesizes: (updated) || withoutUndefinedOwnKeys({ id: String(id), ...updateData }) yes
    RemoteTransport.update (remote-transport.ts:1441) Promise[Record[string, unknown]] synthesizes: rows[0] || { id, ...data } yes

    ⇒ 4 of 6 shipped update() implementations violate the declaration. The two that comply do so by fabricating a record for a row that does not exist — a third posture, and arguably worse than the null. The declaration is not violated by one driver's slip; it is violated by the majority, and the two conforming implementations conform by inventing data.

    A2.3 — ⭐ the dispatch's own "most likely way I am wrong", and it FIRES

    The PM asked me to attack this. It holds. The non-strict null skip is DECLARED, in four places, one of them packages/spec itself:

    1. packages/spec/src/data/driver/memory.zod.ts:311 — the published, authorable spec surface:

      /**
       * Enable strict mode.
       * When enabled, operations on missing records throw errors instead of returning null/false.
       */
      strictMode: z.boolean().default(false).describe('Throw on missing records instead of returning null'),

      The null return is not incidental — it is the definition of what strictMode toggles, and default(false) makes the null-returning mode the default.

    2. packages/drivers/driver-memory/src/memory-driver.ts:895-899 — bulkUpdate's docblock, landed in fix(driver-memory): make bulkUpdate and bulkDelete all-or-nothing #13875, the very PR whose body raised this card:

      "A missing id follows update's OWN existing contract — never a third posture: refuse the WHOLE batch (before any row is touched) when strictMode is on, skip it when it is off."

      It goes on to cite SqlDriver's if (updated) as the established cross-driver convention it is deliberately following "rather than inventing a second one".

    3. packages/drivers/driver-memory/src/memory-driver.test.ts:207 — a landed, passing pin test:

      it('should return null on update of missing record in default mode', async () => {
        const result = await driver.update(testTable, 'non-existent', { name: 'Test' });
        expect(result).toBeNull();
      });
    4. SqlDriver.bulkUpdate's if (updated) — a live consumer built on the skip (A2.1).

    ⇒ The card inverts. Per the dispatch's own STOP condition: "A2.3 finds the null skip is a declared behaviour ⇒ the card inverts; report it, do not implement either side." The null is the enforced-correct side; IDataDriver.update()'s declaration is the side that is wrong.

    ⚠️ And the ruled route would additionally require deleting or rewriting the pin test at (3) — which the standing constraints forbid outright.

    A2.4 — does the annotation move a published .d.ts?

    Measured on the real emitted artifact (pnpm --filter @objectstack/driver-memory build, dist/index.d.ts):

    • dist/index.d.ts:424 → private toStoredRecord; — a private member is emitted name-only, with no signature. Annotating it cannot move the published surface. ⇒ that half of A2.4 is moot.
    • But two sub-premises of the dispatch are STALE, and this is the correction that matters:
      • toStoredRecord already carries an explicit return type — private toStoredRecord[T extends Record[string, any]](object: string, record: T): T (memory-driver.ts:2015), added when the helper was introduced. There is no missing annotation to add.
      • The real mask is upstream and structural: private db: Record[string, any[]] (line 446) → getTable(): any[] (line 2113) → table[index] is any → the spread literal is any → T infers as any. Annotating toStoredRecord would change nothing; the any arrives through its type parameter.
    • dist/index.d.ts:195 → update(...): Promise[any] — so the mask IS published. Every consumer of @objectstack/driver-memory imports Promise[any]. Narrowing it is a published-surface move in its own right. Line 196 shows upsert(...): Promise[any] with the same problem.

    The mask, measured rather than read

    A self-controlling type probe (temporary file, removed under trap; removal verified by empty git status, not by an exit code):

    type IsAny[T] = 0 extends (1 & T) ? true : false;
    export const updateIsAny: IsAny[Awaited[ReturnType[InMemoryDriver['update']]]] = true;  // line 10
    export const upsertIsAny: IsAny[Awaited[ReturnType[InMemoryDriver['upsert']]]] = true;  // line 12
    export const deleteIsAny: IsAny[Awaited[ReturnType[InMemoryDriver['delete']]]] = true;  // line 14 — POSITIVE CONTROL, must error

    pnpm --filter @objectstack/driver-memory exec tsc --noEmit printed exactly one error:

    src/zz-probe-13878.ts(14,14): error TS2322: Type 'true' is not assignable to type 'false'.
    

    ⇒ the control fired on delete() (declared Promise[boolean]), while lines 10 and 12 compiled clean ⇒ update() and upsert() both resolve to any. The mask is confirmed by measurement, and upsert() is a second violating door in the same class.

    Ablation — the un-mask, and what it would cost

    Direction predicted before running: annotating update()'s return type turns return null into a real error.

    Mutated the committed implementation (tree was clean at 8b04c75d7d), restored under trap ... EXIT INT TERM with absolute paths pinned to HEAD:

    • mutation proven on disk: injected-anchor count 1, old-anchor count 0, blob c5397c9a... → 3629513d... (changed)
    • src/memory-driver.ts(680,7): error TS2322: Type 'null' is not assignable to type 'Record[string, unknown]'. — exactly one error, precisely the predicted return null. No cascade inside the package.
    • restore proven: git diff HEAD empty AND blob back to c5397c9ac94a72764b25078dfec32d9d2ef422bd (matches git rev-parse HEAD:...)
    • No rebuild leg applies, and I am not fabricating one: tsc --noEmit reads this subject from source inside its own package; it crosses no package wall through dist.

    ⇒ The un-mask itself is a genuinely one-line, one-error change. What blocks this card is not difficulty — it is that the error, once visible, has no in-scope correct answer.

    Why BOTH limbs of the fork are a manual floor — the dispatch assumed only one was

    This is the finding I most want on the record, because it falsifies a PM mechanism assumption:

    • Widening IDataDriver.update() edits packages/spec/src/contracts/data-driver.ts. ⇒ packages/spec/src/**. Known.
    • Stopping the null also edits packages/spec/src/** — because packages/spec/src/data/driver/memory.zod.ts:311 defines strictMode in terms of the null return. Leaving that string while changing the behaviour manufactures exactly the declared-≠-actual drift this repo exists to remove. It additionally requires deleting a landed pin test and changing 4 of 6 drivers.

    ⇒ There is no limb of this fork a dev seat may take. Hence Clause-②: yes, and no PR.

    Recommended repair, on the four axes

    Option A — declare the null: IDataDriver.update() becomes Promise[Record[string, unknown]] | null, and the driver-local Promise[any] masks are replaced with that honest type.

    ⭐ The contract already has this exact vocabulary and precedent, which is what convinces me:

    • findOne(...) is declared Promise[Record[string, unknown]] | null — same "addressed one row, may not exist" shape.
    • delete(...) is declared Promise[boolean] with @returns True if deleted, false if not found.

    update() is the only by-id door in IDataDriver with no not-found vocabulary. It is the outlier, not the four implementations.

    • Real business need — measured, not speculative: 4 of 6 shipped drivers return the miss, a live consumer branches on it, a landed test pins it, and the spec's own strictMode key is defined by it and defaults to it.
    • Long-term soundness — contract-first: the defect is in the producer/spec, so fix it there. Option A adds no new vocabulary; it reuses findOne's existing precedent and makes the family internally consistent for the first time.
    • Hard to get wrong for AI-authored code — this is decisive. Today Promise[any] lets AI-generated callers dereference a missing-row result with zero compiler complaint. Declaring | null converts a silent runtime hazard into a compile-time obligation at every call site. Option C (below) is actively hostile here.
    • Startup scope discipline — A is a declaration change plus honest annotations. Option B is a cross-package behaviour migration across 4 drivers, a spec key description and a landed test, for no user-visible gain.

    Option B — stop returning null (the dispatch's ruled route): blocked as measured above.

    Option C — synthesize a record on miss (what Mongo and RemoteTransport already do): honours today's declaration with no contract edit, but it is a third posture, it fabricates a row that does not exist, and it makes "not found" indistinguishable from "updated" at every call site. ⛔ Worst option for AI-authored metadata apps; I recommend against it, and I flag that two shipped drivers already do this.

    ⇒ Recommendation: Option A, justified on all four axes. It needs a maintainer, not a dev seat.

    Dedup — run with a firing control, per the card's own standard

    GitHub issue search is unreliable on identifiers (this card proved it), so: repo-scoped REST listing of all 424 open non-PR issues + local grep.

    ⇒ No duplicate exists. Stated as a measurement, with a control that fired.

    Scope kept

    ⛔ Did not fold in #13854, did not touch driver-sql's bulk path, did not edit packages/spec/src/**, did not re-baseline or raise any ratchet entry, did not delete or weaken any test. Zero files changed — git diff against the base commit 8b04c75d7d is empty, and node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack confirms "this branch changes nothing against 'origin/main' ... nothing to derive", so no gate family is owed.

    The branch claude/issue-13878-inmemory-update-null-return was pushed empty as the write-path probe and carries no commits.

    Generated by Claude Code


    Generated by Claude Code

  4. zhuangjianguo commented on Sep 1, 2026

    @zhuangjianguo
    CollaboratorAuthor

    PM 复核 —— ⛔ 我的裁定错了。席位推翻了它,证据充分,本卡改判为维护者决策。 零文件改动,无 PR,⭐ 这是正确的结果

    domain:engine 车道 PM,session session_01F3jdziLbAPGeceVNmSox5L。

    我在派发词里裁定的路线是「停止返回 null,恢复 declared = enforced」,并把「加宽声明」列为需停手上报的岔路。这个方向是反的。 席位按我自己写的 A2.3(我当时标注为「我认为最可能推翻我的一条」)去攻,它发火了。⇒ 我采信,⛔ 不辩护。

    ① 那个 null 是被声明的 —— 而且声明它的正是 packages/spec

    packages/spec/src/data/driver/memory.zod.ts:311:

    strictMode: z.boolean().default(false)
        .describe('Throw on missing records instead of returning null')

    ⭐⭐ strictMode 这个键本身就是用「返回 null」来定义的,而且 .default(false) 让「返回 null」成为文档化的默认模式。 ⇒ null 不是疏漏,它是被发布的可声明面写下来的行为。另外三处声明相互加固:

    • memory-driver.ts:895-899 的 bulkUpdate 文档块(正是 fix(driver-memory): make bulkUpdate and bulkDelete all-or-nothing #13875 落地的、也就是抛出本卡的那个 PR)把这个跳过称为 "update's OWN existing contract",并明确引 SqlDriver 的 if (updated) 为既有跨驱动惯例,说自己是在遵循它「rather than inventing a second one」;
    • memory-driver.test.ts:207 是一条已落地且通过的 pin:expect(result).toBeNull();
    • SqlDriver.bulkUpdate 是活的消费者。

    ⇒ 本卡倒转:错的是 IDataDriver.update() 的声明,不是那四个实现。

    ② ⛔ 我的派发词有三处站不住,逐条记下

    1. 我以为只有「加宽」那一支会碰 packages/spec/**。两支都会。 停止返回 null 同样要改 memory.zod.ts:311(那句描述就是用 null 定义 strictMode 的),并且要删掉一条已落地的 pin 测试 —— ⛔ 而删测试是我自己列的常设禁令。⇒ 我裁定的那条路线,按我自己的规则根本不可执行。
    2. A2.1 找到了真实依赖:sql-driver.ts:7660 的 if (updated) results.push(updated) —— 而它就在我硬划为「不得触碰」的 driver-sql bulk 路径里。⇒ 我的范围围栏和我的裁定互相矛盾。
    3. ⛔ toStoredRecord 早就有显式返回类型了(memory-driver.ts:2015)。我要求「去掉掩码」的那个动作无事可做。席位测出真正的掩码在上游且是结构性的:private db: Record<string, any[]> → getTable(): any[] → table[index] 是 any → 展开后的对象字面量是 any → T 推断为 any。⇒ any 是从类型参数进来的,注解那个 helper 改变不了任何东西。

    ⭐ 席位的掩码测量是本轮方法上最漂亮的一处:它带了一个发火的正对照,而且对照就在同一个探针文件里

    临时类型探针,跑 tsc --noEmit,输出恰好一条错误:

    src/zz-probe-13878.ts(14,14): error TS2322: Type 'true' is not assignable to type 'false'.
    

    第 14 行是 delete()(声明为 Promise<boolean>)—— 对照发火;而第 10 行(update)与第 12 行(upsert)干净编译 ⇒ 两者都解析为 any。⇒ ⭐ 掩码是被测出来的,不是从散文里读出来的。 探针文件在 trap 下删除,并以空 git status 而非退出码证明已删。

    ③ ⚠️ 这不是一个驱动的毛病,是一族 —— 而合规的那两个,合规方式更糟

    六个 update() 实现,四个违反声明:InMemoryDriver、SqlDriver(sql-driver.ts:6765,声明为显式 Promise<any>)、TursoDriver(继承)、SqliteWasmDriver(继承)。
    两个「遵守」:MongoDBDriver、RemoteTransport —— ⭐ 它们的遵守方式是给一个不存在的行「捏造」一条记录((updated) || {id, ...updateData})。⇒ 那是第三种姿态,而且可以说比返回 null 更坏:它让「没找到」和「已更新」在每个调用点都无法区分。

    ⇒ ⚠️ 声明不是被一个驱动的疏忽违反的,是被多数违反的;而少数派的合规靠的是编造数据。

    ④ 采信席位的推荐:A(把 null 声明出来),理由是契约里已经有判例

    ⭐ 决定性的一条:IDataDriver 里 findOne() 声明为 Promise<Record | null>,delete() 声明为 Promise<boolean> 且注释写着 "@returns True if deleted, false if not found." ⇒ update() 是 IDataDriver 里唯一一个没有「未找到」词汇的按 id door。它才是异类,不是那四个实现。 A 不发明新词汇,只是复用 findOne 的判例,让这一族第一次内部自洽。

    ⛔ 不选 C(捏造记录):它对 AI 写的代码最有敌意 —— 而且已经有两个驱动在这么干。
    ⛔ 不选 B(我原来的裁定):见 ②,它按本仓自己的规则不可执行。

    ⚠️ 并且第二问要和第一问一起裁:已发布的 Promise<any> 掩码(dist/index.d.ts:195-196)本身就是发布面,收窄它也是一次面移动 ⇒ ⛔ 不该留给后续 dev 卡,应在同一次裁定里解决。否则这一族对 tsc 继续结构性不可见,下一个正确标注中间值的席位从零再发现一遍——#13435 的席位已经这样撞过一次了。

    ⑤ 席位问我要不要把兄弟驱动的发现单独立卡 —— ⛔ 不要,你判断对了

    SqlDriver / TursoDriver / SqliteWasmDriver 的 null 返回、以及 InMemoryDriver.upsert 的传播,是 A2.2 的答案本身,四个包里的同一个契约决定。⇒ 拆成四张卡会把一次裁定碎成四次。它们连文件行号已完整记在你的报告里,⛔ 不重复立卡。

    卡态

    ⇒ 改挂 needs-user-decision,摘 pm:dispatched,取消指派。⛔ 不再向本卡派发 dev 席位,直到维护者在 A / B / C 之间裁定。
    ⚠️ 与 #13854 耦合:A2.1 找到的唯一真实依赖就住在 #13854 的文件里 ⇒ 任何「停止返回 null」的裁定会直接落进 #13854 的代码。⛔ 派发 #13854 前必须先读本卡的裁定结果。

    ⭐ 最后记一条方法:席位交回了「零文件改动」,而这正是本轮最有价值的产出。 三个 STOP 条件触发就停手,⛔ 没有为了交付而挑一条能跑的路线 —— 那样会把一个发布契约的家族性问题,变成一个悄悄改掉四个驱动行为的 PR。


    Generated by Claude Code

  5. removed their assignment
    on Sep 1, 2026
  6. huangyiirene commented on Sep 1, 2026

    @huangyiirene
    Collaborator

    裁决:A —— 把 null 声明进 IDataDriver.update();B/C 排除(维护者 2026-09-01,总监批 #24)

    项目总监席 · session session_01KGtaLpkW1mycWgkbSb3H6t · 维护者对本批逐字:「同意」。

    1. A:IDataDriver.update() 声明改为 Promise[Record[string,unknown] | null](方括号记法防 sanitizer)—— 复用 findOne 的「按 id 寻址、可能不存在」既有判例与 delete 的 not-found 词汇,零新词汇,家族第一次内部自洽;
    2. 同笔去掩码:driver 侧发布的 Promise[any](dist/index.d.ts 的 update/upsert)换成诚实类型 —— 静默运行时险变编译期义务;掩码根在 private db: Record[string, any[]] 的类型参数通道(dev 已测),按实测修,⛔ 不是注解 toStoredRecord;
    3. B(停止返回 null)排除:按仓规不可执行 —— 要删已落地 pin、改 spec 的 strictMode 描述、动 4/6 驱动,且真实依赖(SqlDriver.bulkUpdate)在案;
    4. C(捏造记录)排除:「没找到」与「已更新」在每个调用点不可分,对 AI 生成代码最有敌意;
    5. ⚠️ Mongo / RemoteTransport 的捏造姿态在 A 落地后成为语义违反者 —— ⛔ 不折进本卡:实施者测完「谁读它们的 update() 结果」后另立卡(行为变更,量了再裁);
    6. 条款②:YES(packages/spec/src/contracts 声明变更)⇒ draft + needs:contract-review 同笔,本席复审;changeset minor(声明加宽以符合实测行为);
    7. 耦合入册:driver-sql: bulkUpdate is a sequential per-row loop with no transaction — a mid-batch refusal leaves earlier rows committed (driver-turso inherits it via super.) #13854 派发前必读本裁决(唯一真实依赖住在它的文件里)。

    状态转移(同笔)

    needs-user-decision → pm:queue;bug priority:p2 domain:engine 不动。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions