You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
cli/serve --dev: an OS_DATABASE_URL whose scheme the CLI does not recognise is ignored in silence, and the boot serves an in-memory SQLite store instead of the database the operator named #23633
Filing gate ①, class (a). reach:measured at the public doorOS_DATABASE_URL=redis://127.0.0.1:6399/0 objectstack serve objectstack.config.ts --dev, on a host fixture with an auth secret, at 650e28a731.
Filed by domain:cli seat 2 (#22648) · sunhaolin · session session_01Q9PZ5Pzqcus4woVvTZwWNU, from #23573's build report (os-dev-report on #23573, PR #23631, head 650e28a731). ⛔ Not a claim. Grade and lane are triage's.
What was measured
The boot printed ✓ Server is ready with the banner Driver: SqlDriver(better-sqlite3) → :memory:.
No line said the URL was ignored, so the operator's named database was silently replaced by an in-memory store.
Why, read on the source
inferDriverTypeFromUrl (packages/cli/src/utils/storage-driver.ts) answers '' for an unrecognised scheme, and resolveStorageDefinition falls through to the dev :memory: default.
An unrecognised OS_DATABASE_DRIVER is already refused there ("Booting on the SQLite default instead would silently ignore the driver you asked for"); the URL arm has no such refusal.
Triage grades it and routes it. The landing is packages/cli/src/utils/storage-driver.ts (domain:cli): refuse an unrecognised OS_DATABASE_URL scheme as the driver arm does.
Read fresh. serve --dev with an OS_DATABASE_URL whose scheme the CLI does not recognise (redis://…) boots in silence on an in-memory SQLite store. This was measured at the public door.
inferDriverTypeFromUrl answers '' for the scheme, and resolveStorageDefinition falls through to the dev :memory: default.
The sibling arm already refuses an unrecognised OS_DATABASE_DRIVER.
Claim: PM loop round 4, batch 3 (the maintainer's 「并发恢复3」 in this seat's chat, about 22:35Z on 2026-10-15; recorded in seat post #6024 §1). Label order: the oldest unclaimed bug at priority:p3 in domain:cli's open queue; no p0, pm:blocking, open target:, p1 or p2 card is open for dispatch (#23497 is pm:blocked; #23050 is seat 2's). #23633: objectstack serve refuses an OS_DATABASE_URL whose scheme it does not recognise, as its driver arm and the runtime's standalone host already do, instead of booting --dev on an in-memory SQLite store in silence · 2026-10-15T23:08Z
Session: session_01FLwgbTSmGMrqsXkBDsiwfG
Account: os-bill (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-23633-unrecognised-database-url-scheme
Worktree: objectstack-issue-23633
Domain: domain:cli
Seat: domain:cli#1 (seat post #6024)
File surface (read on origin/main9bb93706fb; stop on a breach and explain it in the report):
packages/cli/src/utils/storage-driver.ts: inferDriverTypeFromUrl (:255), and resolveDriverType / resolveStorageDefinition (:293, :364) only as far as the refusal needs;
Read, not edited: packages/runtime/src/standalone-stack.ts's detectDriverFromUrl (:399), the runtime host's answer to the same OS_DATABASE_URL.
Container & model: M (one refusal measured at the public door in --dev and production, and its agreement with the runtime host's scheme set), mode:subagent, model: TIER_DEFAULT (dispatch-gates --tier --repo objectstack-ai/objectstack over the path at 9bb93706fb: "no path-derived mandate … The tier stays the PM's per-card judgment call")
Clause-②: no
Basis for the Clause-② line: the accepted set is the documented one. content/docs/deployment/environment-variables.mdx:49 lists file:, :memory:, postgres://, mysql://, mongodb:// and libsql:// / *.turso.io, and the runtime host already refuses any other scheme (detectDriverFromUrl). The CLI's --dev arm silently booted on inputs outside that set; refusing them moves no declared accept set. ⛔ A URL the runtime host accepts is not refused here. No export is added (UnsupportedDriverError is the existing refusal), and there is no packages/spec edit.
Responsibility: packages/cli's storage resolution for serve, platform code in this lane | the banner's Driver: … → :memory: row is the only trace, and nothing says the URL was ignored | an operator who names a database the CLI does not recognise, in os serve --dev and os dev; measured at the public door (OS_DATABASE_URL=redis://127.0.0.1:6399/0 objectstack serve --dev) by seat 2's filing from cli/serve: a production objectstack serve with no OS_DATABASE_URL registers no driver in silence, then fails at AuthPlugin.start with "No driver available for object 'sys_organization'" and a remedy naming a datasource nobody declared #23573's build report
Thread-read: 6188444478
Serial constraints cleared, and the landing order:
os-dev-report
{
"issue": 23633,
"status": "done",
"branch": "claude/issue-23633-unrecognised-database-url-scheme",
"pr": "#23761",
"session": "session_01FLwgbTSmGMrqsXkBDsiwfG (the parent's session; this run is mode:subagent)",
"premise_still_valid": true,
"summary": "Premise held, measured at the public door on base 3b46855: OS_DATABASE_URL=redis://127.0.0.1:6399/0 os serve --dev over a host config printed 'Server is ready' with 'Driver: SqlDriver(better-sqlite3) → :memory:'. Production met the no-storage refusal. os dev did the same through its serve --dev child, after printing 'Database: redis://127.0.0.1:6399/0'. os start over a host config met the no-storage refusal. inferDriverTypeFromUrl (packages/cli/src/utils/storage-driver.ts) now throws UnsupportedDriverError (recognized: false) for any SCHEME:// no arm recognises. The message names OS_DATABASE_URL, the scheme (never the URL, so credentials are not printed) and the recognised spellings in the runtime host's order. Per the seat's reading, the function also takes the runtime host's arms character for character: pg:// is postgres, and a value with no SCHEME:// is a SQLite file. Before, both booted --dev on :memory: in silence and met the no-storage refusal in production. A .db ending no longer rescues an unrecognised scheme. A blank URL stays absent. An explicit OS_DATABASE_DRIVER still wins. serve.ts is not edited: its resolveDriverType call sits above the storage try, so the throw reaches the fatal handler and the boot exits 1 in both postures (measured). Draft PR #23761 is assigned to os-bill. CI was in_progress when this report was written.",
"tests": "Unit pins under packages/cli/src only. storage-driver.test.ts covers pg:// and PG:// → postgres; 4 scheme-less paths → sqlite; '' only for an absent or blank URL; 5 refused URLs (redis://h/app.db among them), each checked for the type, recognized false, driverType = the scheme, and that the message names OS_DATABASE_URL, the scheme and recognised spellings; no credentials or host in the refusal; and an explicit driver winning over redis://. The old row pinning inferDriverTypeFromUrl('redis://localhost') === '' was replaced. driver-vocabulary-parity.test.ts gains a new block that drives both hosts (resolveStandaloneDatabase against resolveStorageDefinition(resolveDriverType(undefined,url))). It has 19 runtime-accepted URLs, each accepted as the same kind and built from that URL (displayUrl === url) with isDev false and true; 5 URLs both hosts refuse, in dev and prod, the CLI refusal being the typed one; and 2 opaque-scheme values on which the hosts must agree. Runs (verify lock): the 2 files 129/129 passed. pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 at head 683bf95: 'Test Files 295 passed (295)', 'Tests 4399 passed (4399)'. pnpm --filter @objectstack/cli typecheck: exit 0 (tsc --noEmit covers src tests, then 'check:test-typecheck: OK'). The integration layer is declared to CI: the diff touches no integration-tier file and no spawn or boot entry. Ablation, through scripts/ablation-replace.mjs WRAP mode against src (the tests import ./storage-driver.js relatively, so no dist is involved), each leg with on-disk proof and its restore proven as blob == HEAD 8100f9353946 with an empty git diff HEAD: A1 return '' for an unrecognised scheme, 12 red / 117 green; A2 pg dropped from the postgres arm, 3 red; A3 the old .db / :memory: arm restored in place of the scheme-less arm, 8 red; A4 blank guard removed, 1 red. The first A1 attempt was a no-op, refused by the tool because the anchor was contained in the replacement. It was re-run with a non-overlapping replacement. Real boots before and after are in before_after.",
"gates": "Local, head 683bf95. eslint: pnpm exec eslint packages/cli --no-inline-config, exit 0, 696 files, 0 errors, 0 warnings (JSON formatter). node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 64 commands, each run once with its exit captured before any pipe. 62 exited 0. 2 exited 3 (PREREQUISITE NOT MET, so NOT MEASURED): pnpm check:dual-build-cjs-loads and pnpm check:i18n-coverage. Both need the whole workspace built, and this run built only the @objectstack/cli closure; CI runs both, as a declared narrowing. --ran: 'Run reconciliation — 64 derived, 62 run, 2 NOT-MEASURED, 0 UNRUN', exit 0. Its STALE TREE line: 3 commits behind origin/main 140238a, and the derived-from file that moved is scripts/sdui-manifest.record.json, an objectui pin bump. Not re-merged: main brought #23741 (serve.ts banner, format.ts), whose diff does not touch resolveDriverType or resolveStorageDefinition. Not owed: the local-only check:dev-prereqs and check:skill-frame-freshness (no packages/spec or framework edit), and a script's own suite (no gate or tool script edited). CI check-runs on 683bf95 at report time (00:4xZ). success: Build Core, Check Changeset, Check PR Size, Governed Surface Queue Guard, Lint · ESLint, Lint · PM dispatch-gates, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Type Check · workspace, Type Check · debt ledger, Test Core (5/6), Test Core (6/6), Dogfood Regression Gate, Auto Label, Check Documentation Links, Flag docs affected by code changes, filter, the three claim/closing guards. skipped: Build Docs, Console Pin Gate, Dogfood Verify CLI, the Dogfood shards, Packed-tarball smoke, Temporal Conformance. in_progress: Test Core (1/6 to 4/6), Type Check · source gates, Type Check · consumer gates, Lint · CI, release & engine gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure. No red.",
"line_budget": "n/a",
"mcp_calls": "0",
"api_writes": "2 relay strokes (each one POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write as objectstack-fleet[bot]). Stroke 1, pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/23761/assignees (os-bill); read-back identical, 12254 bytes, assignees os-bill. Stroke 2: POST /repos//issues/23633/comments, this report. git push is not counted: the empty probe branch plus 4 pushes of the branch.",
"deviations": [
"No real-boot pin landed, against the PM's Zone 3 suggestion. Its home is packages/cli/test, and a new spawner there also needs a roster entry in scripts/check-cli-test-child-env.mjs. Both are outside the file surface. The triage ruling 'Pin --dev and production' is met at the unit layer, with every refused and accepted row run at isDev false and true, which is the layer the existing memory:// and unknown-driver refusals are pinned at. The boots were measured by hand (before_after). See open_questions[1].",
"Four probe files are left at the container root: /os23633-probe.db, /os23633-probe.telemetry.db, /os23633-probe.telemetry.db-shm and /os23633-probe.telemetry.db-wal. They were written by the sqlite:// measurement (out_of_scope_findings[1]). The rm was refused by the harness safety check for a root path, so they need a human removal.",
"The first A1 ablation leg was a no-op, refused by ablation-replace.mjs because the anchor was a substring of the replacement. It was declared and re-run with a non-overlapping replacement.",
"Zone 2. H1 confirmed. H2 confirmed with one correction: the throw comes from resolveDriverType at serve.ts:3101, which is ABOVE the try, so it reaches run()'s fatal handler directly (the catch would also re-throw UnsupportedDriverError). The memory:// boot (exit 1) was the control. H3 is partly falsified: os start over a HOST config reaches this path through its production serve child, measured exit 1 on the no-storage refusal before and the scheme refusal after. Over a plain config it goes through the runtime host as stated.",
"The harness attribution reminder named a model in Co-Authored-By. Commits carry the model-free pair the dispatch and AGENTS.md require.",
"The branch merged origin/main once (683bf95) before the full unit tier ran. origin/main then moved 3 commits, including #23741, a cli serve.ts and format.ts change outside resolveDriverType and resolveStorageDefinition. It was not re-merged before the PR opened; PR CI tests the merge ref."
],
"files_changed": [
".changeset/23633-unrecognised-database-url-scheme.md",
"packages/cli/src/utils/driver-vocabulary-parity.test.ts",
"packages/cli/src/utils/storage-driver.test.ts",
"packages/cli/src/utils/storage-driver.ts"
],
"before_after": {
"fixture": "a throwaway defineStack HOST config (one object, one instantiated no-op plugin). Built CLI node packages/cli/bin/run.js, NODE_ENV unset, OS_CLOUD_URL=off, auth secret set. Before = base 3b46855; after = branch 5493312, built.",
"redis_dev": {
"before": "no exit (the boot reached its banner): 'Server is ready', 'Driver: SqlDriver(better-sqlite3) → :memory:'",
"after": "exit 1: 'Unsupported database URL scheme "redis://" (OS_DATABASE_URL / --database). Supported: postgres:// (or postgresql://, pg://), mysql:// (or mysql2://), mongodb:// (or mongodb+srv://), libsql:// or an https:// .turso. URL (optional @objectstack/driver-turso), wasm-sqlite://, file: (or sqlite:), or a file path with no scheme; :memory: is SQLite's in-memory database. Booting on the SQLite default instead would silently ignore the database you named and write into one you never chose. Fix the URL, or unset it.'"
},
"redis_prod": {
"before": "exit 1: 'This production serve has no storage: no recognized OS_DATABASE_URL declared a default datasource ...' then 'Refusing to boot: this production serve has no storage, ... Set OS_DATABASE_URL, mount a storage driver plugin, or run with --dev.'",
"after": "exit 1: the same 'Unsupported database URL scheme "redis://" ...' sentence"
},
"redis_os_dev": {
"before": "banner 'Driver: SqlDriver(better-sqlite3) → :memory:' after 'Database: redis://127.0.0.1:6399/0'",
"after": "exit 1, the scheme sentence"
},
"redis_os_start": {
"before": "exit 1, the no-storage refusal",
"after": "exit 1, the scheme sentence"
},
"redis_with_credentials_dev": {
"before": "banner → :memory:",
"after": "exit 1, the scheme sentence; the password occurs 0 times in the output"
},
"pg_scheme": {
"before": "dev banner → :memory:; prod exit 1 no-storage refusal",
"after": "dev and prod exit 1 'datasource default: connect failed — connect ECONNREFUSED 127.0.0.1:5999' (the URL is used)"
},
"bare_path": {
"before": "./data/app dev banner → :memory:; prod exit 1 no-storage refusal",
"after": "dev 'Server is ready', '→ ./data/app', file exists; prod './data/app2' 'Server is ready', '→ ./data/app2'"
},
"controls": "memory:// dev exit 1 retirement sentence, unchanged. postgres://...:5999 dev exit 1 ECONNREFUSED, unchanged. file:./ctl.db prod 'Server is ready' '→ file:./ctl.db', unchanged. After only: OS_DATABASE_DRIVER=postgres with redis://...:6399 dev exit 1 ECONNREFUSED 127.0.0.1:6399, so the explicit driver wins and the scheme is not refused. A plain config (runtime host) redis:// dev exit 1 '[StandaloneStack] Unsupported database URL scheme: ...' both before and after."
},
"schemes": [
"Columns: CLI host (resolveStorageDefinition(resolveDriverType(undefined,url))) dev/prod before → after | runtime host (resolveStandaloneDatabase); measured with packages/cli/dist before and after the fix",
"postgres://, postgresql:// | postgres/postgres → postgres/postgres | postgres",
"pg://, PG:// | :memory: default/null → postgres/postgres | postgres",
"mysql://, mysql2:// | mysql/mysql → unchanged | mysql",
"mongodb://, mongodb+srv:// | mongodb/mongodb → unchanged | mongodb",
"libsql://, https://.turso.io | turso/turso → unchanged | turso",
"wasm-sqlite://, *.wasm.db | sqlite-wasm/sqlite-wasm → unchanged | sqlite-wasm",
"memory://, mingo:// | refused/refused → unchanged | refused",
"file:, sqlite:, sqlite://, :memory:, *.db, *.sqlite, *.sqlite3 | sqlite/sqlite → unchanged | sqlite",
"./data/app, /abs/app, app, C:\data\app, C:/data/app (scheme-less, no db extension) | :memory: default/null → sqlite/sqlite | sqlite",
"redis://, REDIS://, rediss://, http(s):// (not turso), ftp://, mssql://, sqlserver://, oracle://, cockroachdb://, mariadb:// | :memory: default/null → refused/refused | refused",
"redis://h/app.db (unrecognised scheme, .db ending) | sqlite/sqlite (a file at that path) → refused/refused | refused",
"jdbc:postgresql://h/db, redis:h (a scheme with no //) | :memory: default/null → sqlite/sqlite | sqlite (out_of_scope_findings[2])",
"blank ' ' | :memory: default/null → unchanged | the unified default file"
],
"open_questions": [
{
"question": "An opaque-scheme value (jdbc:postgresql://h/db, redis:h) has no SCHEME:// and so passes the runtime host's scheme-less arm. That host opens it as a SQLite file and creates bogus directories (out_of_scope_findings[2]). Under the ruling that the CLI never refuses what the runtime host accepts, the CLI host now mirrors it, where before it booted :memory:. Keep the mirror?",
"options": [
"A: keep the mirror (this PR). Fix finding 2 on both hosts at once, with the parity row holding them together.",
"B: the CLI returns '' for these, which is the old silent :memory: in dev.",
"C: the CLI alone refuses them, which breaks the ruling."
],
"recommendation": "A. Business need: no measured producer of such URLs in the repo, so the row is about safety, not a feature. Long term: one answer per URL across both hosts, and the parity row reds whichever host moves first. AI error: one dialect, not two. Startup focus: no new surface. B keeps a silent wrong store, and C forks the hosts."
},
{
"question": "Is a real-boot pin owed beyond the unit pins, for redis:// under --dev and production?",
"options": [
"A: no. The unit pins run every row with isDev false and true, the layer the existing memory:// and unknown-driver refusals use.",
"B: two legs in packages/cli/test/serve-no-storage-boot-guard.integration.test.ts. That file is already on the child-env roster, but it is outside this card's surface, and its test-budget header must be re-measured."
],
"recommendation": "A. The exit path (resolveDriverType above serve's try) is shared with the memory:// refusal and was measured by hand both ways. B adds CI wall to a queue-tier integration file for a path no other refusal pins at that layer."
}
],
"out_of_scope_findings": [
"class: a · reach: public door measured, os dev --no-watch over a plain config with OS_DATABASE_URL=redis://admin:PASSWORD@127.0.0.1:6399/0; os dev's own 'Database:' row redacts it, then the runtime refusal prints '[StandaloneStack] Unsupported database URL scheme: redis://admin:PASSWORD@127.0.0.1:6399/0' in full; also exception: security (credential in a boot log) · evidence: packages/runtime/src/standalone-stack.ts detectDriverFromUrl throw interpolates dbUrl; measured on branch head 5493312, runtime unchanged from main · dedupe words: unsupported database URL scheme prints password; standalone-stack refusal credential leak; detectDriverFromUrl full URL in error",
"class: a · reach: public door measured, OS_DATABASE_URL=sqlite://os23633-probe.db os serve --dev over a host config: banner '→ sqlite://os23633-probe.db', database written to /os23633-probe.db (filesystem root); the runtime host normalises to file: and resolves ./os23633-probe.db · evidence: resolveStorageDefinition sqlite arm strips 'sqlite:' and leaves '//x.db' (sqlite:///tmp/x.db gives '///tmp/x.db'); resolve-project-database.ts normalises sqlite:// on the runtime side only · dedupe words: sqlite:// double slash absolute path; CLI sqlite alias not normalised to file:; resolveStorageDefinition sqlite filename root",
"class: a · reach: public door measured, OS_DATABASE_URL=jdbc:postgresql://127.0.0.1:5999/db os serve --dev over a plain config (runtime host): 'Server is ready', '→ jdbc:postgresql://127.0.0.1:5999/db', and created ./jdbc:postgresql:/127.0.0.1:5999/db, against the module header's 'Unknown URL schemes throw — we never silently fall back to sqlite' · evidence: standalone-stack.ts detectDriverFromUrl scheme-less arm tests only SCHEME:// presence; the CLI host mirrors it by this card's ruling and the new parity row holds them together, so the fix is both hosts at once · dedupe words: jdbc URL treated as sqlite file; opaque scheme bare path arm; detectDriverFromUrl bogus directory",
"carrier: none · noted, not filed: serve.ts step-2 comment block lists the URL-scheme priority and now omits pg://, the scheme-less arm and the unrecognised-scheme refusal (incomplete, not false; serve.ts held by #23623)",
"carrier: none · noted, not filed: content/docs/deployment/cli.mdx:522 'otherwise sqlite' was already untrue for the runtime host before this change, which does not make it false; no docs edit"
]
}
os-dev-report
{
"issue": 23633,
"status": "done",
"round": 1,
"branch": "claude/issue-23633-unrecognised-database-url-scheme",
"head": "40fe66778",
"pr": "#23761",
"session": "session_01FLwgbTSmGMrqsXkBDsiwfG (the parent's session; this run is mode:subagent, the round-1 finisher)",
"premise_still_valid": true,
"summary": "Round 1 verified at head 40fe667, which the previous run pushed before a container restart killed it. Both REWORK asks are met. inferDriverTypeFromUrl refuses an opaque scheme (two or more characters and a colon, other than file: and sqlite:) with the same UnsupportedDriverError and sentence, and a one-letter drive stays a path. The sqlite and sqlite-wasm arms of resolveStorageDefinition translate sqlite: / sqlite:// through the runtime host's normalizeDatabaseUrl. Every gate, pin and measurement passed at that head, so this run wrote no code and pushed nothing. Beyond the asks, a .wasm.db name now selects the WASM engine only for a file, so it no longer rescues an unrecognised scheme (deviations[0]). One new finding: the CLI host opens file://NAME at the filesystem root (out_of_scope_findings[0], open_questions[0]).",
"rework_asks": {
"ask_1_opaque_scheme_refused": {
"verdict": "met",
"lines": "packages/cli/src/utils/storage-driver.ts:352 const scheme = /^[a-z][a-z0-9+.-]*:\\/\\//i.exec(u)?.[0] ?? /^[a-z][a-z0-9+.-]+:/i.exec(u)?.[0]; (the second test needs two or more characters before the colon, so a one-letter drive is no scheme); :353-359 throw UnsupportedDriverError(scheme, unsupportedUrlSchemeMessage(scheme), { recognized: false }), the same type and sentence as the SCHEME:// refusal; :340 returns file: / sqlite: before it.",
"pins": "storage-driver.test.ts:120-121 (jdbc:postgresql://localhost:5432/db, redis:cache refused at inference: type, recognized false, driverType = the scheme, and the message names OS_DATABASE_URL, the scheme and the recognised spellings); :93 (C:\data\app and C:/data/app stay sqlite paths). driver-vocabulary-parity.test.ts:498-499 (the CLI host refuses both in dev AND prod; the runtime host's answer is not pinned, and #23765 is named at :495).",
"proof": "Ablation R1-A 6 red / 143 green, R1-A2 2 red. Public door at 40fe667: jdbc:postgresql://127.0.0.1:5999/db and redis:h exit 1 in dev and production, naming "jdbc:" and "redis:", with no file written."
},
"ask_2_sqlite_alias_translated": {
"verdict": "met",
"lines": "packages/cli/src/utils/storage-driver.ts:107 imports normalizeDatabaseUrl from @objectstack/runtime, so there is no second copy of the rule; :524 the sqlite arm, normalizeDatabaseUrl(databaseUrl ?? ':memory:'); :542 the sqlite-wasm arm, the same rule.",
"pins": "driver-vocabulary-parity.test.ts:519-521 (sqlite://x.db, sqlite:x.db, sqlite:///var/lib/objectstack/x.db: the runtime host's sqliteFile equals the CLI host's config.filename, in dev and prod), plus :522 (sqlite://x.wasm.db). storage-driver.test.ts:274-277 (also SQLITE://x.db; displayUrl keeps the typed URL), :286.",
"proof": "Ablation R1-B1 5 red, R1-B2 2 red. Public door at 40fe667: sqlite://x.db boots to ready in dev and production and creates ./x.db in the probe directory, where base 683bf95 resolved //x.db, the filesystem root.",
"changeset": "updated for both asks (d343c71) and for the .wasm.db rule (40fe667)",
"untouched": "serve.ts, packages/runtime and packages/spec are not in the diff (4 files, read from git off merge base 53646fc). Clause-②: no stays line 2 of the PR body."
}
},
"tests": "At 40fe667, every heavy run through os-verify-lock, each verdict read from its VERDICT command-exit line. (1) Closure build: turbo run build --filter=@objectstack/cli... --concurrency=2, exit 0, 61/61 cached. packages/cli/dist/utils/storage-driver.js carries the round-1 code, by on-disk counts: normalizeDatabaseUrl(databaseUrl 2, wasmFile 3, the opaque regex 1, the old sqlite: strip 0. (2) Unit layer, two shards: pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 --shard=1/2, 'Test Files 148 passed (148)', 'Tests 2464 passed (2464)'; --shard=2/2, 'Test Files 147 passed (147)', 'Tests 1953 passed (1953)'. Total 295 files, 4417 tests, against 4399 at 683bf95. The integration layer is declared to CI: no integration-tier file, spawn entry or driver/kernel boot path is in the diff. (3) pnpm --filter @objectstack/cli typecheck: exit 0 ('check:test-typecheck: OK'). tsc --noEmit --listFiles names both changed test files (include src, no exclude). (4) The two pinned files: 'Tests 149 passed (149)', the baseline for the ablation legs.",
"gates": "Local, at 40fe667. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, derived 64 commands from 4 paths off merge base 53646fc. Each ran in order, its exit captured before any pipe and recorded as 'CMD :: exit N': all 64 exited 0. That includes check:dual-build-cjs-loads ('110 published require entry point(s) across 68 package(s) load') and check:i18n-coverage ('OK (14 config(s), 621 baselined untranslated string(s), none new)'), the 2 NOT MEASURED in round 0. --ran: 'Run reconciliation — 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN', exit 0. Its STALE TREE line reads 19 commits behind origin/main, with 4 derived-from files moved (discovery-routes anchor, check-route-envelope, record-recognisers, sdui-manifest record). Control: the same derivation over the same 4 paths in a throwaway detached tree at origin/main ec44cf6 printed the identical 64 commands (diff exit 0). pnpm exec eslint packages/cli --no-inline-config: exit 0, 696 files, 0 errors, 0 warnings (JSON formatter). pnpm --filter @objectstack/cli typecheck: exit 0. Not owed: check:dev-prereqs and check:skill-frame-freshness (no packages/spec or framework edit), and a script's own suite (no gate or tool script edited). test:repo is CI's. CI check-runs on 40fe667, 40 in all. success (34): Auto Label, Build Core, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Test Core, Test Core (1/6) to (6/6), The card this PR closes must claim this branch, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter. skipped (6): Build Docs, Console Pin Gate, Dogfood Regression Gate (matrix shard), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Temporal Conformance (live PG + MySQL). None red, and none in progress.",
"ablation": [
"Tool: scripts/ablation-replace.mjs in WRAP mode, the wrapped command being the two pinned files under os-verify-lock. The subject is src, because both files import ./storage-driver.js relatively, so no dist/ is involved. Every leg: the tool proved the mutation on disk (anchor x1 → x0, a new blob) and the restore (blob == HEAD 893ea05be3ed, git diff HEAD empty). An outer trap in the leg script proved the restore again by hash and diff, and an on-disk grep found 0 ABLATION markers after each leg. Baseline 149 passed.",
"R1-A, the opaque-scheme refusal: the opaque fallback deleted from the scheme test. 6 red / 143 green, the jdbc:postgresql://..., redis:cache and jdbc:x/app.wasm.db rows in both files.",
"R1-A2, the drive boundary (added; see deviations): the opaque test widened from + to * (one-letter schemes). 2 red: C:\data\app and C:/data/app (storage-driver.test.ts:93).",
"R1-B1, the alias translation in the sqlite arm: normalizeDatabaseUrl replaced by base's .replace(/^sqlite:/, ''). 5 red: sqlite://x.db and sqlite:///var/lib/objectstack/x.db in both files, and SQLITE://x.db. sqlite:x.db stays green, because base's strip already handled it.",
"R1-B2, the alias translation in the sqlite-wasm arm, same mutation: 2 red, the sqlite://x.wasm.db rows in both files.",
"R1-C, the .wasm.db-only-for-a-file rule: the .wasm.db test moved back above every scheme check. 4 red: redis://h/app.wasm.db and jdbc:x/app.wasm.db in both files."
],
"before_after": {
"fixture": "the built CLI (node packages/cli/bin/run.js serve objectstack.config.mjs [--dev] --port RANDOM) over a throwaway HOST config: one object and one plain-object plugin with init, no imports. The cwd is a fresh directory under the scratch dir, NODE_ENV and OS_DATABASE_DRIVER unset, OS_CLOUD_URL=off, an auth secret set, timeout 60 s, killed on 'Server is ready'. Before = 683bf95: storage-driver.ts restored to that commit in the worktree (unstaged, swap proven by blob 8100f9353946), built (dist counts: normalizeDatabaseUrl 0, wasmFile 0, opaque regex 0, sqlite: strip 2), then restored to HEAD (blob 893ea05be3ed == HEAD, git diff HEAD empty, porcelain empty) and rebuilt (61/61 cached, dist counts back to 2/3/1/0). After = 40fe667, built.",
"jdbc_postgresql": {
"before": "dev and production: 'Server is ready', 'Driver: SqlDriver(better-sqlite3) → jdbc:postgresql://127.0.0.1:5999/db', created ./jdbc:postgresql:/127.0.0.1:5999/db",
"after": "dev and production: exit 1, 'Unsupported database URL scheme "jdbc:" (OS_DATABASE_URL / --database). Supported: ...', no file"
},
"redis_opaque": {
"before": "dev and production: 'Server is ready', '→ redis:h', created ./redis:h",
"after": "dev and production: exit 1, the same sentence naming "redis:", no file"
},
"sqlite_double_slash": {
"before": "not booted, because a boot writes /x.db at the filesystem root. Read from the built module: config.filename '//x.db' in dev and prod, while the runtime host answers 'x.db'.",
"after": "dev and production: 'Server is ready', 'Driver: SqlDriver(better-sqlite3) → sqlite://x.db', created ./x.db in the probe directory under the scratch dir, never at /; built module: filename 'x.db', equal to the runtime host's"
},
"redis_wasm_db": {
"before": "dev and production: 'Server is ready', 'Driver: SqliteWasmDriver → redis://h/x.wasm.db', created ./redis:/h/x.wasm.db",
"after": "dev and production: exit 1, the same sentence naming "redis://", no file"
},
"drive_path_C_data_app": {
"before": "read only, no boot: sqlite, config.filename 'C:/data/app', displayUrl 'C:/data/app', dev and prod; the runtime host answers sqlite 'C:/data/app'",
"after": "unchanged: sqlite, filename 'C:/data/app', dev and prod"
},
"control_postgres": {
"before": "dev and production: exit 1, 'datasource default: connect failed — connect ECONNREFUSED 127.0.0.1:5999'",
"after": "unchanged in both postures"
},
"control_fixture": "after only: file:./ctl.db --dev boots to 'Server is ready', '→ file:./ctl.db', and creates ./ctl.db, which shows the fixture boots.",
"root_check": "after every boot, ls / lists no x., jdbc or redis* entry; the round-0 /os23633-probe* files were not touched"
},
"runtime_wasm_db": {
"url": "redis://h/x.wasm.db",
"read": "resolveStandaloneDatabase from packages/runtime/dist at 40fe667 (standalone-stack.ts and resolve-project-database.ts are identical on merge base 53646fc and origin/main), with OS_DATABASE_URL set: driver 'sqlite-wasm', sqliteFile 'redis://h/x.wasm.db', source 'env'. Mechanism, read and not changed: standalone-stack.ts:428 tests /\.wasm\.db$/i before the scheme-less arm (:430) and before the unsupported-scheme throw. sqliteFilenameFromUrl's sqlite-wasm arm (:548-552) strips only wasm-sqlite: and file:, so the file is named by the whole URL.",
"measured": "a plain config (no plugin instances, so serve boots through the runtime host), cwd a fresh scratch directory: dev and production both 'Server is ready', each writing a 1,499,136-byte database at ./redis:/h/x.wasm.db (directories 'redis:' and 'h' created under the cwd).",
"against_the_cli_host": "the CLI host refuses the same URL at 40fe667 (UnsupportedDriverError, recognized false, scheme "redis://"), in dev and prod. At base 683bf95 it agreed with the runtime host (sqlite-wasm, the same file). This is the departure deviations[0] states; the parity block does not pin the runtime host's answer for it."
},
"mcp_calls": "0",
"api_writes": "1 — one relay stroke (POST /repos/objectstack-ai/objectstack/dispatches, which fleet-write executes as objectstack-fleet[bot]) carrying POST /repos//issues/23633/comments, this report. No git push, label-write or PR-body PATCH. REST reads only otherwise: GET issues/23633/comments (twice), GET issues/23761/comments, GET pulls/23761, GET commits/40fe66778.../check-runs.",
"deviations": [
"The *.wasm.db rule goes past the REWORK's two asks, in ask ①'s class (eff7af2, plus the changeset line in 40fe667). A *.wasm.db name now selects the WASM engine only for a file: / sqlite: URL or a value with no scheme (storage-driver.ts:325, :340, :363). The sqlite-wasm arm takes ask ②'s rule too (:542). Reason: tested above every scheme check (base :317), a .wasm.db name rescued any scheme into a SQLite file, so ask ① alone would have left jdbc:x/app.wasm.db booting. Base 683bf95 booted redis://h/x.wasm.db in both postures on 'SqliteWasmDriver → redis://h/x.wasm.db' (before_after). sqlite://x.wasm.db opened //x.wasm.db there, the filesystem root. Cost: the CLI host now refuses redis://h/x.wasm.db, which the runtime host accepts (runtime_wasm_db). That departs from the dispatch ruling 'the CLI never refuses a URL the runtime host means to accept' in the same way the REWORK revised it for opaque schemes. The seat records the runtime host's answer on #23765.",
"The round-1 code was written by the previous run, which a container restart killed after it pushed 40fe667 and before it verified or reported. This run wrote no code. Every gate, pin and measurement passed at 40fe667, so nothing was committed or pushed.",
"Ablation has five legs where three were asked. The alias leg is split per arm (R1-B1 sqlite, R1-B2 sqlite-wasm). R1-A2, the one-letter-drive boundary, was added because R1-A cannot red the drive pin: those rows pass with or without the opaque fallback.",
"before_after: sqlite://x.db was not booted at base. A base boot writes /x.db at the filesystem root, which the dispatch's probe rule forbids, so the base answer was read from the built module instead ('//x.db'). C:/data/app was read only on both sides, as the dispatch asked.",
"The derivation printed STALE TREE (19 behind origin/main, 4 derived-from files moved). origin/main was not merged: no conflict, main-caused red or queue removal occurred. main's 19 commits touch the CLI's serve.ts and format.ts and the runtime's action-execution, http-dispatcher and sandbox, none of the three files this card rests on. The control derivation ran in a throwaway detached worktree at origin/main in the scratch dir, given exactly the 4 paths the no-path derivation named; it was removed after (git worktree remove, exit 0).",
"The verify lock's last holder before this run, a whole-workspace turbo build in this worktree, was dead (its pid gone). dist/ was not trusted: the closure was rebuilt (61/61 cached), and the round-1 code was confirmed in dist/ by on-disk counts before any boot.",
"The harness attribution reminder names a model in its Co-Authored-By line. This run made no commit, so the line was not used."
],
"files_changed": [
"none in this run (no commit). The PR's files at 40fe667, 4 vs merge base 53646fc:",
".changeset/23633-unrecognised-database-url-scheme.md",
"packages/cli/src/utils/driver-vocabulary-parity.test.ts",
"packages/cli/src/utils/storage-driver.test.ts",
"packages/cli/src/utils/storage-driver.ts"
],
"pr_body_changes": [
{
"old": "- The runtime host's arms are adopted.pg:// is postgres, and a value with no SCHEME:// is a SQLite file path, whatever its extension. Both match detectDriverFromUrl (packages/runtime/src/standalone-stack.ts, read only) character for character. The runtime host refuses every other SCHEME://, so the CLI host refuses nothing the runtime host accepts.",
"new": "- The runtime host's arms are adopted, with one class refused.pg:// is postgres, and a value with no scheme is a SQLite file path, whatever its extension, as in detectDriverFromUrl (packages/runtime/src/standalone-stack.ts, read only). An OPAQUE scheme (two or more characters and a : with no //, such as jdbc:postgresql://h/db or redis:h) is refused with the same UnsupportedDriverError and sentence, naming the scheme (\"jdbc:\"). The runtime host still opens those as a SQLite file at a path made of the URL, against its own rule that an unknown scheme never falls back to SQLite; that gap is #23765's. A one-letter drive is a path, not a scheme (C:\\data\\app, C:/data/app)."
},
{
"old": "- A .db ending no longer rescues an unrecognised scheme. The old *.db arm ran before any scheme check, so redis://h/app.db was handed to SQLite as a file path. It is now refused, as the runtime host refuses it.",
"new": "- A .db or .wasm.db ending no longer rescues an unrecognised scheme. The old *.db arm ran before any scheme check, so redis://h/app.db was handed to SQLite as a file path. It is now refused, as the runtime host refuses it. A *.wasm.db name likewise selects the WASM engine only for a file: / sqlite: URL or a path with no scheme, so redis://h/app.wasm.db and jdbc:x/app.wasm.db are refused too. The runtime host still accepts redis://h/x.wasm.db as a WASM SQLite file (measured below); the seat records that host's answer on #23765.\n- sqlite: and sqlite:// are aliases of file:, by the runtime host's own rule. The sqlite and sqlite-wasm arms of resolveStorageDefinition translate the URL through normalizeDatabaseUrl (@objectstack/runtime) rather than a second copy of it. sqlite://x.db and sqlite:x.db open ./x.db, sqlite:///abs/x.db opens /abs/x.db, and sqlite://x.wasm.db opens ./x.wasm.db on the WASM engine. Before, sqlite://x.db opened //x.db, a file at the filesystem root."
},
{
"old": "| a plain config (runtime host), redis://…, --dev | exit 1, [StandaloneStack] Unsupported database URL scheme: … | unchanged (not this path) |",
"new": "| a plain config (runtime host), redis://…, --dev | exit 1, [StandaloneStack] Unsupported database URL scheme: … | unchanged (not this path) |\n\n### Round 1, measured the same way\n\nBefore = 683bf9587 (round 0's head; storage-driver.ts is the only non-test source file between the two); after = 40fe66778. Each boot ran in a throwaway directory, so every file it wrote is listed relative to it.\n\n| OS_DATABASE_URL | Before (dev and production) | After (dev and production) |\n|---|---|---|\n| jdbc:postgresql://127.0.0.1:5999/db | ✓ Server is ready, → jdbc:postgresql://127.0.0.1:5999/db; created ./jdbc:postgresql:/127.0.0.1:5999/db | exit 1, ✗ Unsupported database URL scheme \"jdbc:\" (OS_DATABASE_URL / --database). Supported: …; no file |\n| redis:h | ✓ Server is ready, → redis:h; created ./redis:h | exit 1, the same sentence naming \"redis:\"; no file |\n| sqlite://x.db | the file //x.db, the filesystem root (read from the built module, not booted: a boot writes /x.db) | ✓ Server is ready, → sqlite://x.db; created ./x.db |\n| redis://h/x.wasm.db | ✓ Server is ready, SqliteWasmDriver → redis://h/x.wasm.db; created ./redis:/h/x.wasm.db | exit 1, the same sentence naming \"redis://\"; no file |\n| C:/data/app (read from the built module, no boot) | sqlite, the file C:/data/app | unchanged |\n| CONTROL postgres://u:p@127.0.0.1:5999/db | exit 1, connect ECONNREFUSED 127.0.0.1:5999 | unchanged |\n| CONTROL file:./ctl.db, --dev | (not measured) | ✓ Server is ready, → file:./ctl.db |\n\nThe runtime host, read and booted for redis://h/x.wasm.db and not changed here: resolveStandaloneDatabase answers sqlite-wasm on the file redis://h/x.wasm.db, and a plain config boots to ✓ Server is ready in dev and production with a 1,499,136-byte database at ./redis:/h/x.wasm.db."
},
{
"old": "Only the rows that moved are listed; every other row (postgres://, postgresql://, mysql://, mysql2://, mongodb://, mongodb+srv://, libsql://, https://*.turso.io, wasm-sqlite://, *.wasm.db, memory://, mingo://, file:, sqlite:, sqlite://, :memory:, *.db, *.sqlite, *.sqlite3) was already equal and is unchanged.",
"new": "Only the rows that moved are listed; every other row (postgres://, postgresql://, mysql://, mysql2://, mongodb://, mongodb+srv://, libsql://, https://*.turso.io, wasm-sqlite://, a scheme-less or file:*.wasm.db, memory://, mingo://, file:, sqlite:, :memory:, *.db, *.sqlite, *.sqlite3) was already equal and is unchanged."
},
{
"old": "| jdbc:postgresql://h/db, redis:h (a scheme with no //) | :memory: default / null | sqlite / sqlite | sqlite (see the finding below) |",
"new": "| jdbc:postgresql://h/db, redis:h (a scheme with no //) | :memory: default / null | refused / refused | sqlite, a file at a path made of the URL (#23765) |\n| redis://h/x.wasm.db, jdbc:x/x.wasm.db (an unrecognised scheme with a .wasm.db ending) | sqlite-wasm / sqlite-wasm, a file at a path made of the URL | refused / refused | sqlite-wasm, a file at a path made of the URL (#23765) |\n| sqlite://x.db, sqlite://x.wasm.db | sqlite / sqlite on //x.db; sqlite-wasm on //x.wasm.db (the filesystem root) | sqlite / sqlite on x.db; sqlite-wasm on x.wasm.db | the same files as the CLI after |"
},
{
"old": " - an explicit driver still wins over an unrecognised scheme.",
"new": " - an explicit driver still wins over an unrecognised scheme.\n - round 1: jdbc:postgresql://localhost:5432/db, redis:cache, redis://h/app.wasm.db and jdbc:x/app.wasm.db are refused at inference, naming the scheme; C:\\data\\app, C:/data/app and ./backup:2026.db stay sqlite paths; sqlite://x.db, sqlite:x.db, sqlite:///var/lib/objectstack/x.db and SQLITE://x.db resolve to the file x.db or /var/lib/objectstack/x.db, with the typed URL kept for the banner; a file: / sqlite:*.wasm.db selects sqlite-wasm; sqlite://x.wasm.db is the WASM engine on x.wasm.db."
},
{
"old": " - 2 opaque-scheme values on which the two hosts must give the same verdict, so a change to either host's scheme-less arm reds until the other follows.",
"new": " - 4 values the CLI host refuses, typed, in dev and in prod: the opaque schemes jdbc:postgresql://localhost:5432/db and redis:cache, then redis://h/app.wasm.db and jdbc:x/app.wasm.db. The runtime host's answer for these is deliberately not pinned: today it opens them as a file, the gap #23765 names.\n - 4 alias values both hosts open as the same file, in dev and in prod: sqlite://x.db and sqlite:x.db as x.db, sqlite:///var/lib/objectstack/x.db as itself, and sqlite://x.wasm.db as x.wasm.db on the WASM engine.\n - one more accepted row, file:./data/app.wasm.db as sqlite-wasm."
},
{
"old": "| A4 | the blank-URL guard removed | 1 red |",
"new": "| A4 | the blank-URL guard removed | 1 red |\n\nRound 1, at 40fe66778: the same tool in WRAP mode over the two pinned files (149 tests, baseline 149 passed). The subject is src (both files import ./storage-driver.js), so no dist/ is involved. Each mutation is proven on disk (anchor x1 → x0 and a new blob). Each restore is proven as blob == HEAD 893ea05be3ed with an empty git diff HEAD, by the tool and again by an outer trap.\n\n| Leg | Mutation | Result |\n|---|---|---|\n| R1-A | the opaque-scheme fallback removed from the scheme test | 6 red / 143 green: the jdbc:postgresql://…, redis:cache and jdbc:x/app.wasm.db rows, in both files |\n| R1-A2 | the opaque test widened to a one-letter scheme | 2 red: C:\\data\\app and C:/data/app |\n| R1-B1 | the sqlite arm back to stripping sqlite: in place of normalizeDatabaseUrl | 5 red: sqlite://x.db, sqlite:///var/lib/objectstack/x.db, SQLITE://x.db (sqlite:x.db stays green, since the old strip handled it) |\n| R1-B2 | the sqlite-wasm arm back to stripping sqlite: | 2 red: sqlite://x.wasm.db, in both files |\n| R1-C | the *.wasm.db test moved back above every scheme check | 4 red: redis://h/app.wasm.db and jdbc:x/app.wasm.db, in both files |"
},
{
"old": "## Tests and gates (head 683bf9587)",
"new": "## Tests and gates (head 40fe66778)"
},
{
"old": "- pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: 295 files, 4399 tests passed.",
"new": "- pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2, in two shards (--shard=1/2, --shard=2/2): 148 + 147 = 295 files, 2464 + 1953 = 4417 tests passed."
},
{
"old": "- pnpm --filter @objectstack/cli typecheck: exit 0. tsc --noEmit covers src/** tests, then check:test-typecheck ran OK.",
"new": "- pnpm --filter @objectstack/cli typecheck: exit 0. tsc --noEmit --listFiles lists both changed test files (include: [\"src\"], no exclude), then check:test-typecheck ran OK."
},
{
"old": "- node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 64 commands, run in order, each exit captured before any pipe. 62 exited 0. 2 exited 3 (PREREQUISITE NOT MET) and are NOT MEASURED: check:dual-build-cjs-loads and check:i18n-coverage. Both read the built output of the whole workspace, and this local run built only the @objectstack/cli closure. CI runs both. --ran reconciliation: 64 derived, 62 run, 2 NOT-MEASURED, 0 UNRUN. The derivation read a tree 3 commits behind origin/main. The one derived-from file that moved there is scripts/sdui-manifest.record.json (an objectui pin bump).",
"new": "- node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 64 commands, run in order, each exit captured before any pipe. All 64 exited 0, check:dual-build-cjs-loads (110 require entry points across 68 packages load) and check:i18n-coverage (14 configs, none new) included. --ran reconciliation: 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. The derivation read a tree 19 commits behind origin/main, with 4 derived-from files moved there. The same derivation over the same 4 paths, in a throwaway tree at origin/mainec44cf60b, printed the identical 64 commands."
},
{
"old": "## Acceptance notes",
"new": "## Acceptance notes\n\n- Round 1: the *.wasm.db rule goes past the REWORK's two asks, in ask ①'s class. A .wasm.db name tested above every scheme check rescued redis://h/x.wasm.db and jdbc:x/x.wasm.db into a SQLite file. The CLI host now refuses both, while the runtime host still accepts redis://h/x.wasm.db (measured above). The seat records that host's answer on #23765.\n- Round 1: the write-failure lines of the file:// probe below carry a [driver-sqlite-wasm] prefix while the banner reads SqlDriver(better-sqlite3). Observed once, not investigated. Carrier: none."
},
{
"old": "## Found while measuring, for the seat to file (not fixed here)",
"new": "## Found while measuring\n\nRound 0's three findings, as the seat dispositioned them: 1 is #23764; 2 is folded into this PR (the alias rule above); 3 is #23765, whose CLI half (the refusal) lands here.\n\nRound 1 found one more, for the seat (not changed here). The CLI host reads file://NAME as //NAME, a file at the filesystem root, where the runtime host opens ./NAME. Measured with OS_DATABASE_URL=file://proc/os23633-probe.db os serve --dev over a host config: ✓ Server is ready, banner → file://proc/os23633-probe.db, and every flush of the database failed with ENOENT on //proc/os23633-probe.db, so it answered from memory only. The runtime host, over a plain config, opened ./proc/os23633-probe.db. The cause is the file: strip in both sqlite arms of resolveStorageDefinition, unchanged from main; the runtime host strips file: and an optional //.\n\nRound 0's text, as written then:"
}
],
"open_questions": [
{
"question": "The CLI host opens file://NAME as //NAME, at the filesystem root, where the runtime host opens ./NAME (out_of_scope_findings[0]). Fold it into this PR, or file it?",
"options": [
"A: fold, as a round 2. Strip file: and an optional // in both sqlite arms, and pin file://x.db and file:///abs/x.db in the parity alias block. The bounded in-place exemption's four conditions hold: the same defect class as ask ②, a mechanical fix whose form the runtime host's rule pins, a file this claim holds, and the same gate batch. But the strip would be a second copy of the runtime host's rule, since sqliteFilenameFromUrl is not exported and this card forbids a packages/runtime edit.",
"B: the seat files it, and this PR lands as it stands. The card's fix exports the runtime host's filename rule (sqliteFilenameFromUrl) and has both CLI sqlite arms call it, as ask ② did with normalizeDatabaseUrl."
],
"recommendation": "B. Business need: real but unpulled. The public door measured a silent non-persistent boot, yet no producer in the repo writes a relative file:// database URL (git grep over DATABASE_URL / databaseUrl spellings: 0 hits), so it need not hold up a landing the seat wants before sign-off. Long term: B keeps one rule, the REWORK's own line against a second copy, and A writes exactly that copy. AI error: one shared rule is the structural guard, and two copies are the drift this card exists to close. Startup focus: neither option adds surface, and B moves no scope into a finishing round. Decided by the Zone-1 ruling 'Nothing else moves', so not changed here."
}
],
"out_of_scope_findings": [
"class: a · reach: public door measured at 40fe667. OS_DATABASE_URL=file://proc/os23633-probe.db os serve --dev over a host config gave 'Server is ready', banner '→ file://proc/os23633-probe.db', then '[driver-sqlite-wasm] Writing //proc/os23633-probe.db to disk failed (ENOENT ...). Writes since the last successful flush are NOT on disk: the database keeps answering from memory and looks healthy'. With a writable name the database would land at the filesystem root; this measurement aimed at /proc so that nothing was written under /, and nothing was. The runtime host (a plain config, same URL) booted ready on ./proc/os23633-probe.db under the cwd · evidence: storage-driver.ts:524-526 and :542-544 strip only /^file:/, while the runtime host's sqliteFilenameFromUrl (standalone-stack.ts:552, :554) strips /^file:(\/\/)?/. The same line is on origin/main, 683bf95 and 40fe667, so it is published and round 1 left it unchanged. It is the file: twin of the folded sqlite:// finding · dedupe words: file:// double slash filesystem root CLI; resolveStorageDefinition file prefix strip; sqlite file URL two slashes absolute path; os serve file:// database not persisted",
"carrier: none · noted, not filed: the write-failure lines of that file:// probe carry a [driver-sqlite-wasm] prefix while the banner reads SqlDriver(better-sqlite3). Observed once, not investigated.",
"carrier: none · noted, not filed: round 0's two carrier-none notes (the serve.ts step-2 comment's scheme list; cli.mdx:522) stand as the seat dispositioned them."
]
}
Landed: PR #23761 → b485122a7f (#23633). The card is closed completed, and pm:dispatched is stripped
domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG · 2026-10-16T03:26Z · holder of claim 6204162580, released by this landing.
reading
value
queue
ready and auto-merge armed 03:02Z, enqueued 03:04Z, merged 03:24Z
origin/main
b485122a7f: one parent (a queue squash), an ancestor of origin/main, 4 files +369 / −14, identical to the reviewed head 40fe66778; no queue branch left
card
closed completed by the PR's Fixes #23633; pm:dispatched removed by targeted delete
Review chain:
round 0, report 6205322639;
REWORK 6205385818;
round 1, pushed by the round-1 dev before a container restart, verified and reported by a finisher (6206960203);
ACCEPT 6207056808.
No contract review was owed, read under contract-review.md at 29c7157af.
What landed:os serve's storage resolution (inferDriverTypeFromUrl, resolveStorageDefinition):
refuses an unrecognised <scheme>:// and an opaque scheme, naming the scheme and never the URL;
reads pg:// and a scheme-less path as the runtime host does;
opens sqlite://x.db as ./x.db through the runtime host's normalizeDatabaseUrl;
Activity
objectstack-fleet commentedon Oct 15, 2026
Triage: first grade ·
bug·priority:p3·domain:cli·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-15T09:02ZRead fresh.
serve --devwith anOS_DATABASE_URLwhose scheme the CLI does not recognise (redis://…) boots in silence on an in-memory SQLite store. This was measured at the public door.inferDriverTypeFromUrlanswers''for the scheme, andresolveStorageDefinitionfalls through to the dev:memory:default.OS_DATABASE_DRIVER.Grade p3:
--devonly. In production the same URL now meets PR fix(cli): a production serve with no storage says so before any plugin starts, and refuses when its auth service needs a driver #23631's refusal.→ :memory:.Lane
domain:cli(packages/cli/src/utils/storage-driver.ts).Scope:
--devand production.postgres://andfile:URL still resolve.No order: PR #23631 (merged 08:54Z) touched only
serve.ts.findingleaves on grading.objectstack-fleet commentedon Oct 15, 2026
Claim: PM loop round 4, batch 3 (the maintainer's 「并发恢复3」 in this seat's chat, about 22:35Z on 2026-10-15; recorded in seat post #6024 §1). Label order: the oldest unclaimed
bugatpriority:p3indomain:cli's open queue; no p0,pm:blocking, opentarget:, p1 or p2 card is open for dispatch (#23497 ispm:blocked; #23050 is seat 2's). #23633:objectstack serverefuses anOS_DATABASE_URLwhose scheme it does not recognise, as its driver arm and the runtime's standalone host already do, instead of booting--devon an in-memory SQLite store in silence · 2026-10-15T23:08ZSession:
session_01FLwgbTSmGMrqsXkBDsiwfGAccount:
os-bill(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23633-unrecognised-database-url-schemeWorktree:
objectstack-issue-23633Domain:
domain:cliSeat:
domain:cli#1(seat post #6024)File surface (read on
origin/main9bb93706fb; stop on a breach and explain it in the report):packages/cli/src/utils/storage-driver.ts:inferDriverTypeFromUrl(:255), andresolveDriverType/resolveStorageDefinition(:293,:364) only as far as the refusal needs;packages/cli/src/**(new test files; ⛔packages/cli/src/commands/serve.tsis held by cli/serve: with --no-server the ready banner still prints "API: http://localhost:PORT/" and an MCP connect URL, though no HTTP server is composed and nothing listens #23623 and is not edited), and one changeset.packages/runtime/src/standalone-stack.ts'sdetectDriverFromUrl(:399), the runtime host's answer to the sameOS_DATABASE_URL.Container & model:
M(one refusal measured at the public door in--devand production, and its agreement with the runtime host's scheme set),mode:subagent,model: TIER_DEFAULT(dispatch-gates --tier --repo objectstack-ai/objectstackover the path at9bb93706fb: "no path-derived mandate … The tier stays the PM's per-card judgment call")Clause-②: no
Basis for the Clause-② line: the accepted set is the documented one.
content/docs/deployment/environment-variables.mdx:49listsfile:,:memory:,postgres://,mysql://,mongodb://andlibsql:///*.turso.io, and the runtime host already refuses any other scheme (detectDriverFromUrl). The CLI's--devarm silently booted on inputs outside that set; refusing them moves no declared accept set. ⛔ A URL the runtime host accepts is not refused here. No export is added (UnsupportedDriverErroris the existing refusal), and there is nopackages/specedit.Responsibility:
packages/cli's storage resolution forserve, platform code in this lane | the banner'sDriver: … → :memory:row is the only trace, and nothing says the URL was ignored | an operator who names a database the CLI does not recognise, inos serve --devandos dev; measured at the public door (OS_DATABASE_URL=redis://127.0.0.1:6399/0 objectstack serve --dev) by seat 2's filing from cli/serve: a productionobjectstack servewith no OS_DATABASE_URL registers no driver in silence, then fails at AuthPlugin.start with "No driver available for object 'sys_organization'" and a remedy naming a datasource nobody declared #23573's build reportThread-read: 6188444478
Serial constraints cleared, and the landing order:
storage-driver.ts(every open PR's file list was read in this act), and nopm:dispatchedclaim names it. cli/serve: with --no-server the ready banner still prints "API: http://localhost:PORT/" and an MCP connect URL, though no HTTP server is composed and nothing listens #23623 (this seat, in flight) holdsserve.tsandutils/format.ts, which this card does not edit.Restart-touch:line on an openpm:on-holdcard names the file.serve.ts.Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
os-dev-report
{
"issue": 23633,
"status": "done",
"branch": "claude/issue-23633-unrecognised-database-url-scheme",
"pr": "#23761",
"session": "session_01FLwgbTSmGMrqsXkBDsiwfG (the parent's session; this run is mode:subagent)",
"premise_still_valid": true,
"summary": "Premise held, measured at the public door on base 3b46855: OS_DATABASE_URL=redis://127.0.0.1:6399/0 os serve --dev over a host config printed 'Server is ready' with 'Driver: SqlDriver(better-sqlite3) → :memory:'. Production met the no-storage refusal. os dev did the same through its serve --dev child, after printing 'Database: redis://127.0.0.1:6399/0'. os start over a host config met the no-storage refusal. inferDriverTypeFromUrl (packages/cli/src/utils/storage-driver.ts) now throws UnsupportedDriverError (recognized: false) for any SCHEME:// no arm recognises. The message names OS_DATABASE_URL, the scheme (never the URL, so credentials are not printed) and the recognised spellings in the runtime host's order. Per the seat's reading, the function also takes the runtime host's arms character for character: pg:// is postgres, and a value with no SCHEME:// is a SQLite file. Before, both booted --dev on :memory: in silence and met the no-storage refusal in production. A .db ending no longer rescues an unrecognised scheme. A blank URL stays absent. An explicit OS_DATABASE_DRIVER still wins. serve.ts is not edited: its resolveDriverType call sits above the storage try, so the throw reaches the fatal handler and the boot exits 1 in both postures (measured). Draft PR #23761 is assigned to os-bill. CI was in_progress when this report was written.",
"tests": "Unit pins under packages/cli/src only. storage-driver.test.ts covers pg:// and PG:// → postgres; 4 scheme-less paths → sqlite; '' only for an absent or blank URL; 5 refused URLs (redis://h/app.db among them), each checked for the type, recognized false, driverType = the scheme, and that the message names OS_DATABASE_URL, the scheme and recognised spellings; no credentials or host in the refusal; and an explicit driver winning over redis://. The old row pinning inferDriverTypeFromUrl('redis://localhost') === '' was replaced. driver-vocabulary-parity.test.ts gains a new block that drives both hosts (resolveStandaloneDatabase against resolveStorageDefinition(resolveDriverType(undefined,url))). It has 19 runtime-accepted URLs, each accepted as the same kind and built from that URL (displayUrl === url) with isDev false and true; 5 URLs both hosts refuse, in dev and prod, the CLI refusal being the typed one; and 2 opaque-scheme values on which the hosts must agree. Runs (verify lock): the 2 files 129/129 passed. pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 at head 683bf95: 'Test Files 295 passed (295)', 'Tests 4399 passed (4399)'. pnpm --filter @objectstack/cli typecheck: exit 0 (tsc --noEmit covers src tests, then 'check:test-typecheck: OK'). The integration layer is declared to CI: the diff touches no integration-tier file and no spawn or boot entry. Ablation, through scripts/ablation-replace.mjs WRAP mode against src (the tests import ./storage-driver.js relatively, so no dist is involved), each leg with on-disk proof and its restore proven as blob == HEAD 8100f9353946 with an empty git diff HEAD: A1 return '' for an unrecognised scheme, 12 red / 117 green; A2 pg dropped from the postgres arm, 3 red; A3 the old .db / :memory: arm restored in place of the scheme-less arm, 8 red; A4 blank guard removed, 1 red. The first A1 attempt was a no-op, refused by the tool because the anchor was contained in the replacement. It was re-run with a non-overlapping replacement. Real boots before and after are in before_after.",
"gates": "Local, head 683bf95. eslint: pnpm exec eslint packages/cli --no-inline-config, exit 0, 696 files, 0 errors, 0 warnings (JSON formatter). node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 64 commands, each run once with its exit captured before any pipe. 62 exited 0. 2 exited 3 (PREREQUISITE NOT MET, so NOT MEASURED): pnpm check:dual-build-cjs-loads and pnpm check:i18n-coverage. Both need the whole workspace built, and this run built only the @objectstack/cli closure; CI runs both, as a declared narrowing. --ran: 'Run reconciliation — 64 derived, 62 run, 2 NOT-MEASURED, 0 UNRUN', exit 0. Its STALE TREE line: 3 commits behind origin/main 140238a, and the derived-from file that moved is scripts/sdui-manifest.record.json, an objectui pin bump. Not re-merged: main brought #23741 (serve.ts banner, format.ts), whose diff does not touch resolveDriverType or resolveStorageDefinition. Not owed: the local-only check:dev-prereqs and check:skill-frame-freshness (no packages/spec or framework edit), and a script's own suite (no gate or tool script edited). CI check-runs on 683bf95 at report time (00:4xZ). success: Build Core, Check Changeset, Check PR Size, Governed Surface Queue Guard, Lint · ESLint, Lint · PM dispatch-gates, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Type Check · workspace, Type Check · debt ledger, Test Core (5/6), Test Core (6/6), Dogfood Regression Gate, Auto Label, Check Documentation Links, Flag docs affected by code changes, filter, the three claim/closing guards. skipped: Build Docs, Console Pin Gate, Dogfood Verify CLI, the Dogfood shards, Packed-tarball smoke, Temporal Conformance. in_progress: Test Core (1/6 to 4/6), Type Check · source gates, Type Check · consumer gates, Lint · CI, release & engine gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure. No red.",
"line_budget": "n/a",
"mcp_calls": "0",
"api_writes": "2 relay strokes (each one POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write as objectstack-fleet[bot]). Stroke 1, pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/23761/assignees (os-bill); read-back identical, 12254 bytes, assignees os-bill. Stroke 2: POST /repos//issues/23633/comments, this report. git push is not counted: the empty probe branch plus 4 pushes of the branch.",
"deviations": [
"No real-boot pin landed, against the PM's Zone 3 suggestion. Its home is packages/cli/test, and a new spawner there also needs a roster entry in scripts/check-cli-test-child-env.mjs. Both are outside the file surface. The triage ruling 'Pin --dev and production' is met at the unit layer, with every refused and accepted row run at isDev false and true, which is the layer the existing memory:// and unknown-driver refusals are pinned at. The boots were measured by hand (before_after). See open_questions[1].",
"Four probe files are left at the container root: /os23633-probe.db, /os23633-probe.telemetry.db, /os23633-probe.telemetry.db-shm and /os23633-probe.telemetry.db-wal. They were written by the sqlite:// measurement (out_of_scope_findings[1]). The rm was refused by the harness safety check for a root path, so they need a human removal.",
"The first A1 ablation leg was a no-op, refused by ablation-replace.mjs because the anchor was a substring of the replacement. It was declared and re-run with a non-overlapping replacement.",
"Zone 2. H1 confirmed. H2 confirmed with one correction: the throw comes from resolveDriverType at serve.ts:3101, which is ABOVE the try, so it reaches run()'s fatal handler directly (the catch would also re-throw UnsupportedDriverError). The memory:// boot (exit 1) was the control. H3 is partly falsified: os start over a HOST config reaches this path through its production serve child, measured exit 1 on the no-storage refusal before and the scheme refusal after. Over a plain config it goes through the runtime host as stated.",
"The harness attribution reminder named a model in Co-Authored-By. Commits carry the model-free pair the dispatch and AGENTS.md require.",
"The branch merged origin/main once (683bf95) before the full unit tier ran. origin/main then moved 3 commits, including #23741, a cli serve.ts and format.ts change outside resolveDriverType and resolveStorageDefinition. It was not re-merged before the PR opened; PR CI tests the merge ref."
],
"files_changed": [
".changeset/23633-unrecognised-database-url-scheme.md",
"packages/cli/src/utils/driver-vocabulary-parity.test.ts",
"packages/cli/src/utils/storage-driver.test.ts",
"packages/cli/src/utils/storage-driver.ts"
],
"before_after": {
"fixture": "a throwaway defineStack HOST config (one object, one instantiated no-op plugin). Built CLI node packages/cli/bin/run.js, NODE_ENV unset, OS_CLOUD_URL=off, auth secret set. Before = base 3b46855; after = branch 5493312, built.",
"redis_dev": {
"before": "no exit (the boot reached its banner): 'Server is ready', 'Driver: SqlDriver(better-sqlite3) → :memory:'",
"after": "exit 1: 'Unsupported database URL scheme "redis://" (OS_DATABASE_URL / --database). Supported: postgres:// (or postgresql://, pg://), mysql:// (or mysql2://), mongodb:// (or mongodb+srv://), libsql:// or an https:// .turso. URL (optional @objectstack/driver-turso), wasm-sqlite://, file: (or sqlite:), or a file path with no scheme; :memory: is SQLite's in-memory database. Booting on the SQLite default instead would silently ignore the database you named and write into one you never chose. Fix the URL, or unset it.'"
},
"redis_prod": {
"before": "exit 1: 'This production serve has no storage: no recognized OS_DATABASE_URL declared a default datasource ...' then 'Refusing to boot: this production serve has no storage, ... Set OS_DATABASE_URL, mount a storage driver plugin, or run with --dev.'",
"after": "exit 1: the same 'Unsupported database URL scheme "redis://" ...' sentence"
},
"redis_os_dev": {
"before": "banner 'Driver: SqlDriver(better-sqlite3) → :memory:' after 'Database: redis://127.0.0.1:6399/0'",
"after": "exit 1, the scheme sentence"
},
"redis_os_start": {
"before": "exit 1, the no-storage refusal",
"after": "exit 1, the scheme sentence"
},
"redis_with_credentials_dev": {
"before": "banner → :memory:",
"after": "exit 1, the scheme sentence; the password occurs 0 times in the output"
},
"pg_scheme": {
"before": "dev banner → :memory:; prod exit 1 no-storage refusal",
"after": "dev and prod exit 1 'datasource default: connect failed — connect ECONNREFUSED 127.0.0.1:5999' (the URL is used)"
},
"bare_path": {
"before": "./data/app dev banner → :memory:; prod exit 1 no-storage refusal",
"after": "dev 'Server is ready', '→ ./data/app', file exists; prod './data/app2' 'Server is ready', '→ ./data/app2'"
},
"controls": "memory:// dev exit 1 retirement sentence, unchanged. postgres://...:5999 dev exit 1 ECONNREFUSED, unchanged. file:./ctl.db prod 'Server is ready' '→ file:./ctl.db', unchanged. After only: OS_DATABASE_DRIVER=postgres with redis://...:6399 dev exit 1 ECONNREFUSED 127.0.0.1:6399, so the explicit driver wins and the scheme is not refused. A plain config (runtime host) redis:// dev exit 1 '[StandaloneStack] Unsupported database URL scheme: ...' both before and after."
},
"schemes": [
"Columns: CLI host (resolveStorageDefinition(resolveDriverType(undefined,url))) dev/prod before → after | runtime host (resolveStandaloneDatabase); measured with packages/cli/dist before and after the fix",
"postgres://, postgresql:// | postgres/postgres → postgres/postgres | postgres",
"pg://, PG:// | :memory: default/null → postgres/postgres | postgres",
"mysql://, mysql2:// | mysql/mysql → unchanged | mysql",
"mongodb://, mongodb+srv:// | mongodb/mongodb → unchanged | mongodb",
"libsql://, https://.turso.io | turso/turso → unchanged | turso",
"wasm-sqlite://, *.wasm.db | sqlite-wasm/sqlite-wasm → unchanged | sqlite-wasm",
"memory://, mingo:// | refused/refused → unchanged | refused",
"file:, sqlite:, sqlite://, :memory:, *.db, *.sqlite, *.sqlite3 | sqlite/sqlite → unchanged | sqlite",
"./data/app, /abs/app, app, C:\data\app, C:/data/app (scheme-less, no db extension) | :memory: default/null → sqlite/sqlite | sqlite",
"redis://, REDIS://, rediss://, http(s):// (not turso), ftp://, mssql://, sqlserver://, oracle://, cockroachdb://, mariadb:// | :memory: default/null → refused/refused | refused",
"redis://h/app.db (unrecognised scheme, .db ending) | sqlite/sqlite (a file at that path) → refused/refused | refused",
"jdbc:postgresql://h/db, redis:h (a scheme with no //) | :memory: default/null → sqlite/sqlite | sqlite (out_of_scope_findings[2])",
"blank ' ' | :memory: default/null → unchanged | the unified default file"
],
"open_questions": [
{
"question": "An opaque-scheme value (jdbc:postgresql://h/db, redis:h) has no SCHEME:// and so passes the runtime host's scheme-less arm. That host opens it as a SQLite file and creates bogus directories (out_of_scope_findings[2]). Under the ruling that the CLI never refuses what the runtime host accepts, the CLI host now mirrors it, where before it booted :memory:. Keep the mirror?",
"options": [
"A: keep the mirror (this PR). Fix finding 2 on both hosts at once, with the parity row holding them together.",
"B: the CLI returns '' for these, which is the old silent :memory: in dev.",
"C: the CLI alone refuses them, which breaks the ruling."
],
"recommendation": "A. Business need: no measured producer of such URLs in the repo, so the row is about safety, not a feature. Long term: one answer per URL across both hosts, and the parity row reds whichever host moves first. AI error: one dialect, not two. Startup focus: no new surface. B keeps a silent wrong store, and C forks the hosts."
},
{
"question": "Is a real-boot pin owed beyond the unit pins, for redis:// under --dev and production?",
"options": [
"A: no. The unit pins run every row with isDev false and true, the layer the existing memory:// and unknown-driver refusals use.",
"B: two legs in packages/cli/test/serve-no-storage-boot-guard.integration.test.ts. That file is already on the child-env roster, but it is outside this card's surface, and its test-budget header must be re-measured."
],
"recommendation": "A. The exit path (resolveDriverType above serve's try) is shared with the memory:// refusal and was measured by hand both ways. B adds CI wall to a queue-tier integration file for a path no other refusal pins at that layer."
}
],
"out_of_scope_findings": [
"class: a · reach: public door measured, os dev --no-watch over a plain config with OS_DATABASE_URL=redis://admin:PASSWORD@127.0.0.1:6399/0; os dev's own 'Database:' row redacts it, then the runtime refusal prints '[StandaloneStack] Unsupported database URL scheme: redis://admin:PASSWORD@127.0.0.1:6399/0' in full; also exception: security (credential in a boot log) · evidence: packages/runtime/src/standalone-stack.ts detectDriverFromUrl throw interpolates dbUrl; measured on branch head 5493312, runtime unchanged from main · dedupe words: unsupported database URL scheme prints password; standalone-stack refusal credential leak; detectDriverFromUrl full URL in error",
"class: a · reach: public door measured, OS_DATABASE_URL=sqlite://os23633-probe.db os serve --dev over a host config: banner '→ sqlite://os23633-probe.db', database written to /os23633-probe.db (filesystem root); the runtime host normalises to file: and resolves ./os23633-probe.db · evidence: resolveStorageDefinition sqlite arm strips 'sqlite:' and leaves '//x.db' (sqlite:///tmp/x.db gives '///tmp/x.db'); resolve-project-database.ts normalises sqlite:// on the runtime side only · dedupe words: sqlite:// double slash absolute path; CLI sqlite alias not normalised to file:; resolveStorageDefinition sqlite filename root",
"class: a · reach: public door measured, OS_DATABASE_URL=jdbc:postgresql://127.0.0.1:5999/db os serve --dev over a plain config (runtime host): 'Server is ready', '→ jdbc:postgresql://127.0.0.1:5999/db', and created ./jdbc:postgresql:/127.0.0.1:5999/db, against the module header's 'Unknown URL schemes throw — we never silently fall back to sqlite' · evidence: standalone-stack.ts detectDriverFromUrl scheme-less arm tests only SCHEME:// presence; the CLI host mirrors it by this card's ruling and the new parity row holds them together, so the fix is both hosts at once · dedupe words: jdbc URL treated as sqlite file; opaque scheme bare path arm; detectDriverFromUrl bogus directory",
"carrier: none · noted, not filed: serve.ts step-2 comment block lists the URL-scheme priority and now omits pg://, the scheme-less arm and the unrecognised-scheme refusal (incomplete, not false; serve.ts held by #23623)",
"carrier: none · noted, not filed: content/docs/deployment/cli.mdx:522 'otherwise sqlite' was already untrue for the runtime host before this change, which does not make it false; no docs edit"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
os-dev-report
{
"issue": 23633,
"status": "done",
"round": 1,
"branch": "claude/issue-23633-unrecognised-database-url-scheme",
"head": "40fe66778",
"pr": "#23761",
"session": "session_01FLwgbTSmGMrqsXkBDsiwfG (the parent's session; this run is mode:subagent, the round-1 finisher)",
"premise_still_valid": true,
"summary": "Round 1 verified at head 40fe667, which the previous run pushed before a container restart killed it. Both REWORK asks are met. inferDriverTypeFromUrl refuses an opaque scheme (two or more characters and a colon, other than file: and sqlite:) with the same UnsupportedDriverError and sentence, and a one-letter drive stays a path. The sqlite and sqlite-wasm arms of resolveStorageDefinition translate sqlite: / sqlite:// through the runtime host's normalizeDatabaseUrl. Every gate, pin and measurement passed at that head, so this run wrote no code and pushed nothing. Beyond the asks, a .wasm.db name now selects the WASM engine only for a file, so it no longer rescues an unrecognised scheme (deviations[0]). One new finding: the CLI host opens file://NAME at the filesystem root (out_of_scope_findings[0], open_questions[0]).",
"rework_asks": {
"ask_1_opaque_scheme_refused": {
"verdict": "met",
"lines": "packages/cli/src/utils/storage-driver.ts:352
const scheme = /^[a-z][a-z0-9+.-]*:\\/\\//i.exec(u)?.[0] ?? /^[a-z][a-z0-9+.-]+:/i.exec(u)?.[0];(the second test needs two or more characters before the colon, so a one-letter drive is no scheme); :353-359 throw UnsupportedDriverError(scheme, unsupportedUrlSchemeMessage(scheme), { recognized: false }), the same type and sentence as the SCHEME:// refusal; :340 returns file: / sqlite: before it.","pins": "storage-driver.test.ts:120-121 (jdbc:postgresql://localhost:5432/db, redis:cache refused at inference: type, recognized false, driverType = the scheme, and the message names OS_DATABASE_URL, the scheme and the recognised spellings); :93 (C:\data\app and C:/data/app stay sqlite paths). driver-vocabulary-parity.test.ts:498-499 (the CLI host refuses both in dev AND prod; the runtime host's answer is not pinned, and #23765 is named at :495).",
"proof": "Ablation R1-A 6 red / 143 green, R1-A2 2 red. Public door at 40fe667: jdbc:postgresql://127.0.0.1:5999/db and redis:h exit 1 in dev and production, naming "jdbc:" and "redis:", with no file written."
},
"ask_2_sqlite_alias_translated": {
"verdict": "met",
"lines": "packages/cli/src/utils/storage-driver.ts:107 imports normalizeDatabaseUrl from @objectstack/runtime, so there is no second copy of the rule; :524 the sqlite arm,
normalizeDatabaseUrl(databaseUrl ?? ':memory:'); :542 the sqlite-wasm arm, the same rule.","pins": "driver-vocabulary-parity.test.ts:519-521 (sqlite://x.db, sqlite:x.db, sqlite:///var/lib/objectstack/x.db: the runtime host's sqliteFile equals the CLI host's config.filename, in dev and prod), plus :522 (sqlite://x.wasm.db). storage-driver.test.ts:274-277 (also SQLITE://x.db; displayUrl keeps the typed URL), :286.",
"proof": "Ablation R1-B1 5 red, R1-B2 2 red. Public door at 40fe667: sqlite://x.db boots to ready in dev and production and creates ./x.db in the probe directory, where base 683bf95 resolved //x.db, the filesystem root.",
"changeset": "updated for both asks (d343c71) and for the .wasm.db rule (40fe667)",
"untouched": "serve.ts, packages/runtime and packages/spec are not in the diff (4 files, read from git off merge base 53646fc). Clause-②: no stays line 2 of the PR body."
}
},
"tests": "At 40fe667, every heavy run through os-verify-lock, each verdict read from its VERDICT command-exit line. (1) Closure build: turbo run build --filter=@objectstack/cli... --concurrency=2, exit 0, 61/61 cached. packages/cli/dist/utils/storage-driver.js carries the round-1 code, by on-disk counts: normalizeDatabaseUrl(databaseUrl 2, wasmFile 3, the opaque regex 1, the old sqlite: strip 0. (2) Unit layer, two shards: pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 --shard=1/2, 'Test Files 148 passed (148)', 'Tests 2464 passed (2464)'; --shard=2/2, 'Test Files 147 passed (147)', 'Tests 1953 passed (1953)'. Total 295 files, 4417 tests, against 4399 at 683bf95. The integration layer is declared to CI: no integration-tier file, spawn entry or driver/kernel boot path is in the diff. (3) pnpm --filter @objectstack/cli typecheck: exit 0 ('check:test-typecheck: OK'). tsc --noEmit --listFiles names both changed test files (include src, no exclude). (4) The two pinned files: 'Tests 149 passed (149)', the baseline for the ablation legs.",
"gates": "Local, at 40fe667. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, derived 64 commands from 4 paths off merge base 53646fc. Each ran in order, its exit captured before any pipe and recorded as 'CMD :: exit N': all 64 exited 0. That includes check:dual-build-cjs-loads ('110 published require entry point(s) across 68 package(s) load') and check:i18n-coverage ('OK (14 config(s), 621 baselined untranslated string(s), none new)'), the 2 NOT MEASURED in round 0. --ran: 'Run reconciliation — 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN', exit 0. Its STALE TREE line reads 19 commits behind origin/main, with 4 derived-from files moved (discovery-routes anchor, check-route-envelope, record-recognisers, sdui-manifest record). Control: the same derivation over the same 4 paths in a throwaway detached tree at origin/main ec44cf6 printed the identical 64 commands (diff exit 0). pnpm exec eslint packages/cli --no-inline-config: exit 0, 696 files, 0 errors, 0 warnings (JSON formatter). pnpm --filter @objectstack/cli typecheck: exit 0. Not owed: check:dev-prereqs and check:skill-frame-freshness (no packages/spec or framework edit), and a script's own suite (no gate or tool script edited). test:repo is CI's. CI check-runs on 40fe667, 40 in all. success (34): Auto Label, Build Core, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Test Core, Test Core (1/6) to (6/6), The card this PR closes must claim this branch, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter. skipped (6): Build Docs, Console Pin Gate, Dogfood Regression Gate (matrix shard), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Temporal Conformance (live PG + MySQL). None red, and none in progress.",
"ablation": [
"Tool: scripts/ablation-replace.mjs in WRAP mode, the wrapped command being the two pinned files under os-verify-lock. The subject is src, because both files import ./storage-driver.js relatively, so no dist/ is involved. Every leg: the tool proved the mutation on disk (anchor x1 → x0, a new blob) and the restore (blob == HEAD 893ea05be3ed, git diff HEAD empty). An outer trap in the leg script proved the restore again by hash and diff, and an on-disk grep found 0 ABLATION markers after each leg. Baseline 149 passed.",
"R1-A, the opaque-scheme refusal: the opaque fallback deleted from the scheme test. 6 red / 143 green, the jdbc:postgresql://..., redis:cache and jdbc:x/app.wasm.db rows in both files.",
"R1-A2, the drive boundary (added; see deviations): the opaque test widened from + to * (one-letter schemes). 2 red: C:\data\app and C:/data/app (storage-driver.test.ts:93).",
"R1-B1, the alias translation in the sqlite arm: normalizeDatabaseUrl replaced by base's .replace(/^sqlite:/, ''). 5 red: sqlite://x.db and sqlite:///var/lib/objectstack/x.db in both files, and SQLITE://x.db. sqlite:x.db stays green, because base's strip already handled it.",
"R1-B2, the alias translation in the sqlite-wasm arm, same mutation: 2 red, the sqlite://x.wasm.db rows in both files.",
"R1-C, the .wasm.db-only-for-a-file rule: the .wasm.db test moved back above every scheme check. 4 red: redis://h/app.wasm.db and jdbc:x/app.wasm.db in both files."
],
"before_after": {
"fixture": "the built CLI (node packages/cli/bin/run.js serve objectstack.config.mjs [--dev] --port RANDOM) over a throwaway HOST config: one object and one plain-object plugin with init, no imports. The cwd is a fresh directory under the scratch dir, NODE_ENV and OS_DATABASE_DRIVER unset, OS_CLOUD_URL=off, an auth secret set, timeout 60 s, killed on 'Server is ready'. Before = 683bf95: storage-driver.ts restored to that commit in the worktree (unstaged, swap proven by blob 8100f9353946), built (dist counts: normalizeDatabaseUrl 0, wasmFile 0, opaque regex 0, sqlite: strip 2), then restored to HEAD (blob 893ea05be3ed == HEAD, git diff HEAD empty, porcelain empty) and rebuilt (61/61 cached, dist counts back to 2/3/1/0). After = 40fe667, built.",
"jdbc_postgresql": {
"before": "dev and production: 'Server is ready', 'Driver: SqlDriver(better-sqlite3) → jdbc:postgresql://127.0.0.1:5999/db', created ./jdbc:postgresql:/127.0.0.1:5999/db",
"after": "dev and production: exit 1, 'Unsupported database URL scheme "jdbc:" (OS_DATABASE_URL / --database). Supported: ...', no file"
},
"redis_opaque": {
"before": "dev and production: 'Server is ready', '→ redis:h', created ./redis:h",
"after": "dev and production: exit 1, the same sentence naming "redis:", no file"
},
"sqlite_double_slash": {
"before": "not booted, because a boot writes /x.db at the filesystem root. Read from the built module: config.filename '//x.db' in dev and prod, while the runtime host answers 'x.db'.",
"after": "dev and production: 'Server is ready', 'Driver: SqlDriver(better-sqlite3) → sqlite://x.db', created ./x.db in the probe directory under the scratch dir, never at /; built module: filename 'x.db', equal to the runtime host's"
},
"redis_wasm_db": {
"before": "dev and production: 'Server is ready', 'Driver: SqliteWasmDriver → redis://h/x.wasm.db', created ./redis:/h/x.wasm.db",
"after": "dev and production: exit 1, the same sentence naming "redis://", no file"
},
"drive_path_C_data_app": {
"before": "read only, no boot: sqlite, config.filename 'C:/data/app', displayUrl 'C:/data/app', dev and prod; the runtime host answers sqlite 'C:/data/app'",
"after": "unchanged: sqlite, filename 'C:/data/app', dev and prod"
},
"control_postgres": {
"before": "dev and production: exit 1, 'datasource default: connect failed — connect ECONNREFUSED 127.0.0.1:5999'",
"after": "unchanged in both postures"
},
"control_fixture": "after only: file:./ctl.db --dev boots to 'Server is ready', '→ file:./ctl.db', and creates ./ctl.db, which shows the fixture boots.",
"root_check": "after every boot, ls / lists no x., jdbc or redis* entry; the round-0 /os23633-probe* files were not touched"
},
"runtime_wasm_db": {
"url": "redis://h/x.wasm.db",
"read": "resolveStandaloneDatabase from packages/runtime/dist at 40fe667 (standalone-stack.ts and resolve-project-database.ts are identical on merge base 53646fc and origin/main), with OS_DATABASE_URL set: driver 'sqlite-wasm', sqliteFile 'redis://h/x.wasm.db', source 'env'. Mechanism, read and not changed: standalone-stack.ts:428 tests /\.wasm\.db$/i before the scheme-less arm (:430) and before the unsupported-scheme throw. sqliteFilenameFromUrl's sqlite-wasm arm (:548-552) strips only wasm-sqlite: and file:, so the file is named by the whole URL.",
"measured": "a plain config (no plugin instances, so serve boots through the runtime host), cwd a fresh scratch directory: dev and production both 'Server is ready', each writing a 1,499,136-byte database at ./redis:/h/x.wasm.db (directories 'redis:' and 'h' created under the cwd).",
"against_the_cli_host": "the CLI host refuses the same URL at 40fe667 (UnsupportedDriverError, recognized false, scheme "redis://"), in dev and prod. At base 683bf95 it agreed with the runtime host (sqlite-wasm, the same file). This is the departure deviations[0] states; the parity block does not pin the runtime host's answer for it."
},
"mcp_calls": "0",
"api_writes": "1 — one relay stroke (POST /repos/objectstack-ai/objectstack/dispatches, which fleet-write executes as objectstack-fleet[bot]) carrying POST /repos//issues/23633/comments, this report. No git push, label-write or PR-body PATCH. REST reads only otherwise: GET issues/23633/comments (twice), GET issues/23761/comments, GET pulls/23761, GET commits/40fe66778.../check-runs.",
"deviations": [
"The *.wasm.db rule goes past the REWORK's two asks, in ask ①'s class (eff7af2, plus the changeset line in 40fe667). A *.wasm.db name now selects the WASM engine only for a file: / sqlite: URL or a value with no scheme (storage-driver.ts:325, :340, :363). The sqlite-wasm arm takes ask ②'s rule too (:542). Reason: tested above every scheme check (base :317), a .wasm.db name rescued any scheme into a SQLite file, so ask ① alone would have left jdbc:x/app.wasm.db booting. Base 683bf95 booted redis://h/x.wasm.db in both postures on 'SqliteWasmDriver → redis://h/x.wasm.db' (before_after). sqlite://x.wasm.db opened //x.wasm.db there, the filesystem root. Cost: the CLI host now refuses redis://h/x.wasm.db, which the runtime host accepts (runtime_wasm_db). That departs from the dispatch ruling 'the CLI never refuses a URL the runtime host means to accept' in the same way the REWORK revised it for opaque schemes. The seat records the runtime host's answer on #23765.",
"The round-1 code was written by the previous run, which a container restart killed after it pushed 40fe667 and before it verified or reported. This run wrote no code. Every gate, pin and measurement passed at 40fe667, so nothing was committed or pushed.",
"Ablation has five legs where three were asked. The alias leg is split per arm (R1-B1 sqlite, R1-B2 sqlite-wasm). R1-A2, the one-letter-drive boundary, was added because R1-A cannot red the drive pin: those rows pass with or without the opaque fallback.",
"before_after: sqlite://x.db was not booted at base. A base boot writes /x.db at the filesystem root, which the dispatch's probe rule forbids, so the base answer was read from the built module instead ('//x.db'). C:/data/app was read only on both sides, as the dispatch asked.",
"The derivation printed STALE TREE (19 behind origin/main, 4 derived-from files moved). origin/main was not merged: no conflict, main-caused red or queue removal occurred. main's 19 commits touch the CLI's serve.ts and format.ts and the runtime's action-execution, http-dispatcher and sandbox, none of the three files this card rests on. The control derivation ran in a throwaway detached worktree at origin/main in the scratch dir, given exactly the 4 paths the no-path derivation named; it was removed after (git worktree remove, exit 0).",
"The verify lock's last holder before this run, a whole-workspace turbo build in this worktree, was dead (its pid gone). dist/ was not trusted: the closure was rebuilt (61/61 cached), and the round-1 code was confirmed in dist/ by on-disk counts before any boot.",
"The harness attribution reminder names a model in its Co-Authored-By line. This run made no commit, so the line was not used."
],
"files_changed": [
"none in this run (no commit). The PR's files at 40fe667, 4 vs merge base 53646fc:",
".changeset/23633-unrecognised-database-url-scheme.md",
"packages/cli/src/utils/driver-vocabulary-parity.test.ts",
"packages/cli/src/utils/storage-driver.test.ts",
"packages/cli/src/utils/storage-driver.ts"
],
"pr_body_changes": [
{
"old": "- The runtime host's arms are adopted.
pg://is postgres, and a value with noSCHEME://is a SQLite file path, whatever its extension. Both matchdetectDriverFromUrl(packages/runtime/src/standalone-stack.ts, read only) character for character. The runtime host refuses every otherSCHEME://, so the CLI host refuses nothing the runtime host accepts.","new": "- The runtime host's arms are adopted, with one class refused.
pg://is postgres, and a value with no scheme is a SQLite file path, whatever its extension, as indetectDriverFromUrl(packages/runtime/src/standalone-stack.ts, read only). An OPAQUE scheme (two or more characters and a:with no//, such asjdbc:postgresql://h/dborredis:h) is refused with the sameUnsupportedDriverErrorand sentence, naming the scheme (\"jdbc:\"). The runtime host still opens those as a SQLite file at a path made of the URL, against its own rule that an unknown scheme never falls back to SQLite; that gap is #23765's. A one-letter drive is a path, not a scheme (C:\\data\\app,C:/data/app)."},
{
"old": "- A
.dbending no longer rescues an unrecognised scheme. The old*.dbarm ran before any scheme check, soredis://h/app.dbwas handed to SQLite as a file path. It is now refused, as the runtime host refuses it.","new": "- A
.dbor.wasm.dbending no longer rescues an unrecognised scheme. The old*.dbarm ran before any scheme check, soredis://h/app.dbwas handed to SQLite as a file path. It is now refused, as the runtime host refuses it. A*.wasm.dbname likewise selects the WASM engine only for afile:/sqlite:URL or a path with no scheme, soredis://h/app.wasm.dbandjdbc:x/app.wasm.dbare refused too. The runtime host still acceptsredis://h/x.wasm.dbas a WASM SQLite file (measured below); the seat records that host's answer on #23765.\n-sqlite:andsqlite://are aliases offile:, by the runtime host's own rule. The sqlite and sqlite-wasm arms ofresolveStorageDefinitiontranslate the URL throughnormalizeDatabaseUrl(@objectstack/runtime) rather than a second copy of it.sqlite://x.dbandsqlite:x.dbopen./x.db,sqlite:///abs/x.dbopens/abs/x.db, andsqlite://x.wasm.dbopens./x.wasm.dbon the WASM engine. Before,sqlite://x.dbopened//x.db, a file at the filesystem root."},
{
"old": "| a plain config (runtime host),
redis://…,--dev| exit 1,[StandaloneStack] Unsupported database URL scheme: …| unchanged (not this path) |","new": "| a plain config (runtime host),
redis://…,--dev| exit 1,[StandaloneStack] Unsupported database URL scheme: …| unchanged (not this path) |\n\n### Round 1, measured the same way\n\nBefore =683bf9587(round 0's head;storage-driver.tsis the only non-test source file between the two); after =40fe66778. Each boot ran in a throwaway directory, so every file it wrote is listed relative to it.\n\n|OS_DATABASE_URL| Before (dev and production) | After (dev and production) |\n|---|---|---|\n|jdbc:postgresql://127.0.0.1:5999/db|✓ Server is ready,→ jdbc:postgresql://127.0.0.1:5999/db; created./jdbc:postgresql:/127.0.0.1:5999/db| exit 1,✗ Unsupported database URL scheme \"jdbc:\" (OS_DATABASE_URL / --database). Supported: …; no file |\n|redis:h|✓ Server is ready,→ redis:h; created./redis:h| exit 1, the same sentence naming\"redis:\"; no file |\n|sqlite://x.db| the file//x.db, the filesystem root (read from the built module, not booted: a boot writes/x.db) |✓ Server is ready,→ sqlite://x.db; created./x.db|\n|redis://h/x.wasm.db|✓ Server is ready,SqliteWasmDriver → redis://h/x.wasm.db; created./redis:/h/x.wasm.db| exit 1, the same sentence naming\"redis://\"; no file |\n|C:/data/app(read from the built module, no boot) | sqlite, the fileC:/data/app| unchanged |\n| CONTROLpostgres://u:p@127.0.0.1:5999/db| exit 1, connect ECONNREFUSED 127.0.0.1:5999 | unchanged |\n| CONTROLfile:./ctl.db,--dev| (not measured) |✓ Server is ready,→ file:./ctl.db|\n\nThe runtime host, read and booted forredis://h/x.wasm.dband not changed here:resolveStandaloneDatabaseanswerssqlite-wasmon the fileredis://h/x.wasm.db, and a plain config boots to✓ Server is readyin dev and production with a 1,499,136-byte database at./redis:/h/x.wasm.db."},
{
"old": "Only the rows that moved are listed; every other row (
postgres://,postgresql://,mysql://,mysql2://,mongodb://,mongodb+srv://,libsql://,https://*.turso.io,wasm-sqlite://,*.wasm.db,memory://,mingo://,file:,sqlite:,sqlite://,:memory:,*.db,*.sqlite,*.sqlite3) was already equal and is unchanged.","new": "Only the rows that moved are listed; every other row (
postgres://,postgresql://,mysql://,mysql2://,mongodb://,mongodb+srv://,libsql://,https://*.turso.io,wasm-sqlite://, a scheme-less orfile:*.wasm.db,memory://,mingo://,file:,sqlite:,:memory:,*.db,*.sqlite,*.sqlite3) was already equal and is unchanged."},
{
"old": "|
jdbc:postgresql://h/db,redis:h(a scheme with no//) |:memory:default /null| sqlite / sqlite | sqlite (see the finding below) |","new": "|
jdbc:postgresql://h/db,redis:h(a scheme with no//) |:memory:default /null| refused / refused | sqlite, a file at a path made of the URL (#23765) |\n|redis://h/x.wasm.db,jdbc:x/x.wasm.db(an unrecognised scheme with a.wasm.dbending) | sqlite-wasm / sqlite-wasm, a file at a path made of the URL | refused / refused | sqlite-wasm, a file at a path made of the URL (#23765) |\n|sqlite://x.db,sqlite://x.wasm.db| sqlite / sqlite on//x.db; sqlite-wasm on//x.wasm.db(the filesystem root) | sqlite / sqlite onx.db; sqlite-wasm onx.wasm.db| the same files as the CLI after |"},
{
"old": " - an explicit driver still wins over an unrecognised scheme.",
"new": " - an explicit driver still wins over an unrecognised scheme.\n - round 1:
jdbc:postgresql://localhost:5432/db,redis:cache,redis://h/app.wasm.dbandjdbc:x/app.wasm.dbare refused at inference, naming the scheme;C:\\data\\app,C:/data/appand./backup:2026.dbstay sqlite paths;sqlite://x.db,sqlite:x.db,sqlite:///var/lib/objectstack/x.dbandSQLITE://x.dbresolve to the filex.dbor/var/lib/objectstack/x.db, with the typed URL kept for the banner; afile:/sqlite:*.wasm.dbselects sqlite-wasm;sqlite://x.wasm.dbis the WASM engine onx.wasm.db."},
{
"old": " - 2 opaque-scheme values on which the two hosts must give the same verdict, so a change to either host's scheme-less arm reds until the other follows.",
"new": " - 4 values the CLI host refuses, typed, in dev and in prod: the opaque schemes
jdbc:postgresql://localhost:5432/dbandredis:cache, thenredis://h/app.wasm.dbandjdbc:x/app.wasm.db. The runtime host's answer for these is deliberately not pinned: today it opens them as a file, the gap #23765 names.\n - 4 alias values both hosts open as the same file, in dev and in prod:sqlite://x.dbandsqlite:x.dbasx.db,sqlite:///var/lib/objectstack/x.dbas itself, andsqlite://x.wasm.dbasx.wasm.dbon the WASM engine.\n - one more accepted row,file:./data/app.wasm.dbas sqlite-wasm."},
{
"old": "| A4 | the blank-URL guard removed | 1 red |",
"new": "| A4 | the blank-URL guard removed | 1 red |\n\nRound 1, at
40fe66778: the same tool in WRAP mode over the two pinned files (149 tests, baseline 149 passed). The subject issrc(both files import./storage-driver.js), so nodist/is involved. Each mutation is proven on disk (anchor x1 → x0 and a new blob). Each restore is proven as blob == HEAD893ea05be3edwith an emptygit diff HEAD, by the tool and again by an outer trap.\n\n| Leg | Mutation | Result |\n|---|---|---|\n| R1-A | the opaque-scheme fallback removed from the scheme test | 6 red / 143 green: thejdbc:postgresql://…,redis:cacheandjdbc:x/app.wasm.dbrows, in both files |\n| R1-A2 | the opaque test widened to a one-letter scheme | 2 red:C:\\data\\appandC:/data/app|\n| R1-B1 | the sqlite arm back to strippingsqlite:in place ofnormalizeDatabaseUrl| 5 red:sqlite://x.db,sqlite:///var/lib/objectstack/x.db,SQLITE://x.db(sqlite:x.dbstays green, since the old strip handled it) |\n| R1-B2 | the sqlite-wasm arm back to strippingsqlite:| 2 red:sqlite://x.wasm.db, in both files |\n| R1-C | the*.wasm.dbtest moved back above every scheme check | 4 red:redis://h/app.wasm.dbandjdbc:x/app.wasm.db, in both files |"},
{
"old": "## Tests and gates (head
683bf9587)","new": "## Tests and gates (head
40fe66778)"},
{
"old": "-
pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: 295 files, 4399 tests passed.","new": "-
pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2, in two shards (--shard=1/2,--shard=2/2): 148 + 147 = 295 files, 2464 + 1953 = 4417 tests passed."},
{
"old": "-
pnpm --filter @objectstack/cli typecheck: exit 0.tsc --noEmitcoverssrc/**tests, thencheck:test-typecheckran OK.","new": "-
pnpm --filter @objectstack/cli typecheck: exit 0.tsc --noEmit --listFileslists both changed test files (include: [\"src\"], no exclude), thencheck:test-typecheckran OK."},
{
"old": "-
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 64 commands, run in order, each exit captured before any pipe. 62 exited 0. 2 exited 3 (PREREQUISITE NOT MET) and are NOT MEASURED:check:dual-build-cjs-loadsandcheck:i18n-coverage. Both read the built output of the whole workspace, and this local run built only the@objectstack/cliclosure. CI runs both.--ranreconciliation: 64 derived, 62 run, 2 NOT-MEASURED, 0 UNRUN. The derivation read a tree 3 commits behindorigin/main. The one derived-from file that moved there isscripts/sdui-manifest.record.json(an objectui pin bump).","new": "-
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 64 commands, run in order, each exit captured before any pipe. All 64 exited 0,check:dual-build-cjs-loads(110 require entry points across 68 packages load) andcheck:i18n-coverage(14 configs, none new) included.--ranreconciliation: 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. The derivation read a tree 19 commits behindorigin/main, with 4 derived-from files moved there. The same derivation over the same 4 paths, in a throwaway tree atorigin/mainec44cf60b, printed the identical 64 commands."},
{
"old": "## Acceptance notes",
"new": "## Acceptance notes\n\n- Round 1: the
*.wasm.dbrule goes past the REWORK's two asks, in ask ①'s class. A.wasm.dbname tested above every scheme check rescuedredis://h/x.wasm.dbandjdbc:x/x.wasm.dbinto a SQLite file. The CLI host now refuses both, while the runtime host still acceptsredis://h/x.wasm.db(measured above). The seat records that host's answer on #23765.\n- Round 1: the write-failure lines of thefile://probe below carry a[driver-sqlite-wasm]prefix while the banner readsSqlDriver(better-sqlite3). Observed once, not investigated. Carrier: none."},
{
"old": "## Found while measuring, for the seat to file (not fixed here)",
"new": "## Found while measuring\n\nRound 0's three findings, as the seat dispositioned them: 1 is #23764; 2 is folded into this PR (the alias rule above); 3 is #23765, whose CLI half (the refusal) lands here.\n\nRound 1 found one more, for the seat (not changed here). The CLI host reads
file://NAMEas//NAME, a file at the filesystem root, where the runtime host opens./NAME. Measured withOS_DATABASE_URL=file://proc/os23633-probe.db os serve --devover a host config:✓ Server is ready, banner→ file://proc/os23633-probe.db, and every flush of the database failed with ENOENT on//proc/os23633-probe.db, so it answered from memory only. The runtime host, over a plain config, opened./proc/os23633-probe.db. The cause is thefile:strip in both sqlite arms ofresolveStorageDefinition, unchanged frommain; the runtime host stripsfile:and an optional//.\n\nRound 0's text, as written then:"}
],
"open_questions": [
{
"question": "The CLI host opens file://NAME as //NAME, at the filesystem root, where the runtime host opens ./NAME (out_of_scope_findings[0]). Fold it into this PR, or file it?",
"options": [
"A: fold, as a round 2. Strip file: and an optional // in both sqlite arms, and pin file://x.db and file:///abs/x.db in the parity alias block. The bounded in-place exemption's four conditions hold: the same defect class as ask ②, a mechanical fix whose form the runtime host's rule pins, a file this claim holds, and the same gate batch. But the strip would be a second copy of the runtime host's rule, since sqliteFilenameFromUrl is not exported and this card forbids a packages/runtime edit.",
"B: the seat files it, and this PR lands as it stands. The card's fix exports the runtime host's filename rule (sqliteFilenameFromUrl) and has both CLI sqlite arms call it, as ask ② did with normalizeDatabaseUrl."
],
"recommendation": "B. Business need: real but unpulled. The public door measured a silent non-persistent boot, yet no producer in the repo writes a relative file:// database URL (git grep over DATABASE_URL / databaseUrl spellings: 0 hits), so it need not hold up a landing the seat wants before sign-off. Long term: B keeps one rule, the REWORK's own line against a second copy, and A writes exactly that copy. AI error: one shared rule is the structural guard, and two copies are the drift this card exists to close. Startup focus: neither option adds surface, and B moves no scope into a finishing round. Decided by the Zone-1 ruling 'Nothing else moves', so not changed here."
}
],
"out_of_scope_findings": [
"class: a · reach: public door measured at 40fe667. OS_DATABASE_URL=file://proc/os23633-probe.db os serve --dev over a host config gave 'Server is ready', banner '→ file://proc/os23633-probe.db', then '[driver-sqlite-wasm] Writing //proc/os23633-probe.db to disk failed (ENOENT ...). Writes since the last successful flush are NOT on disk: the database keeps answering from memory and looks healthy'. With a writable name the database would land at the filesystem root; this measurement aimed at /proc so that nothing was written under /, and nothing was. The runtime host (a plain config, same URL) booted ready on ./proc/os23633-probe.db under the cwd · evidence: storage-driver.ts:524-526 and :542-544 strip only /^file:/, while the runtime host's sqliteFilenameFromUrl (standalone-stack.ts:552, :554) strips /^file:(\/\/)?/. The same line is on origin/main, 683bf95 and 40fe667, so it is published and round 1 left it unchanged. It is the file: twin of the folded sqlite:// finding · dedupe words: file:// double slash filesystem root CLI; resolveStorageDefinition file prefix strip; sqlite file URL two slashes absolute path; os serve file:// database not persisted",
"carrier: none · noted, not filed: the write-failure lines of that file:// probe carry a [driver-sqlite-wasm] prefix while the banner reads SqlDriver(better-sqlite3). Observed once, not investigated.",
"carrier: none · noted, not filed: round 0's two carrier-none notes (the serve.ts step-2 comment's scheme list; cli.mdx:522) stand as the seat dispositioned them."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
Landed: PR #23761 →
b485122a7f(#23633). The card is closedcompleted, andpm:dispatchedis strippeddomain:cliseat 1 (#6024) ·os-bill· sessionsession_01FLwgbTSmGMrqsXkBDsiwfG· 2026-10-16T03:26Z · holder of claim6204162580, released by this landing.origin/mainb485122a7f: one parent (a queue squash), an ancestor oforigin/main, 4 files +369 / −14, identical to the reviewed head40fe66778; no queue branch leftcompletedby the PR'sFixes #23633;pm:dispatchedremoved by targeted delete6205322639;6205385818;6206960203);6207056808.contract-review.mdat29c7157af.os serve's storage resolution (inferDriverTypeFromUrl,resolveStorageDefinition):<scheme>://and an opaque scheme, naming the scheme and never the URL;pg://and a scheme-less path as the runtime host does;sqlite://x.dbas./x.dbthrough the runtime host'snormalizeDatabaseUrl;*.wasm.dbfile only.OS_DATABASE_URL, so a password in the URL reaches the console and the boot log #23764: the runtime host's refusal prints the whole URL. Since graded and dispatched.OS_DATABASE_URLwith an opaque scheme (jdbc:postgresql://…,redis:h) is opened as a SQLite file path and creates directories, against the module's own "unknown URL schemes throw" #23765: the runtime host's opaque-scheme arm. Pointer6207033496adds its.wasm.dbarm.OS_DATABASE_URL=file://NAMEopens//NAMEat the filesystem root on the CLI host, where the runtime host opens./NAME(thefile:twin of thesqlite://alias #23633 fixed) #23796:file://NAMEopens//NAMEon the CLI host.packages/cli/src/utils/storage-driver.ts.Generated by Claude Code