Skip to content

fix(cli): os migrate finds the SQL driver on a wasm-sqlite:// database, so the ownership ceremony and os migrate plan run on it - #23671

Merged
objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-23648-sqlite-wasm-sql-driver
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-23648-sqlite-wasm-sql-driver

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23648
Clause-②: no

On PR #23647. This card builds on PR #23647 (#23558), whose dialectOfDriver resolves the wasm driver to sqlite once it is found. As ruled, this branch first merged that PR's head 107a7a923c as one merge commit, without rewriting it. PR #23647 has since landed on main (c2bb302c8, from that same head). This branch then merged main twice (286eb9ef3, then 1ced0f82b), and its head is 0e446f4bf. So the file diff against main is this card's alone: 5 files. The commit list still shows PR #23647's four commits, because main holds them as one squash commit.

What changed

  • packages/cli/src/utils/schema-migrate.ts: SQL_DRIVER_SERVICES lists driver.com.objectstack.driver.sqlite-wasm, the key a wasm-sqlite:// boot was measured to register. It is one more name in a list, not a driver.* pattern. A driver without the schema-migration seam (detectManagedDrift and applyMigrationEntries) is still not found under any key. A SQL driver under a key the list does not name is still not found either.
  • packages/cli/src/utils/unmanaged-tables.ts: os migrate plan's unmanaged-table sweep resolves the dialect through dialectOfDriver (dialectName first), not from config.client as a string only. This is folded in because the change above makes it reachable. With only the list fix, every wasm-sqlite:// plan reported the section unreadable ("the connected dialect (unnamed client) is not one this sweep enumerates tables on"), since this driver's client is a knex client class. PR fix(cli): the organization-ownership ceremony reads its dialect once, through metadata-core's shared resolver, and refuses an unresolved one before any statement #23647's Acceptance notes recorded this reader as having "no reach today". This PR is what gives it reach.
  • Two new test files and one changeset (@objectstack/cli patch). No new export.

Premise check

Read on origin/main 7cb33cfef with PR #23647 merged in (60ae7a6a8).

  • H1, the service key: measured. I booted bootSchemaStack on wasm-sqlite://FILE and listed every driver.* service. There is exactly one: driver.com.objectstack.driver.sqlite-wasm, an _SqliteWasmDriver with dialectName: 'sqlite' and a class-valued config.client. stack.driver was null and dbLabel was unknown. DefaultDatasourcePlugin registers the default driver as driver. followed by the driver's name.
  • H2, the readers.
    • SQL_DRIVER_SERVICES is read by findSqlDriverVia, which findSqlDriver and findSqlDriverForKernel call.
    • findSqlDriver is called by bootSchemaStack, the boot behind 19 os migrate / os storage / os secret commands.
    • findSqlDriverForKernel is called by os serve's boot-time migration gate when an artifact is pinned with OS_ARTIFACT_URL.
    • DeferSchemaDdlPlugin.init iterates the list too. It also arms every driver.* service, so the wasm driver was already held to a dry run.
    • findTelemetryDriver uses its own list (driver.telemetry) and is unchanged.
    • os migrate plan meets the same lookup: it was measured (below) and is folded in.
  • H3, the seam: measured. On the booted driver, detectManagedDrift, applyMigrationEntries, setDeferredDdl and execute are functions, and raw is undefined. execute is the seam resolvePlannedDriverExec takes. SqliteWasmDriver extends SqlDriver.
  • H4, the dialect: measured. With PR fix(cli): the organization-ownership ceremony reads its dialect once, through metadata-core's shared resolver, and refuses an unresolved one before any statement #23647 merged in, the ceremony's plan and apply both run on a wasm-sqlite:// database. The apply step is handed dialect: 'sqlite' (unit pin). The plan's first statement is the SQLite catalog read.

Before and after, at the doors

Fixture: a project with a compiled artifact that declares no object, plus OS_AUTH_SECRET. The database was created on wasm-sqlite:// by os migrate apply, and each door ran on its own copy. "Before" is the same copy run with schema-migrate.ts and unmanaged-tables.ts restored from 60ae7a6a8. The restore was trap-guarded, and afterwards each blob equalled HEAD and git diff HEAD was empty. "After" was measured at 030a6d826 (the list fix) for the ceremony's --apply and for os migrate apply on a fresh file, and at 525ecbe67 (with the sweep fix) for the rest. Every command was run through bin/run-dev.js with --json.

door before after
os migrate organization-ownership (plan) exit 1, plan_refused / driver-unsupported exit 0, plan written (client: null)
… --apply --backup-taken --plan-file exit 1, apply_refused / driver-unsupported exit 0, all nine steps done, the adr-0131-organization-ownership ledger row persisted in the file
os migrate plan exit 0, {"error":"no_sql_driver","changes":[]} exit 0, a plan; unmanagedTables.status: read, 71 physical tables (the list fix alone: unreadable, "unnamed client")
os migrate apply (fresh file) exit 0, {"error":"no_sql_driver"}, nothing created exit 0, 71 tables created and persisted in the file
os migrate multi-value-columns exit 0, no_sql_driver exit 0, targets: [] (no conversion applies on SQLite)
os migrate unmapped-columns --object sys_user exit 0, no_sql_driver exit 0, columns: []
duplicates, account-issuer, meta --stored, audit-metadata-bodies, comment-reactions, security-catalog-overlays, os storage orphans exit 0, database: unknown exit 0, the same counts, database names the file
recorded-by, resume, os secret rewrap, os secret orphans exit 0 exit 0, the same answer (no driver read)
value-shapes, files-to-references, summary-nulls exit 1, "No app objects are loaded" the same: the fixture declares no object, so files-to-references' driver read (planMediaColumnMove) was not reached. NOT MEASURED.
os serve with OS_ARTIFACT_URL=file://… declaring storage.notNull on a nullable column booted ("Server is ready"); the destructive drift was only warned refused to boot: "Refusing to boot — 1 destructive schema change(s) required", naming the column

The os serve row is the one behaviour change outside os migrate. Its gate's own comment says "no SQL driver" is that gate's everything-is-fine answer. On a wasm-sqlite:// database the gate now runs as it does on every other SQL database. It was measured on its own fixture, seeded through file: and served through wasm-sqlite://, at a4b58bf18. The before side had the new key removed through scripts/ablation-replace.mjs, and afterwards the blob equalled HEAD.

Tests

  • packages/cli/src/utils/schema-migrate.sql-driver-lookup.test.ts (new, unit tier, 12 tests):
    • The lookup finds the wasm driver under its key.
    • It still finds the three previously listed keys.
    • Control: a SQL-shaped driver under an unlisted key (driver.com.example.driver.sql-compatible, driver.telemetry, driver.default) stays unfound.
    • Control: a driver without the schema-migration seam stays unfound under the memory, MongoDB and wasm keys.
    • At the command, with bootSchemaStack replaced by a stack whose driver is the real lookup's answer: both ceremony arms refuse driver-unsupported on three refused shapes, with exit 1, no statement run, no plan file and the apply step never called. Both arms proceed on the wasm driver, and the apply step is handed dialect: 'sqlite'.
  • packages/cli/src/commands/migrate/sqlite-wasm.integration.test.ts (new, integration tier, 3 tests): the real boots on one wasm-sqlite:// file, through each command's run().
    • os migrate apply creates the schema in the file (sys_migration among it, read back past the driver).
    • os migrate plan plans it, with the unmanaged-table sweep read and its count equal to the file's tables.
    • The ceremony plans it, then applies the reviewed plan: every step done, and the ledger row in the file.

Runs, all under the shared verify lock:

  • Final head 0e446f4bf.
    • unit project, by path: the new lookup file, unmanaged-tables.test.ts, and the two published-subpath pins. 4 files, 82 tests passed.
    • integration project, by path: the new file, unmanaged-tables, organization-ownership-plan, schema-migrate, and the four plan.* integration suites. 8 files, 33 tests passed. This layer was run locally because the diff adds an integration-tier file and changes the sweep those suites read.
  • 1eb7378cd (the merge of 286eb9ef3). The cli code is the same as the final head; the later merge brought only .claude/** and scripts/pm/**.
    • The whole unit layer: 290 of 292 files and 4325 tests passed. The 2 failed files (test/published-subpath-console.pin.test.ts, test/published-subpath-hook-body.pin.test.ts) refused because packages/cli was not built ("./dist/index.js is absent"). That is a missing prerequisite, not a measurement. Both passed on the final head once the cli was built (above).
    • pnpm --filter @objectstack/cli typecheck (tsc --noEmit and check:test-typecheck): exit 0.

Ablation

These are one-off proofs, run through scripts/ablation-replace.mjs in WRAP mode with an EXIT INT TERM restore trap, at a4b58bf18. No dist/ is involved: the subjects are cli src files, imported relatively, and metadata-core is aliased to src in the cli vitest config.

  • The new key dropped from SQL_DRIVER_SERVICES. On disk, anchor 1 went to 0 and blob b1fcdf46db73 became a46e1ec56ca9.
    • Unit: 3 red, 9 green. The three wasm positives went red; every control and the no-regression case stayed green.
    • Integration: 3 red, 0 green (no_sql_driver, and the ceremony's exit 1).
    • Restored: blob equals HEAD and git diff HEAD is empty.
  • The sweep returned to reading config.client as a string only. On disk, anchor 1 went to 0 and the marker went to 1. Blob c647aec174bc became a2097e2e9892.
    • Integration: 1 red, 2 green. The os migrate plan case went red (unreadable).
    • Restored: blob equals HEAD and git diff HEAD is empty.

Gates

Local, on 0e446f4bf:

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 64 commands from merge base 1ced0f82b, over the 5 changed paths. All 64 were run in sequence, each exit code captured before any pipe: 64 of 64 exit 0.
  • --ran: "64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN", exit 0.
  • check:dual-build-cjs-loads, check:i18n, check:i18n-coverage and check:i18n-walk-parity exited 3 (PREREQUISITE NOT MET) on the first pass, at 1eb7378cd. So did check:type-check-debt, whose closure build queued past its lock budget. All five exited 0 at 0e446f4bf, after a turbo build of the closures they name.
  • The ratchet families in this set, check:type-check-coverage and check:type-check-debt: exit 0.
  • Not named by the derivation: eslint --no-inline-config packages/cli exited 0 with no output, and a JSON-format run over the four changed .ts files reported 0 errors and 0 warnings in each. The repo-wide pnpm lint is CI's.
  • Control-byte self-scan of the 5 changed files: no hit.

Acceptance notes

  • os serve keeps a second list of driver service names (describeRegisteredDriver in serve.ts), and it also omits sqlite-wasm. Serve reads it only to label the banner when it resolved no driver label itself. It was not measured and is not changed here. Carrier: none.
  • os migrate duplicates passes String(config.client) as its client. For this driver that string is the client class's source text. It is read only to pick MySQL backtick quoting, so the SQLite quoting stays right. It is not echoed into the report: measured, the report on the wasm database matched the one before, apart from the database name, and no output carried the class text. Carrier: none.
  • A stale header comment. organization-ownership.dialect.test.ts (PR fix(cli): the organization-ownership ceremony reads its dialect once, through metadata-core's shared resolver, and refuses an unresolved one before any statement #23647's file, now on main) says the command's boot "does not hand that driver over today". After this PR it does. The file is not edited here.

Generated by Claude Code

claude added 11 commits October 15, 2026 08:09
…ce, through metadata-core's dialectOfDriver, and refuses an unresolved one before any statement

Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU
Co-authored-by: Claude <noreply@anthropic.com>
…emony change as measured

The sweep is os migrate plan's, not the ceremony's, and the schema boot never plans
SqliteWasmDriver, so the change had no reach there.

Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU
Co-authored-by: Claude <noreply@anthropic.com>
…107a7a9) into claude/issue-23648-sqlite-wasm-sql-driver

This branch builds on PR #23647's dialectOfDriver, which resolves the
wasm driver to sqlite once the schema boot finds it. The branch is merged
as it stands, not rewritten.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
…mDriver

SQL_DRIVER_SERVICES gains driver.com.objectstack.driver.sqlite-wasm, the
service key a wasm-sqlite:// boot registers (measured). Every os migrate
command found no SQL driver on such a database, so the organization-ownership
ceremony refused it as driver-unsupported and os migrate plan answered
no_sql_driver with exit 0. A non-SQL driver is still not found.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
… database's dialect

Once the schema boot finds SqliteWasmDriver, os migrate plan reaches the
unmanaged-table sweep, which read config.client as a string only. That
driver's client is a knex class, so every wasm-sqlite:// plan reported the
section unreadable ('unnamed client'). The sweep now resolves the dialect
through metadata-core's dialectOfDriver, dialectName first.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
… the lookup's controls

The integration pin runs os migrate apply, os migrate plan and the
organization-ownership ceremony's plan and apply through their real boots on
a wasm-sqlite:// database. The unit pin holds the lookup to the measured
service key, and its controls: a SQL driver under an unlisted key and a
non-SQL driver under any key stay unfound, and the ceremony refuses both
arms driver-unsupported on them.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
…river

PR #23647 has landed on main (c2bb302, from the same head 107a7a9
this branch merged), so after this merge the branch's diff against main
is this card's alone.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 57994c311f4acc576e1a1eb9ad4162d191d15905 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5a045a4c9a8a2e09eac884330c55ab004171bc9d — the merge of head 0e446f4bf4cf5de29f557d1bbc330a938b1ef65c into base 57994c311f4acc576e1a1eb9ad4162d191d15905, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5a045a4c9a8a2e09eac884330c55ab004171bc9d && git checkout 5a045a4c9a8a2e09eac884330c55ab004171bc9d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 57994c311f4acc576e1a1eb9ad4162d191d15905 0e446f4bf4cf5de29f557d1bbc330a938b1ef65c && git checkout -B drift-repro 57994c311f4acc576e1a1eb9ad4162d191d15905 && git merge --no-ff 0e446f4bf4cf5de29f557d1bbc330a938b1ef65c

node scripts/docs-audit/affected-docs.mjs --json 57994c311f4acc576e1a1eb9ad4162d191d15905

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

✅ ACCEPT: PR #23671 at 0e446f4bf (#23648, Fixes #23648). It queues on a true-green head; no contract review is owed

domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG · 2026-10-15T12:19Z · holder of the claim on #23648. Report: os-dev-report 6191751771.

席位核对

  1. Shape, read on GitHub:
  2. The fix:
    • SQL_DRIVER_SERVICES gains driver.com.objectstack.driver.sqlite-wasm, a named key, not a driver.* pattern. findSqlDriverVia's two-method check still keeps a non-SQL driver unfound. Both refusal controls are pinned: a driver without the seam under any key, and a SQL driver under an unlisted key.
    • Ablation 1 (the key deleted) turns the three wasm positives and the three integration cases red, and the controls stay green.
  3. unmanaged-tables.ts: accepted, and the claim's surface is supplemented.
  4. Clause-②: no holds, judged by the seat:
    • No authorable key, export or accepted input moves.
    • The ceremony and every os migrate command now run on a database their own text already says is supported ("The ceremony supports SQLite, PostgreSQL and MySQL.", organization-ownership.ts:345/:516, organization-ownership-plan.ts:576).
    • The os serve artifact boot gate, a behaviour change. On wasm-sqlite:// it now refuses a boot that used to go ahead. The seat reads this as the gate's documented policy (Artifact-pinned boot: OS_ARTIFACT_URL (+ OS_ARTIFACT_SHA256) — boot a stack from a published artifact by reference #8368 acceptance [WIP] Fix error in step four of the action run #5) reaching a SQL database it missed. Its own no-driver branch is scoped to "memory / mongo" (artifact-boot-migration.ts:119-:121). It is not a narrowed contract.
    • The changeset discloses it.
    • No contract review. None of the contract-review faces is touched: no Clause-②: yes, no packages/spec/src/**, no governed text. Under contract-review.md, that means CI plus this seat's reading, with no second agent.
  5. The changeset, checked sentence by sentence against the diff and the report's door readings (@objectstack/cli patch, right for a fix):
    • FROM: the ceremony's plan and --apply exit 1 with driver-unsupported; plan answers {"error":"no_sql_driver","changes":[]}; apply answers {"error":"no_sql_driver"} and creates nothing; the other commands answer no_sql_driver or name the database unknown.
    • TO: the two refusal controls stay as before.
    • The os serve paragraph: "used to find no driver and let the boot go ahead with the artifact's schema changes unapplied, a destructive one included" matches the no-driver early return and the measured before-boot.
  6. Tests and gates:
    • Local on the final head: unit 4 files, 82 passed; integration 8 files, 33 passed. On 1eb7378cd (the same cli code), the whole cli unit layer passed 4325, apart from 2 files that needed a built cli and passed once it was built. Typecheck exit 0.
    • Derived gates: 64 derived, 64 run, 0 not measured.
    • CI on this head, read in this act: 28 success, 6 skipped, and 5 in_progress (Test Core 1, 2, 3, 4 and 6 of 6). No failure.
  7. Out of scope (carrier none, recorded in the PR's Acceptance notes):

Landing: once every check on 0e446f4bf is green, the seat marks the PR ready and arms auto-merge. Fixes #23648 closes the card when it merges.


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants