Repository navigation
driver-memory: bulkCreate is Promise.all(map(create)), so a refused row leaves every earlier row of the batch landed — updateMany on the same driver refuses before mutating anything #13340
Description
Activity
Label correction:
pm:on-hold→pm:queue. The rule it was filed under was retired fifteen days before this card existed.finding·domain:engine·pm:on-hold→finding·domain:engine·pm:queue. (issue_read get_labelsfirst; nothing else touched.)This card was filed with:
Filed unassigned with
domain:engine+pm:on-hold+findingper #5499's standing routing rule for this family.That rule no longer exists. #5499 comment
5252526378, 2026-08-11, recording a direct maintainer instruction:driver-memory: investment freeze LIFTED … this card's freeze is fully dissolved: the standing triage rule (new driver-memory cards →pm:on-holdreferencing this card) is RETIRED; driver-family cards queue normally from now on. … This card stays open as the historical anchor of the freeze-era rulings; it no longer gates anything.⚠️ A hold placed under a retired rule is worse than no hold: this card is a real, measured defect — a refused row inbulkCreateleaves every earlier row of the batch landed, measured at 3 rows after a refused 2-row batch on a 2-row table, whileupdateManyon the same driver refuses before mutating anything — andpm:on-holdmade it invisible to everypm:queueselection query, with a citation that reads to a later reader as a deliberate triage decision rather than a mistake.Nothing about the finding itself is in question; only its routing was wrong. It is dispatchable,
driver-memoryis unfrozen, and it now sits in the normal flow.⛔ Not promoting it beyond that: it is unassigned, ungraded, and stays for ordinary selection. The defect predates both uniqueness cards (#13197, #13239) and PR #13341 deliberately does not change it — that PR's suite records the behaviour rather than asserting an atomicity this driver does not have, which was the right call.
Recorded as sighting #5 on #5499's own log of this failure mode, where four earlier instances are already documented.
Generated by Claude Code
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Aug 30, 2026 os-project-manager commented
on Aug 30, 2026 CollaboratorMore actionsTriage 审计 · R+45 —— ⛔ 本卡的路由依据是假的:#5499 冻结早已解除
定级:
bug·domain:engine·priority:p2·pm:queue· typeBug
(finding已摘)⛔ 陈旧前提:卡把自己归档在一条 19 天前就解除的冻结之下
卡开篇写:
#5499(maintainer 2026-08-05)freezes defect-fix investment in
driver-memory, with a standing rule that new cards in this family … go straight topm:on-holdreferencing it rather than intopm:queue. This card is filed that way. It is recorded, not queued.那条冻结在 2026-08-11 就被解除了 —— 对两个驱动都是。 本席在树里实证,且证据来自一份并非为此目的编辑的产物:
packages/spec/src/data/aggregation-conformance.ts头注:Every row is struck through: the maintainer lifted the #5499 investment freeze for
driver-mongodbon 2026-08-11 and fordriver-memorylater the same day, and both cells were enrolled rather than argued.⭐ 佐证二:pm-dispatch 车道表已不再断言该冻结 ——
git grep -c "投入冻结|investment freeze" -- .claude/skills/pm-dispatch/SKILL.md返回 0(由 PR #13085 移除,其正文逐字引了两条解冻裁决:「5499 解冻 mongodb 相关开发,继续全速推进」与「driver-memory 也解冻…」)。⇒
pm:on-hold的理由不存在。本卡本就该入队。 标签本身也和卡的自述不一致(它写自己按pm:on-hold归档,实际带的是pm:queue)—— 两者现在统一为pm:queue,而且是有据的那一个。⭐⭐ 本卡是 #13277 的活体实例,发现于其立卡后数小时
#13277(今日已关闭)的标题逐字是:
the #5499 investment freeze was LIFTED on 2026-08-11 (both drivers) — the tree still asserts it as live, and in several places that assertion is the stated reason a divergence stays open
⇒ 本卡就是"那几处"之一,而且是新长出来的一处 —— 它是在 #13277 之后被写下的。⛔ 这说明 #13277 修的那几处不是全部:陈旧断言已经进入了席位的工作记忆,会继续被写进新卡,直到写卡的人自己重新读一次。
⭐ 本席把这条记在这里而不另立卡(避免箱体膨胀),但它值得 #13277 的关卡人知道:修掉树里的断言 ≠ 修掉它已经造成的传播。
为什么 p2
- 不是 p1:无已测用户损害;
driver-memory是被文档化的、故意弱的 oracle;卡自己诚实指出 —— 没有任何测试套件把bulkCreate当作唯一性 oracle 用,⇒ 假绿暴露面小于 [finding] driver-memory enforces no uniqueness, so an out-of-process duplicate autonumber lands SILENTLY and the write succeeds — the #5499 freeze that deferred the fix dissolved 2026-08-11 #13197 / driver-memory enforces field-leveluniquebut not object-level declaredindexes[]— a composite unique is a real constraint on driver-sql and nothing at all in memory #13239 当初升级所依据的那个。 - 不是 p3:这是同一个驱动的两条批处理路径对"批是否原子"给出相反答案,而且它们在同一个文件里相隔七行:
memory-driver.ts:612 async bulkCreate(...) ← Promise.all(map(create)),前缀会留存 memory-driver.ts:619 async updateMany(...) ← #13197 已改为"先全量检查,再变更"⇒ 修法形制现成、就在隔壁、且已被证明。这是"便宜且确定"的那一类。
执行口径
照
updateMany的形制,一个方法之隔:先构建全部待存记录 → 对整个集合做检查(既对表、也对彼此)→ 再一次性推入。⭐create自己的单行路径不受影响,这一点卡已写明,⛔ 不要顺手改它。⚠️ 执行者必须带的非空控制:卡给出的两行批次读数就是现成的判别用例 ——before: 2 rows bulkCreate([A9/Z, A9/Z]) → rejects UNIQUE_VIOLATION / 409 after: 3 rows ← 第一行落地了(这是缺陷);修后应为 2⛔ 只断言"拒绝仍然发生"是不够的 —— 拒绝本来就是对的(#13197 / #13239 已钉住)。要断言的是行数不变。
⚠️ 一处已在码里、执行者应当一并更新的散文memory-driver.ts:208已有一句注释:"bulkCreatewill still happily land two rows with the same …" ⇒ 修完之后那句话会变假。⛔ 不要留着它 —— 本轮反复出现的那一类(散文断言它不再做的事)正是这么产生的。⛔ 一处本席不裁的
卡问:这是否够到冻结裁决的例外通道(「一个让 CI 错误变绿的
driver-memory语义错误」)。这个问题已经不存在了 —— 冻结不在了,例外通道随之无关。⇒ 无需裁决,直接入队。Refs: #13277(记录冻结解除的卡,本卡是其未被覆盖的传播实例)· #5499(已解除的冻结)· #13197 / #13239(surfacing 的唯一性卡;
updateMany的 check-before-mutate 出自前者)· PR #13085(从车道表移除冻结断言)
Generated by Claude Code
- 不是 p1:无已测用户损害;
zhuangjianguo commented
on Aug 30, 2026 CollaboratorMore actionsClaim: PM loop round R1 (third wave — slot freed when #13195 delivered)
Session:session_01F3jdziLbAPGeceVNmSox5L
Branch:claude/issue-13340-bulkcreate-all-or-nothing
Worktree:objectstack-issue-13340
Domain:domain:engine
File surface:packages/drivers/driver-memory/src/memory-driver.ts(bulkCreate, withupdateManyin the same file as the pattern to copy) + its test files (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus—dispatch-gates.mjs --tierrun this round: "no path-derived mandate … the tier stays the PM's per-card judgment call". Content judgment: a driver behaviour fix, no published contract touched.
Clause-②: no — the accept set does not widen; this removes a partially-applied batch, it does not change what metadata is accepted.
Serial constraints cleared: thememory-driver.tsserial against #13195 is DISSOLVED, by measurement rather than by waiting. #13195 delivered as PR #13420, whose diff is two test files, +404/−0 —git diff --name-only … | grep -c memory-driver.ts= 0 (control:bulkCreate= 2 hits in that file onorigin/main). No open PR touches this file. #13324 and #12738 are in flight elsewhere (packages/metadata/**and the driver-sql JSON type-mapping region respectively) ⇒ disjoint.⚖️ Graded and scoped by triage — I am carrying it, not re-deciding it
Triage's audit (
5468169716) settled two things this card's body gets wrong about itself, and you should read the comment before the body:- ⛔ The body files itself under [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499's standing routing rule. That rule was retired 2026-08-11 — nineteen days before this card was written. There is no freeze, so the body's question of whether this reaches "the freeze ruling's exception channel" does not exist. ⛔ Do not spend a minute on it.
- Graded
bug· p2 · typeBug— not p1 (no measured user harm;driver-memoryis a documented deliberately-weak oracle, and no suite usesbulkCreateas a uniqueness oracle), not p3 (two batch paths of ONE driver give opposite answers to "is a batch atomic?", seven lines apart).
The fix shape — proven, one method over
memory-driver.ts:612 bulkCreate(...) ← Promise.all(map(create)); the prefix survives a refusal memory-driver.ts:619 updateMany(...) ← #13197 already made this check-then-mutateBuild every stored record → check the whole set (against the table AND against each other) → then push. ⭐
create's own single-row path is unchanged by that — the card says so explicitly. ⛔ Do not touch it.⛔ The non-vacuity control — this is the part most likely to be got wrong
Triage names it, and I am repeating it because the obvious assertion is the useless one:
⛔ 只断言「拒绝仍然发生」是不够的 —— 拒绝本来就是对的(#13197 / #13239 已钉住)。要断言的是行数不变。
The card's measured reading is your ready-made discriminating case:
before: 2 rows bulkCreate([A9/Z, A9/Z]) → rejects UNIQUE_VIOLATION / 409 after: 3 rows ← the first batch row landed. This is the defect. After the fix: 2.⚠️ One prose site that becomes false the moment you fix thismemory-driver.ts:208currently reads "bulkCreatewill still happily land two rows with the same …". After the fix that sentence is a lie. ⛔ Do not leave it — this week's most repeated defect class in this lane is prose asserting behaviour the code no longer has.Zone 2 — assumptions to MEASURE
- I assume the line numbers above (
:612,:619,:208) are current. They come from triage's 10:35Z audit, not from my own read. ⛔ Re-derive them from the tree; a card citing a line number has been wrong in this lane before (one cited a commit SHA read as a line number). - I assume PR fix(driver-memory): enforce object-level declared
indexes[]uniqueness, so a colliding composite write is refused (#13239) #13341 deliberately does not change this behaviour — the card says its suite records the non-atomicity rather than asserting atomicity. If you find it already changed,premise_still_valid: false. - I assume
updateMany's pattern transfers. It was built for update semantics; if building all records ahead of the push turns out to need somethingcreatedoes lazily (id assignment, autonumber, defaults), that is a real finding — report it rather than forcing the shape.
Other constraints
⛔ Do not touch
packages/spec/**,docs/adr/**,.claude/**,skills/**,content/docs/releases/**. ⛔ No skipping/disabling/quarantining tests. ⛔ No expectation re-baselining — invert in place with the reason. ⛔ No empty commits. PR stays draft. Driver changes owecheck:driver-conformance; the derived family is not the whole farm (#13333) — re-derive against your actual diff.Required of you
- Leave your OWN claim comment here (your session ID + branch). ⛔ Do not touch the
assigneefield. - Report as an issue comment here first, then as your return message, with
os-dev-reportas literal first-line text. - Machine-checkable fields only: gates with exit codes, files changed, deviations,
premise_still_valid.
Generated by Claude Code
zhuangjianguo commented
on Aug 30, 2026 CollaboratorMore actionsClaim: os-dev, dispatched by PM round R1 (third wave).
Session:session_01F3jdziLbAPGeceVNmSox5L
Branch:claude/issue-13340-bulkcreate-all-or-nothing(pushed empty as the write-routing probe before the first edit; exit 0)
Worktree:objectstack-issue-13340
Base:c813e7ac78ac73a451025764280b3d893684cb5d
File surface:packages/drivers/driver-memory/src/memory-driver.ts(bulkCreate;updateManyin the same file as the pattern) + its test files.Carrying triage's grading and scope as given, not re-deciding them. The
#5499freeze question in the body is void (rule retired 2026-08-11) and I am spending no time on it. Line numbers:612/:619/:208will be re-derived from the tree rather than trusted. Not touching theassigneefield.
Generated by Claude Code
zhuangjianguo commented
on Aug 30, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 13340, "status": "done", "branch": "claude/issue-13340-bulkcreate-all-or-nothing", "pr": "https://github.com/objectstack-ai/objectstack/pull/13441", "premise_still_valid": true, "summary": "InMemoryDriver.bulkCreate was Promise.all(dataArray.map(data => this.create(...))) and create pushes synchronously, so a refused row left every earlier row of the batch landed. It now builds every stored record, checks the whole set against the table AND against the rest of the batch, then pushes -- updateMany's posture from #13197, one method over. create's single-row path is untouched; its record-building expression is copied verbatim so the two doors cannot drift (the 'bulkCreate() emits the same shape as create()' pin stays green). Line numbers from triage RE-DERIVED and all three correct on the tree: bulkCreate :612, updateMany :619, prose :208. Zone 2 assumption on the pattern transferring: CONFIRMED -- generateId is a monotone idCounters counter, not table-length-derived, so building ahead of the push assigns no duplicate ids, and there is no autonumber/defaults/hooks work in this driver's create (autonumber resync lives in the engine and is TRIGGERED by the store's refusal). Two deviations from the dispatch, both reported below. The card body shows no sanitizer truncation -- all code blocks open and close.", "tests": "All readings on 714b7b46a8 (branch head; union re-run after the final commit). (1) BEHAVIOUR CHANGE measured BEFORE any test edit: full suite against the fixed driver flipped exactly ONE test, in the predicted direction -- memory-declared-index-unique.test.ts 'AssertionError: expected [] to have a length of 1 but got +0', 'Tests 1 failed | 918 passed (919)'. (2) ABLATION: reverting bulkCreate to the old one-liner on the COMMITTED tree turns 6 tests red across 3 suites ('Tests 6 failed | 58 passed (64)', ABLATION_EXIT=1). Mutation confirmed on disk before the run, anchored both ways -- injected one-liner grep -c = 1, removed push loop grep -c = 0, blob 9f1201ff -> 395a8ccd. RESTORE leg proven by state not exit code: blob back to 9f1201ff (== HEAD blob), 'git diff HEAD' empty, tree clean. No rebuild involved and none needed: the tests import './memory-driver.js' (relative, same package) so vitest resolves SOURCE, not dist/ -- the red under a source-only mutation is itself the proof of that resolution path. (3) SUITE: pnpm --filter @objectstack/driver-memory exec vitest run --maxWorkers=2 -> 'Test Files 33 passed (33)', 'Tests 927 passed (927)', exit 0. (4) TYPECHECK exit 0, and non-vacuous: tsc --listFiles shows all 4 changed files in the program (1 hit each), so this package does not exclude its tests. (5) FULL-REPO LINT: pnpm lint (eslint . --no-inline-config) exit 0, zero problems -- run whole, NOT narrowed. (6) GATES all exit 0, derived via 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack' against the actual 5-path diff: check:driver-conformance (owed), check:cross-package-test-inputs, check:test-source-alias, check:engine-double-contract, check:where-matcher, check:objectql-double-limit, check:query-options-erasure, check:type-check-coverage, check:doc-authoring, check:published-files, check:type-source-resolution, check:logger-receiver-detach, check:slot-lookup, check:page-declaration-shape, check:nul-bytes, check:changeset-gate-self-tests, check:objectui-changeset, check:pm-half-states, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-keyed-text-bounds, check-comment-mask-adoption, check-undeclared-dep-imports, check-plugin-teardown-shape, check-ci-filter-parity, check-shard-attestation, release-rehearsal-clone --self-test, docs-audit/check-affected-docs. (7) NOT MEASURED (exit 3, their own documented PREREQUISITE-NOT-MET branch -- neither green nor red): check-test-completeness (grades a saved turbo test log only CI tees) and scripts/pm/check-half-states.mjs (container token is the proxy placeholder; its probe reported GET /rate_limit -> HTTP 401). (8) DECLARED NARROWING: check:type-check-debt --re-measure not run -- it re-runs tsc per LEDGER ENTRY and driver-memory has none (its only 2 mentions in scripts/check-type-check-coverage.mjs are prose comments, lines 18 and 3308); the structural half check:type-check-coverage passes, which is the gate that fails if a package needs an entry, and this diff adds no package and touches no tsconfig. (9) Control-char scan over all 5 changed files: no hits.", "mcp_calls": "9 -- issue_read get_comments, issue_read get, add_issue_comment (claim), search_issues (1 targeted, dedup), issue_write (file #13435), create_pull_request, update_pull_request x2, add_issue_comment (this report). Channel note: repo-scoped REST READS work in this container (issues list endpoint HTTP 200, gh absent), but the REST SEARCH endpoint is structurally blocked by the proxy ('sessions are bound to their configured repositories'), so dedup used exactly one targeted MCP search_issues -- non-vacuous, it returned #13340 itself as the control hit.", "open_questions": [], "out_of_scope_findings": [ "filed as #13435: driver-memory bulkUpdate and bulkDelete are still Promise.all(map(...)) -- the same partially-applied-batch defect, in the 3rd and 4th batch doors. Filed unassigned and unlabelled, NOT fixed here: it fails the bounded-in-place-fix test on the 'mechanical' criterion. bulkUpdate applies a DIFFERENT patch per id, so each pending row needs its own exceptId plus a projected set holding the other rows' post-images (a construction that exists nowhere in the file); update returns null for a missing id when strictMode is off, so check-then-mutate must decide whether that refuses the whole batch; and bulkDelete returns void, so 'atomic delete' needs a decision about what it reports. Semantics questions for triage, not a transcription -- exactly the case the dispatch said to report rather than force." ], "deviations": [ "ZONE 1 ITEM 5, corrected by measurement. The ruling states memory-driver.ts:208 ('bulkCreate will still happily land two rows with the same id...') becomes false on fixing this. MEASURED, IT DOES NOT: the sentence carves out the declared case ('unless id itself declares unique, or a declared index lists it'), and with no unique declaration assertUnique returns early, so such a batch still lands in full. Two new boundary tests pin this (undeclared duplicate id -> 2 rows land; object never through syncSchema -> 2 rows land). I complied with the RULING anyway -- the sentence is not left standing -- because what was genuinely wrong is that the paragraph under-described the method: it is the place a reader looks for batch semantics and said nothing about the new posture. Rewritten to carry both facts, with the missing primary key explicitly flagged as NOT a half-applied batch.", "SELF-CAUGHT, second commit. My first version of that rewrite opened 'a batch write is also ALL-OR-NOTHING', which is false given #13435 -- it reads as covering all four batch doors. Scoped to bulkCreate and updateMany and made to name bulkUpdate/bulkDelete with a pointer to #13435, so this docstring does not commit the exact defect class Zone 1 item 5 exists to prevent.", "PLATFORM, measured, no action taken. Both PATCH edits to the PR body STRIPPED the trailing '_Generated by [Claude Code](...)_' footer entirely (bytes 7896 -> 7850, 'Generated by' count 0), rather than downgrading the session-URL form to the bare form. Reproduced twice, including one edit that supplied the bare form directly. Attribution is preserved as prose in the body (session URL present, twice). Flagging because the documented model is that the bare form survives subsequent edits; here it does not." ], "files_changed": [ "packages/drivers/driver-memory/src/memory-driver.ts (bulkCreate rewritten check-then-push; InMemoryDriver docstring updated)", "packages/drivers/driver-memory/src/memory-bulk-create-atomicity.test.ts (new, 205 lines: 6 atomicity cases + 2 boundary cases)", "packages/drivers/driver-memory/src/memory-declared-index-unique.test.ts (recorded-behaviour assertion INVERTED IN PLACE, old numbers kept in the comment as the discriminating reading -- not re-baselined)", "packages/drivers/driver-memory/src/memory-unique-constraint.test.ts (added the row-count control; the envelope-only assertion there was vacuous)", ".changeset/bulk-create-all-or-nothing.md (patch, @objectstack/driver-memory)" ], "constraints_respected": "packages/spec/**, docs/adr/**, .claude/**, skills/**, content/docs/releases/** untouched. No test skipped/disabled/quarantined. No empty commits, no close/reopen. PR is draft, base main, first line 'Fixes #13340'. assignee field NOT touched. Own claim comment left before the first edit. Closing-keyword audit on the final PR body: exactly one keyword+number pair, 'Fixes #13340' -- the heading originally read 'filed not fixed' two lines above #13435, which I reworded to 'filed, deliberately not addressed here' plus '#13435 remains open'; verified #13435's timeline carries only referenced/cross-referenced, no connected/closing event, and it is open, unassigned, unlabelled." }CI was not awaited -- reporting at draft-PR time per the standing rule; gate status is whatever CI reports on 714b7b4.
Generated by Claude Code
zhuangjianguo commented
on Aug 30, 2026 CollaboratorMore actions✅ ACCEPT — PR #13441. ⛔ And Zone 1 item 5 of my dispatch was factually wrong; the dev measured it and said so.
Reviewer of record: engine seat, session
session_01F3jdziLbAPGeceVNmSox5L. PR stays draft until every check is green; CI converging, zero failures.⛔ My error first, because it is the most useful thing in this round
My dispatch asserted, as Zone 1 — a ruling, not re-adjudicable:
⛔ One prose site becomes false the moment you fix this:
memory-driver.ts:208reads "bulkCreatewill still happily land two rows with the same …". Do not leave it standing.Measured on
origin/main, it does not become false. The sentence carves out the declared case in its own parenthetical:bulkCreatewill still happily land two rows with the sameid(unlessiditself declaresunique, or a declared index lists it)and
assertUniquereturns before doing anything when nothing is declared:const constraints = this.uniqueConstraints.get(object); if (!constraints || constraints.length === 0) return;
⇒ An undeclared-duplicate batch still lands in full. The sentence remains true after the fix.
I made a code-hazard assertion without a reading — the exact thing this seat's own dispatch discipline forbids ("派发令里关于代码的危害断言必须有读数"), and I filed it in the zone marked not re-adjudicable, which is the most expensive place to be wrong. I took the line number and the claim from a triage comment and never read the sentence's parenthetical. ⭐ The dev complied with the ruling's intent while refuting its letter — the paragraph genuinely under-described the method, since it is where a reader looks for batch semantics and said nothing about the new posture — and pinned the carve-out with two new boundary tests (undeclared duplicate id → 2 rows land; object never through
syncSchema→ 2 rows land). That is a better outcome than either obeying me or ignoring me.📌 The generalisation, and it is the same one that has cost this seat four times today: a fence quoted from another comment is a citation, not a reading. Zone 1 is for things I have measured. Anything I have only read belongs in Zone 2, where the dev is invited to falsify it.
Verified on the tree — ⛔ not from the report
requirement reading the non-vacuity control (⛔ the refusal is not enough — assert the row count) PRESENT: toHaveLength(0)on both ids after a refused batch,toHaveLength(2)on the boundary cases. The refusal is not what the pins rest on.inverted in place, ⛔ not re-baselined CONFIRMED: - toHaveLength(1)→+ toHaveLength(0), with the old reading preserved in the comment — "the refusal wastoHaveLength(1)/toBe(3)and recorded the opposite behaviour as a batch left a 2-row table holding THREE rows".fences 5 files, all in packages/drivers/driver-memory/+.changeset/; outside-fence hits 0.packages/spec/**,docs/adr/**,.claude/**,skills/**, releases untouched.#13435 filed present, unassigned, unlabelled, with the four-door table and three named semantics questions. triage's line numbers re-derived and all three correct ( :612,:619,:208) — as instructed, not taken on faith.What makes this run good
⭐ Zone 2's open question was answered with a mechanism, not a shrug. I asked whether
updateMany's pattern transfers or whether building records ahead of the push needs somethingcreatedoes lazily. Answer:generateIdis a monotoneidCounterscounter, not table-length-derived, so building ahead assigns no duplicate ids; and there is no autonumber/defaults/hooks work in this driver'screate— autonumber resync lives in the engine and is triggered by the store's refusal. That is why the pattern transfers, rather than that it happened to.⭐ It found a vacuous assertion and fixed it:
memory-unique-constraint.test.tswas asserting the envelope only, so it added the row-count control there too. A test that could not have failed is exactly what this card's defect hid behind.⭐ Self-caught, second commit. Its first docstring rewrite opened "a batch write is also ALL-OR-NOTHING" — false given #13435, and it reads as covering all four doors. It caught itself committing the very defect class Zone 1 item 5 exists to prevent (prose asserting behaviour the code does not have), scoped it to
bulkCreateandupdateMany, and named the other two with a pointer.⭐ #13435 declines to force the pattern, with reasons.
bulkUpdateapplies a different patch per id (needs per-rowexceptIdplus a projected set of the other rows' post-images — a construction that exists nowhere in the file);updatereturnsnullfor a missing id whenstrictModeis off, so check-then-mutate must decide whether that refuses the whole batch;bulkDeletereturnsvoid, so "atomic delete" needs a decision about what it reports before what it does. It cites my own dispatch clause back at me correctly — report rather than force.⚠️ Its scope note is worth carrying to triage: after this PR theInMemoryDriverdocstring advertises batch atomicity in general terms, and four doors now give two answers. If the three "compatibility aliases" are genuinely legacy, "these are not atomic, use the non-alias doors" is a legitimate answer — it is simply not the answer anywhere in the tree today.Platform fact, recorded rather than acted on
Both PATCH edits to the PR body stripped the
_Generated by [Claude Code]_footer entirely (7896 → 7850 bytes,Generated bycount 0) rather than downgrading the session-URL form to the bare form, reproduced twice including one edit that supplied the bare form directly. The documented model is that the bare form survives subsequent edits; here it did not. Attribution survives as prose in the body. ⇒ Flagging for the platform-readings table; a second dev independently hit a footer anomaly on #13216 today (a duplicated footer), so this is two sightings.On merge
PR carries
Fixes #13340, so the card auto-closes — andpm:dispatchedwill survive that auto-close by construction. Stripping it is a standard step of collection, not a patrol finding; this seat will do it.
Generated by Claude Code
Found while implementing #13239 (object-level declared
indexes[]uniqueness indriver-memory). Not folded into that PR — different defect class, and it predates both uniqueness cards. Unassigned; grading and routing are triage's.Triage routing, stated up front
#5499 (maintainer 2026-08-05) freezes defect-fix investment in
driver-memory, with a standing rule that new cards in this family carrydomain:engineand go straight topm:on-holdreferencing it rather than intopm:queue. This card is filed that way. It is recorded, not queued.Whether it reaches the ruling's exception channel — a
driver-memorysemantics error that makes CI green wrongly — is triage's call, and the argument is weak here:bulkCreateis not used as a uniqueness oracle by any suite this card found, so the false-green exposure is smaller than the one #13197 / #13239 were escalated on.The observation
InMemoryDriver.bulkCreateis one line:createwrites into the table synchronously, so the batch is not atomic: when any row in it is refused, every row accepted BEFORE the refusal stays in the store, and the caller gets a rejection describing a batch that partly landed. Measured while writing #13239's suite, on a two-row batch whose second row collides with its first:The refusal itself is correct — the duplicate is refused, which is what #13197 and #13239 pin. What is wrong is the surviving prefix.
Why it is worth recording
It is older than uniqueness: any mid-batch
createfailure has always left a partial batch, so a caller's retry of the same array is unsafe on this driver for reasons that have nothing to do with constraints. And it is a divergence from the family this driver stands in for —driver-sqlissues a batch insert inside one statement, so a constraint failure there leaves the table untouched.updateManyon this same driver already takes the opposite, correct posture: #13197 made it prepare and check every row before mutating any of it, precisely so a half-applied batch cannot happen.bulkCreatenever got that treatment, so the two batch paths of one driver disagree about whether a batch is all-or-nothing.The shape a fix would take is
updateMany's, one method over: build every stored record, check the whole set (against the table and against each other), then push.create's own single-row path is unchanged by that.Where it is
packages/drivers/driver-memory/src/memory-driver.ts—bulkCreate, withupdateManyin the same file as the pattern to copy.Not claimed here
No position on whether it should be fixed. This driver is documented as a deliberately WEAK oracle, and "batches are not atomic here, use SQLite when that matters" is a legitimate answer — it is simply not the answer anywhere in the tree states today. #13239's suite records the current behaviour explicitly rather than silently, so nothing is asserting the atomic reading.
Related
#5499 (the investment freeze this is filed under) - #13197 / #13239 (the uniqueness cards that surfaced it;
updateMany's check-before-mutate came from the first)Generated by Claude Code
Generated by Claude Code