Skip to content

fix(cli): os migrate plan/apply examine the datasource a datasourceMapping default routes objects to, as os dev does - #24199

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-24120-migrate-plan-declared-default
Oct 17, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-24120-migrate-plan-declared-default

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24120

Clause-②: no

What was wrong

Take a project whose primary store is a declared datasource that datasourceMapping routes as the default: datasources: [{ name: 'qa_main', driver: 'sqlite', … }] with datasourceMapping: [{ default: true, datasource: 'qa_main' }]. On it, os migrate plan resolved qa_main's database as its primary ("the project config's default datasource qa_main"), but it registered no driver named qa_main. Every object the rule routes there, platform objects included, was bound to no driver. The plan printed Examined 0 managed table(s) and a PARTIAL plan, and logged [ObjectQL] Datasource 'qa_main' mapped for object 'sys_metadata' is not registered. os dev on the same config reports drift in that database and names os migrate as the remedy.

Mechanism, measured (dispatch hypotheses H1–H3)

  • H1 holds. Reproduced at cb8756b00 on a minimal scratch stack (qa_main sqlite plus the default mapping, one app object, provisioned by a boot, then staged). The plan reported Examined 0 managed table(s) and coverage 0 of 71 declared object(s) … (71 bound to no driver). os dev on the same database printed [schema-drift] os24120_thing.extra: column exists in the database but not in metadata (orphaned).
  • H2, located one level above schema-migrate.ts's driver lookup. AppPlugin connects declared datasources only through the datasource-connection service (honourDatasourceMappingBeforeSchemaSync and start(), then connectDeclared, the ADR-0062 D1 path). os dev / os serve publish that service from DatasourceAdminServicePlugin (packages/cli/src/commands/serve.ts, the Runtime Datasource Admin block). The migrate boot (bootSchemaStack, packages/cli/src/utils/schema-migrate.ts) published nothing, so AppPlugin left qa_main metadata-only. Even with a driver, the planned set was the primary and the telemetry sibling only.
  • H3 is false, as the dispatch allowed. The definition is already in hand: AppPlugin connects the declared datasource from the artifact or config it holds. packages/runtime (resolve-project-database.ts) is untouched.

The fix (packages/cli only)

  • bootSchemaStack gets composeDeclaredDatasources. It is set by os migrate plan, by apply, and by unmapped-columns (see below). It composes DeclaredDatasourceConnectionPlugin, which publishes datasource-connection as a DatasourceConnectionService over the shared driver factory. That is the same path os dev connects qa_main by, under its declared name. It is not composed when the composition already brings a publisher.
    • Its connect policy admits managed datasources only. An external / validate-only datasource holds no table a migration plans, and its objects are federated.
    • A read-only probe's factory opens an absent SQLite file as an empty in-memory database, as the primary's does. Measured: a fresh project's plan leaves no data/ directory behind.
  • The planned drivers are the primary and the sibling, plus each SQL driver that meets two conditions: it is registered under a declared datasource name the service reports available, and a registered non-federated object resolves to it (the engine's routing decides). SchemaStack.databaseOf names each planned driver's database. A composition note names each declared datasource and its database, in both faces.
  • distinctDatabaseDrivers: the unmanaged-table sweep reads each database once. The primary resolved from a declared default and the declared connection are two connections to one file, and without this physicalTables doubled (measured 144 against 72).
  • os migrate unmapped-columns (outside the claim's file surface). It filters the plan's own findings. Once the plan composed qa_main, its refusal on this config, sys_user is not in the set "os migrate plan" diffs: it is bound to no driver, became false. It now composes the same datasources and names the database through databaseOf. Edited under the standing rule that a published defect this change makes false is fixed in the same change. The claim's file surface needs that path added.

Scope. The mechanism connects every managed declared datasource os serve connects: one a mapping routes objects to, one an object binds, or one with autoConnect. It is not limited to the default. Limiting it would have needed a filter that keeps the same defect on a non-default mapping rule. Behaviour change: a routed managed datasource that cannot connect now fails plan / apply / unmapped-columns with the connect error os serve gives. Measured with an unreachable mapped postgres: exit 1 and datasource 'qa_pg': connect failed — connect ECONNREFUSED … fail-fast per ADR-0062 D5. Under OS_ALLOW_DRIVER_CONNECT_FAILURE=1 the plan exits 0 and is PARTIAL (1 bound to no driver).

Evidence

The pin is packages/cli/src/commands/migrate/plan.declared-default-datasource.integration.test.ts (integration tier, // test-budget: 80). It provisions with os dev --compile and OS_MIGRATE_AND_EXIT=1, then stages two drifts in qa_main's file: a column the app object stopped declaring, and a column on sys_user. Then it asserts four things:

  • the plan's drift set equals the [schema-drift] set os dev prints, and holds both staged drifts;
  • unexaminedObjects: 0, and databaseSource is config-datasource qa_main;
  • physicalTables equals the file's table count;
  • os migrate unmapped-columns --object sys_user reads fx24120_legacy.
run tree result
pin, after 0b5ed65b4 1 passed, 56.1 s
pin, before (schema-migrate.ts, plan.ts, apply.ts restored to cb8756b00) be919df4a red: AssertionError: expected [] to deeply equal [ 'fx24120_thing.extra', …(1) ]
unmapped leg, before (only unmapped-columns.ts restored to cb8756b00) 0b5ed65b4 red: os migrate unmapped-columns --object sys_user failed (exit 1, Datasource 'qa_main' … is not registered)
policy ablation (schemaMode check replaced by true, scripts/ablation-replace.mjs) 3df5f9f1f red: the external and validate-only cases of schema-migrate.declared-datasources.test.ts

Each before or ablation leg restored with git checkout HEAD --, with git diff HEAD empty and blob hashes equal to HEAD.

Manual readings on the scratch stack, after the fix: the plan examined 71 tables and listed os24120_thing.extra [drop_column]. apply --allow-destructive --yes applied it, and the next plan showed 0 changes and 0 unexamined. --database-url file: pointing at the same file gave Examined 71, the same drift, and physicalTables: 72.

Local checks at 0b5ed65b4:

  • pnpm --filter @objectstack/cli typecheck && vitest run --project unit: exit 0, 310 files / 4599 tests.
  • vitest run --project integration over the pin and unmapped-columns.integration.test.ts: 2 files / 11 tests passed.
  • pnpm exec eslint --no-inline-config over the 6 changed .ts files: exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands: 65 families derived; all 65 run, each exit 0. --ran: 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN.

CI owns the rest: the farm, Test Core, the type-check lanes, dogfood.

Acceptance notes

  • Residue, stated: runtime-origin datasources (created in Setup) are restored and connected by DatasourceAdminServicePlugin.start() on os serve, which this boot does not run. Objects bound to them stay "bound to no driver", and the plan says so (PARTIAL).
  • os migrate organization-ownership (held off on this seat) and security-catalog-overlays are not opted in. On a datasourceMapping-default deployment the former's drops "through that set's drift" still examine none of the routed tables. Not measured here.
  • docs/qa/platform-checklist/areas/cli.json, item cli.dev-automigrate-policy: step 2's note that os migrate plan "composes no driver for it and prints a PARTIAL plan" is stale once this lands, and the plan can serve as that step's oracle again.
  • Neighbour cli: os migrate plan lists sys_packages under "declared by no object in this plan" right after os migrate apply created it — the unmanaged-table sweep's known set omits the one table migrate itself plans outside the object set #24059 (sys_packages listed as unmanaged) is not touched: the sweep listed sys_packages alone, once, before and after.
  • A missing declared index (staged by dropping idx_sys_user_created_at) is a create_index drift in the plan, while os dev's boot creates it silently. The two agree on the remedy. This predates this change; the pin stages no such drift.
  • origin/main moved 5 commits since cb8756b00 (to 2bcaefba9). None touches a path here, and the branch was not merged. CI's merge ref and the queue's rebuilt generation carry the joint check.

Generated by Claude Code

…rces os serve connects

A datasourceMapping rule routing objects to a declared datasource (the
declared default included) left them bound to no driver on the migrate boot:
no datasource-connection service was published, so AppPlugin kept every
declared datasource metadata-only. Publish one (managed datasources only)
and plan each declared datasource the deployment's objects resolve to.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb
…eMapping-default datasource

Also read each planned database once in the unmanaged-table sweep: the
primary resolved from a declared default and the declared connection are one
file, and were counted twice.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb
…er-per-database sweep

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

It filters the plan's own findings; on a datasourceMapping-default project
its refusal ("not in the set os migrate plan diffs: bound to no driver")
became false once the plan composed the declared datasource. SchemaStack
names each planned driver's database (databaseOf).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb
…e that cannot connect

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 15 documentable anchor(s).

12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/data-modeling/drivers.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts))
  • content/docs/data-modeling/external-datasources.mdx (via canConnect (symbol, a field of const object MANAGED_DATASOURCES_ONLY))
  • content/docs/data-modeling/indexing.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/data-modeling/queries.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate unmapped-columns (command, read off packages/cli/src/commands/migrate/unmapped-columns.ts))
  • content/docs/deployment/cli.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts), os migrate unmapped-columns (command, read off packages/cli/src/commands/migrate/unmapped-columns.ts))
  • content/docs/deployment/index.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/deployment/self-hosting.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts))
  • content/docs/kernel/services-checklist.mdx (via providesServices (symbol, a field of class DeclaredDatasourceConnectionPlugin), os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/plugins/anatomy.mdx (via providesServices (symbol, a field of class DeclaredDatasourceConnectionPlugin))
  • content/docs/protocol/kernel/lifecycle.mdx (via providesServices (symbol, a field of class DeclaredDatasourceConnectionPlugin), os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/upgrading.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v17/17-3.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v17/17-4.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v17/17-5.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v17/17-6.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v17/17-7.mdx (via os migrate apply (command, read off packages/cli/src/commands/migrate/apply.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts), os migrate unmapped-columns (command, read off packages/cli/src/commands/migrate/unmapped-columns.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 29 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 7d6ec91d2f89e834fc5b82183a95cb3c5427a882 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d86bedc91b799a0cf9e332de3eae6005e9d76a6a — the merge of head 0b5ed65b4e090d9f073ca805d4e83f10a2fe3c7e into base 7d6ec91d2f89e834fc5b82183a95cb3c5427a882, 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 d86bedc91b799a0cf9e332de3eae6005e9d76a6a && git checkout d86bedc91b799a0cf9e332de3eae6005e9d76a6a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7d6ec91d2f89e834fc5b82183a95cb3c5427a882 0b5ed65b4e090d9f073ca805d4e83f10a2fe3c7e && git checkout -B drift-repro 7d6ec91d2f89e834fc5b82183a95cb3c5427a882 && git merge --no-ff 0b5ed65b4e090d9f073ca805d4e83f10a2fe3c7e

node scripts/docs-audit/affected-docs.mjs --json 7d6ec91d2f89e834fc5b82183a95cb3c5427a882

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 7d6ec91d2f89e834fc5b82183a95cb3c5427a882 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT · PR #24199 at 0b5ed65b4 (#24120)

domain:cli seat 2 · marchtian · session session_01QyoWKK9RwUNxBxmevy9Neb · 2026-10-17T17:11Z · holder of claim 6235684683, amended as 6237265660. The dev's report is comment 6237239957 on #24120, and the seat reviewed it against this PR, not the report's own account.

  1. Shape. Draft → main. The first line is Fixes #24120, the only closing keyword in the body (body read in full). Clause-②: no is line-initial, and the assignee is marchtian. 7 files, all in packages/cli, inside the amended surface. check-governed-merges.mjs --pr 24199: NOT governed, 583 changed lines.

  2. Premise, measured.

    • H1 reproduced: 0 of 71 objects examined, while os dev reported drift.
    • H2 held one level up. AppPlugin connects declared datasources only through the datasource-connection service, which os serve publishes and the migrate boot never did.
    • H3 is false: no packages/runtime change.

    Seat checks of the diff:

    • DeclaredDatasourceConnectionPlugin is a thin publisher of os serve's own DatasourceConnectionService and createDefaultDatasourceDriverFactory from @objectstack/service-datasource. It is not a second copy of the connect logic. It differs in two declared ways: a managed-only policy, and a read-only probe that opens an absent SQLite file as an empty in-memory database.
    • It is skipped when the composition already publishes the service.
    • @objectstack/service-datasource is already a declared dependency of @objectstack/cli on main.
    • schema-migrate.ts is re-exported from none of @objectstack/cli's entries (., ./console, ./hook-body), so the new exports change no published surface.

    Proof:

    • The integration pin asserts that the plan's drift set equals os dev's [schema-drift] set, with 0 unexamined objects.
    • BEFORE (the three source files at cb8756b00): red, expected [] to deeply equal [ fx24120_thing.extra, … ]. The unmapped-columns leg is red the same way.
    • The policy ablation reddens the unit pin's external and validate-only cases.
    • AFTER: integration 1 / 1; unit 310 files / 4599 tests.
  3. Check-runs on head 0b5ed65b4 at this act: 27 success, 6 skipped, 6 in_progress (Test Core 1/6–6/6). The Lint and Type Check families are green. Local: 65 derived / 65 run (--ran exit 0); ESLint over the 6 changed .ts files exit 0.

  4. Accepted deviations:

    • unmapped-columns.ts is opted in, a declared breach amended into the surface. Its refusal on this config would otherwise have become false.
    • A second, unit-tier pin.
    • The mechanism covers every managed declared datasource os serve connects (mapping-routed, bound, autoConnect), not only the default. A default-only filter would have kept the same defect for every other mapping rule.
    • A behaviour the PR adds, read and accepted: a routed managed datasource that cannot connect now fails plan / apply / unmapped-columns with os serve's connect error. It is no longer skipped as PARTIAL, and OS_ALLOW_DRIVER_CONNECT_FAILURE degrades it as on os serve (ADR-0062 D5 fail-fast). This is a CLI behaviour, not a published contract face, and the changeset states it under "Refused now".
    • physicalTables is de-duplicated per database (distinctDatabaseDrivers). Without that, this change would have doubled a published --json value.

    Changeset, read sentence by sentence against the diff:

    • the problem paragraph matches H1;
    • "Now", "Not connected", "Refused now" and "os migrate unmapped-columns follows" each match a hunk;
    • "Unchanged: a project that declares no datasource gets the plan it got before" holds: with no publisher-relevant datasource, findDeclaredDatasourceDrivers returns [] and plannedDrivers is the old pair;
    • the physicalTables sentence matches distinctDatabaseDrivers.
  5. Out of scope. All five findings carry carrier: none and stay in Acceptance notes. None meets the filing gate with a measured reach:: the ownership ceremony is held off and NOT MEASURED, stale QA text, the runtime-origin datasource residue, the cli: os migrate plan lists sys_packages under "declared by no object in this plan" right after os migrate apply created it — the unmanaged-table sweep's known set omits the one table migrate itself plans outside the object set #24059 neighbour (unaffected) and a pre-existing index note.

  6. Landing. Ordinary queue landing. Once every check on this head is green or a roster skip, the seat flips ready and arms auto-merge through the relay. Fixes #24120 closes the card on merge, and the seat strips pm:dispatched in the same act.


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