Uh oh!
There was an error while loading. Please reload this page.
fix(cli): a raw-SQL seam that cannot answer is absent, not empty (#10677) - #10788
Conversation
) `os migrate duplicates --database-url memory://qa` exited 0 with `duplicates:[]`, `skipped:[]`, `counters.status:"read"` — the exact false all-clear the #8928 ruling's `no_sql_seam` refusal exists to prevent. That refusal was dead code for the memory driver. `InMemoryDriver.execute()` logs `Raw execution not supported in InMemory driver` and returns `null`: it neither throws nor is absent. The seam resolver asks whether the driver has the SHAPE of a seam (`typeof d.execute === 'function'`), which that satisfies, so the `if (!exec)` guard never fired — and `normalizeRows(null)` is `[]`, which is also what a real driver returns for a SELECT that matched nothing. The guard now keys on the distinction that actually separates the two: a seam that ANSWERS returns a result set, and one that cannot answer returns no result set at all. One trivial statement is put to the resolved seam before the scan starts, and every individual probe is held to the same standard, so a probe that returns no result set becomes a `skipped` entry with its reason instead of zero findings. No driver package is touched — the 2026-08-05 investment freeze covers the driver-memory/driver-mongodb family, so this is the consumer-side route triage directed. A seam that THROWS is deliberately left alone: that is a driver present and refusing loudly, already reported honestly by the per-probe `skipped` path, and claiming it here would invent a refusal #8928 never mandated. The clause-7 knownGap in the QA checklist said the memory driver exposes no raw-SQL seam. It exposes a no-op one; corrected, with the drifted duplicates.ts line citations re-derived. The mongodb branch is left explicitly unverified — that driver was not loaded for this fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r
📓 Docs Drift CheckThis PR changes 1 package(s): 2 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 — 23 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 9fb8cb33b99881eaeee7c8d1322032d71bfda6f0 && git checkout 9fb8cb33b99881eaeee7c8d1322032d71bfda6f0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 53428b8c9dc6ba3aa79b6b00a1a3a83877be7f81 2a74cb35df9f089418c56312fad1db34dbba430a && git checkout -B drift-repro 53428b8c9dc6ba3aa79b6b00a1a3a83877be7f81 && git merge --no-ff 2a74cb35df9f089418c56312fad1db34dbba430a
node scripts/docs-audit/affected-docs.mjs --json 53428b8c9dc6ba3aa79b6b00a1a3a83877be7f81
|
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32492878078 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
`get()` returned `null` and `list()` returned `[]` over a storage seam that accepted the query and ran nothing. `InMemoryDriver.execute()` logs `Raw execution not supported in InMemory driver` and returns `null`; the local `normalizeRows` maps that to `[]`, which is also what a working driver returns for a SELECT that matched nothing. Both read paths reported that emptiness as a product answer callers act on, and `start()`'s own `sys_packages` rehydration skipped in silence because of it. Reads now establish the seam ANSWERED before reading emptiness as a fact: a result that is not a result set is refused with a declared ADR-0112 envelope (`SERVICE_UNAVAILABLE` / 503), and the skipped boot rehydration is logged at warn. A seam that answers with genuinely zero rows is unchanged. Third instance of one class (#10677 / PR #10788, #10789 / PR #10964): a seam that cannot answer is absent, not empty. The predicate is a local copy — `metadata-protocol` deliberately does not publish its own, and this package does not depend on it. Part of #10965 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
Fixes#10677
The defect
os migrate duplicatesreported a clean bill of health over a driver it never queried — the exact false all-clear the #8928 ruling'sno_sql_seamrefusal exists to prevent. Reproduced at head before the change:Three statements swallowed, and the report says the install is clean.
InMemoryDriver.execute()(memory-driver.ts:300-302, re-derived at head) logs that warning and returnsnull— it neither throws nor is absent. Two consequences compounded:resolveSeedTenancySeam'scanRun(seed-tenancy-backfill.ts:271at head; the card cited:247from79ebb37) askstypeof d.execute === 'function'. That is a question about the driver's shape, and a no-opexecutehas the right shape — soresolveSeedTenancyExecreturned a usable exec and theif (!exec)guard atduplicates.ts:697never fired.normalizeRows(null)is[](seed-tenancy-backfill.ts:351,if (!result) return [];), and[]is also what a real driver returns for a SELECT that matched nothing.So the refusal was dead code for one of the two drivers the clause names.
What the guard keys on
Not "is this the memory driver" and not "does memory report
no_sql_seam". It keys on the distinction the old guard conflated:A driver that answers returns a result set — a bare row array (better-sqlite3 through knex),
{ rows }(pg), or the[rows, fields]tuple (mysql2) — and an empty result set in any of those spellings is still a result set. A driver that cannot answer returns no result set at all. No driver is named anywhere in the implementation, so any host with the same no-op shape is covered without an allowlist to maintain.Two guards, both on that one predicate:
no_sql_seambranch. This is the card's fix.catchblocks the collector already has, which is what this command's contract has always promised for a target it could not read: "Anything that could not be probed is listed inskippedwith the reason" (the file's own header). No new refusal, no new report key. Named here because it is beyond the literal card text — same defect class, same file, same gate family, and it is what keepscounters.statusfrom claiming"read"over a table nothing read.After:
Two behaviours deliberately NOT changed
skippedpath already reports it honestly (duplicate probe failed:plus the driver's own message). Claiming it here would swallow a transient connection error as "no seam" and would invent a refusal Report the business identifiers already minted twice by the tenancy split — an operator-facing inventory, on installs the #8686 backfill cannot repair #8928 never mandated. Pinned in both directions by a test.normalizeRowsalready flattened to[], so the only possible change is "reported as unreadable" replacing "reported as zero rows". A live sqlite run is in the evidence below.Scope
⛔ No driver package is modified. The maintainer's 2026-08-05 investment freeze covers the
driver-memory/driver-mongodbfamily; makingInMemoryDriver.execute()throw is the tidier contract and the card offers it first, but it is fenced. This is the consumer-side route triage directed, and it works — no escalation needed.⛔ The mongodb branch is not claimed. That driver was not loaded for this fix, so no test here asserts anything about it, and the checklist correction says so explicitly rather than assuming it behaves like memory.
The checklist carries the one-line clause-7 knownGap correction the card names — the memory driver exposes a no-op seam, not no seam — plus the
duplicates.tsline citations it holds, re-derived because this change moved them. Item revision bumped with a history entry, per that ledger's rule.Evidence
Gate union re-derived from the real changeset with
node scripts/pm/dispatch-gates.mjs(no paths passed) and run at2a74cb35df. Every gate below reports its own verdict line:check:nul-bytesOK (scanned 6239 text file(s) ... no raw ASCII control bytes)check:changeset-gate-self-testscheck:objectui-changeset✓ objectui-range --self-test: all checks passedcheck:cross-package-test-inputsOK: 13 package(s) read outside themselves, all declaredcheck:test-source-aliasOK — 72 packages with tests scannedcheck:type-source-resolutionOK — 76 packages with a tsconfig.json scannedcheck:slot-lookup✓ ratchet holds: 107 unswept site(s) ... none newcheck:doc-authoring✓ 389 files cleancheck:doc-formula-expressions✓ 22 record-scoped formula example(s) ... judged cleancheck:query-options-erasure✓ ratchet holds: 67 unswept non-test site(s) ... none newcheck:engine-double-contractOK — 371 pinned, 133 in the DEBT ledger, 2 exemptcheck:where-matcher✓ 271 matcher(s) discovered ... none newcheck:platform-checklistOK — 15 areas, 205 items(not in CI by maintainer decision; run by hand because this PR edits the ledger)check-adr-0087-registration/check-changeset-no-major/check-empty-changeset/check-affected-docsTests and typecheck, both through
scripts/pm/os-verify-lock.sh:Declared narrowing: the full
@objectstack/clisuite (137 files / ~495s, the largest item on the Test Core critical path per that package'svitest.config.ts) was not run locally — the five duplicates suites plus the package typecheck and ESLint were.check:type-check-debt --re-measurewas likewise not run: it needs the whole./packages/*closure built. CI runs both.The reverse verification is also inside the test file rather than only in this description:
duplicates.null-seam.test.tsboots a realmemory://stack, collects the report through the bare exec (assertingduplicates: [],skipped: [],counters.status: "read"— the defect, reproduced), then collects it again through the wrapped seam and asserts the same targets now land inskipped. It cannot rot into a test that only proves a real SQL driver still works.A live sqlite run confirms the preflight does not refuse a real driver: it cleared the guard, ran the scan, and produced an honest
skippedentry for a table that did not exist —duplicate probe failed: ... no such table: crm_case— rather than a refusal.Generated by Claude Code