Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .changeset/migrate-duplicates-null-seam-refusal.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
---
"@objectstack/cli": patch
---

`os migrate duplicates` no longer reports a clean bill of health over a driver it
could not query (#10677). The `no_sql_seam` refusal #8928 mandated was dead code
for the memory driver, so the exact outcome the ruling exists to forbid was
reachable:

```
os migrate duplicates --database-url memory://qa
-> exit 0 {"duplicates":[],"skipped":[],"counters":{"status":"read"}}
```

`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. Three statements were swallowed and the report said
the install was clean.

The command now separates the two cases the guard used to conflate: **a seam
that cannot answer is absent, not empty.** It asks the resolved seam one trivial
statement before the scan starts and refuses when the answer is not a result
set, and it holds every individual probe to the same standard, so a probe that
returns no result set becomes a `skipped` entry with its reason instead of zero
findings.

```
os migrate duplicates --database-url memory://qa
-> exit 1 {"error":"no_sql_seam","detail":"The active driver exposes no
usable raw SQL seam — it is either absent, or present but
returning no result set — …"}
```

Nothing here names a driver: a seam is judged by what it returns, so any host
with the same no-op shape is covered without an allowlist to maintain. No driver
package was modified.

Two behaviours are deliberately unchanged. A seam that **throws** is a driver
present and refusing loudly, and the per-probe `skipped` path already reports
that honestly — claiming it here would swallow a transient connection error as
"no seam" and would invent a refusal #8928 never mandated. And a real SQL driver
is unaffected: every shape the new check rejects is one `normalizeRows` already
flattened to `[]`, so no row that used to be reported can be lost.
11 changes: 6 additions & 5 deletions docs/qa/platform-checklist/areas/cli.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -1045,7 +1045,7 @@
"title": "os migrate duplicates: a read-only JSON inventory of identifiers minted across partitions — within-partition repeats excluded, nothing written, the live two-counter condition reported, and runnable BEFORE the #8686 repair destroys the evidence",
"since": "v17",
"status": "active",
"revision": 1,
"revision": 2,
"priority": "P1",
"surface": "cli",
"personas": ["operator (local shell, pre-repair audit)"],
Expand All@@ -1057,7 +1057,7 @@
],
"knownGaps": [
"ORDERING IS LOAD-BEARING: do NOT boot the dev server again between seeding and scanning — a #8686-repairing boot backfills organization_id = NULL, which is exactly the evidence this report inventories (the command's own header calls the evidence perishable). The duplicates command itself boots read-only (deferSchemaDdl + readOnlyProbe) and is safe to run any number of times",
"the memory and mongodb drivers expose no raw-SQL seam, so the negative no_sql_seam probe needs a second scratch config on the memory driver — stage it per run"
"the memory driver does NOT lack a raw-SQL seam — `InMemoryDriver.execute()` logs `Raw execution not supported in InMemory driver` and returns null, which is exactly the SHAPE the seam resolver tests for, so the no_sql_seam refusal was dead code for it (a clean `duplicates:[]` exit 0 instead, #10677). The refusal now keys on whether a seam returns a RESULT SET rather than on whether `execute` exists, so the memory driver does take the no_sql_seam branch. The negative probe still needs a second scratch config on the memory driver — stage it per run. The mongodb branch is NOT covered by that fix's evidence — the driver was not loaded — so treat it as unverified rather than assuming it behaves like memory"
]
},
"steps": [
Expand DownExpand Up@@ -1110,7 +1110,7 @@
{
"clause": "a driver with no raw-SQL seam refuses LOUDLY: {error: 'no_sql_seam', …} with exit 1 — an empty clean report from a driver the probe cannot run against would be indistinguishable from 'never looked'",
"oracle": "log",
"verify": "the memory-driver run emits the no_sql_seam payload and echo $? is 1 (duplicates.ts:697-712); a boot failure likewise answers {error: 'boot_failed'} exit 1, never a zero-duplicate success",
"verify": "the memory-driver run emits the no_sql_seam payload and echo $? is 1 (duplicates.ts:787-812); a boot failure likewise answers {error: 'boot_failed'} exit 1, never a zero-duplicate success",
"evidence": "the refusal payload + exit code"
}
],
Expand All@@ -1122,13 +1122,14 @@
],
"traps": ["stale-dist", "absence-inference"],
"source": [
"packages/cli/src/commands/migrate/duplicates.ts (the :17-80 contract header encoding the 2026-08-16 maintainer ruling's five points; report interfaces :82-165; the cross-partition HAVING at :263-266; flags :648-656; read-only boot :670-679; no_sql_seam refusal :697-712)",
"packages/cli/src/commands/migrate/duplicates.ts (the :17-80 contract header encoding the 2026-08-16 maintainer ruling's five points; report interfaces :82-165; the cross-partition HAVING at :263-266; the seam-answer guards at :434-523; flags :739-747; read-only boot :761-770; no_sql_seam refusal :787-812)",
"packages/cli/src/commands/migrate/duplicates.contract.test.ts (the full JSON shape against a real sqlite), duplicates.pre-repair.test.ts (byte-identical DB + the #8686 repair measured destroying the evidence), duplicates.integration.test.ts, duplicates.probe-sql.test.ts — seam pins; none drives the oclif command end-to-end, hence no automated entry",
"#8928 (the card and ruling), #8686 / #8844 (the closed producers whose damage this inventories)",
"sibling item cli.migrate-plan-apply-json (lists duplicates as a variant; the scratch-DB boot recipe is shared)"
],
"history": [
{ "revision": 1, "date": "2026-08-20", "change": "new — scoped scan-functionality sweep (扫描功能): `os migrate duplicates` landed 2026-08-16 (#8928) after the sibling migrate item's enumeration was authored, so the subcommand had no functional coverage. Authored from the :17-80 contract header's five ruling points, with the perishability ordering (seed → scan → only then any repair-bearing boot) carried as a load-bearing knownGap and the no-JSON-flag posture spelled out so the #4873 sweep does not misread an oclif 2", "ref": "claude/new-session-0pv25p" }
{ "revision": 1, "date": "2026-08-20", "change": "new — scoped scan-functionality sweep (扫描功能): `os migrate duplicates` landed 2026-08-16 (#8928) after the sibling migrate item's enumeration was authored, so the subcommand had no functional coverage. Authored from the :17-80 contract header's five ruling points, with the perishability ordering (seed → scan → only then any repair-bearing boot) carried as a load-bearing knownGap and the no-JSON-flag posture spelled out so the #4873 sweep does not misread an oclif 2", "ref": "claude/new-session-0pv25p" },
{ "revision": 2, "date": "2026-08-21", "change": "clause-7 knownGap was factually wrong about the memory driver, and the run that trusted it produced a false all-clear: the driver exposes a NO-OP `execute` seam (warn + return null), not no seam, so the no_sql_seam refusal never fired and the scan answered exit 0 with `duplicates:[]` — indistinguishable from `never looked`. Corrected the knownGap, and re-derived the drifted duplicates.ts line citations against the fix that makes the refusal live (#10677). The mongodb half is left explicitly UNVERIFIED rather than restated: that driver was not loaded for the fix", "ref": "#10677" }
]
},
{
Expand Down
Loading
Loading