Skip to content

fix(cli): os migrate plan/apply compose the requires-supplied provider a connector hard-depends on - #21739

Queued
objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/issue-21732-migrate-plan-requires
Queued

objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/issue-21732-migrate-plan-requires

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21732

Clause-②: no

What was wrong

os migrate plan and os migrate apply exited 1 on a fresh create-objectstack -t blank app and on examples/app-showcase:

✗ [Kernel] Dependency 'com.objectstack.service-automation' not found for plugin 'com.objectstack.connector.rest'

The four connectors (connector-rest, -openapi, -mcp, -slack) declare dependencies = ['com.objectstack.service-automation']. The blank template and the showcase ask for automation only through requires: ['automation', …]. os serve turns that token into the provider through Serve.CAPABILITY_PROVIDERS. buildSchemaMigrationPlugins composed config.plugins and never read requires, so the kernel could not order the boot.

What changed

packages/cli/src/utils/schema-migration-plugins.ts gains resolveRequiredProviders, called once the host config has loaded:

  • It uses the lookup serve uses. That means Serve.CAPABILITY_PROVIDERS, exact identity matching through Serve.providesCapability, and the rule that an explicit instance in plugins wins. It keeps no second copy of the token table.
  • It is generalised by dependency, not special-cased by token. A provider is composed only when a plugin already in the composition hard-depends on it and a token supplies it. The tokens searched are the config's declared requires plus the always-on slate serve appends (Serve.ALWAYS_ON_CAPABILITIES). The search runs to a fixed point, so the provider's own hard dependencies resolve the same way. A dependency that no token supplies is left to the kernel, which refuses it the same way os serve does for that config.
  • Each provider is booted in a declared posture. DECLARATION_PROVIDER_POSTURES has one row today: automation, constructed { armRuntime: false, packageRoot }. Inert mode brings the engine and the node registry up. It then registers no flow, binds no trigger or job, materializes no connector and resumes no suspended run.
  • The suspended-run store is left at its default, on purpose. This differs from the data-migration arm's suspendedRunStore: 'memory'. In inert mode the store is never attached, because start() returns before that step. The default is what makes init() declare sys_automation_run and sys_flow_dispatch beside sys_flow_credential. Those are the tables os serve creates for this capability, so the plan now covers them.
  • A token the lookup resolves but no posture covers is refused by name. The refusal names the plugin, its dependency and the token. The alternative was to boot that provider's start() inside a dry run with a posture nobody measured.
  • Configs with no unmet hard dependency are untouched. The resolver returns before it even loads serve's module.

Which other requires tokens get this treatment: only automation, because it is the one provider any shipped plugin hard-depends on (checked with git grep over packages/**/src for plugin dependencies declarations). Composing every declared provider would boot the served tier inside a dry run (triggers, email, approvals and so on, each with its own start()). That is out of scope here; see Acceptance notes.

Proof: the two apps, run under /tmp

The CLI was built at 826d5ce44. The showcase was copied to /tmp/i21732/showcase and a blank app was scaffolded at /tmp/i21732/qa (create-objectstack qa -t blank --skip-install). Each copy's node_modules is a symlink to the workspace's examples/app-showcase/node_modules, so each resolves this tree's packages. Every run used a fresh file:/tmp/i21732/*.db.

app before (base 316be321e) plan plan --json apply --json --yes re-plan --json
showcase exit 1, Dependency … not found for plugin 'com.objectstack.connector.openapi' 0 (34 tables to create) 0 (34 pending) 0 (34 created) 0 (0 pending)
blank scaffold exit 1, … for plugin 'com.objectstack.connector.rest' 0 (13 tables to create) 0 (13 pending) 0 (13 created) 0 (0 pending)

Nothing is armed. The plan prints [Automation] inert mode (armRuntime: false) — … no flow registered, no trigger or schedule armed, no connector materialized, no suspended run resumed. After apply, every table in both databases has zero rows (35 tables in the showcase database, 13 in the blank one). The composition note reads: Composed AutomationServicePlugin for `requires: ['automation']` — 'com.objectstack.connector.openapi' depends on it — in its declaration posture (engine up, nothing armed).

Tests

  • src/utils/schema-migration-plugins.test.ts (unit tier) has five new cases:
    • the requires-supplied provider is composed, inert, with packageRoot, and triggers is not composed;
    • control: a declared token that nothing depends on composes nothing;
    • an explicit provider in plugins wins and no second instance is composed;
    • a dependency with no supplying token is left to the kernel;
    • a resolved token with no posture (job, from the always-on slate) is refused by name.
  • src/utils/schema-migrate.requires-providers.integration.test.ts (integration tier, because it imports bootSchemaStack) runs a real kernel boot of a config with requires: ['automation'] and a plugin that hard-depends on the provider. The pin checks four things: the dependent init() ran after the provider's (it registers a connector provider factory), the config's flow is not registered (listFlows() is []), sys_automation_run and sys_flow_dispatch are in the object set, and the composition note is printed.
  • Ablation, committed-state, through scripts/ablation-replace.mjs. The mutation replaced plugins.push(...resolved.plugins); with a no-op. The anchor went from 1 to 0 and the blob from 1ad093cfbcd6 to 7465100641c3. Both new pins went red: the unit composition case, and the integration boot with exactly the defect's message, [Kernel] Dependency 'com.objectstack.service-automation' not found for plugin 'com.example.os21732.connector' (2 failed, 36 passed). After the restore, the blob equals HEAD and git diff HEAD is empty. The suite reads src directly, so no dist leg applies.

Local gates (head 826d5ce44)

  • pnpm --filter @objectstack/cli typecheck: exit 0 (tsc --noEmit plus check:test-typecheck OK).
  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: 3776 tests, 11 failed in 3 files on the first run. Two of those files, published-subpath-console.pin and published-subpath-hook-body.pin, read the packed .d.ts, and this worktree's CLI dist had been built with OS_SKIP_DTS=1. The third was a 5000 ms timeout in hook-timeout-override-refusal, on a shared box. After a real-types pnpm --filter @objectstack/cli build, all three files pass (33/33). The integration tier is left to CI, except for the new file, which ran locally and passed.
  • node scripts/pm/dispatch-gates.mjs --commands: 64 derived commands, all run. The final exit is 0 for all 64. check:dts-closure and check:dual-build-cjs-loads were red on the first pass for the same OS_SKIP_DTS reason and went green after the real-types build. --ran reconciliation: 64 derived, 64 run, 0 unrun.
  • eslint, narrowed to the three changed .ts files. I ran eslint --no-inline-config --format json: 3 files, 0 errors, 0 warnings. The lint population comes from eslint.config.mjs (eslint ., and these files are not ignored). The narrowing does not hide anything: this config never enables type-aware linting (no parserOptions.project, as the config itself states), so this diff cannot change the verdict on any file it does not touch. The full pnpm lint run is CI's.

Acceptance notes

  • Plan coverage of requires-supplied providers that nothing depends on (not fixed here; reported to the PM). The showcase declares approvals, messaging and webhooks in requires, and their providers register sys_approval_*, sys_inbox_message / sys_notification_* and sys_webhook. The showcase plan above lists none of them, because this composition boots only the providers the kernel cannot order without. os serve composes all of them. Whether the plan should declare every requires-supplied provider's objects, and if so what posture would be safe for each start(), is a separate decision.
  • Fresh-database plan output carries pre-existing boot lines. These are WARN [ObjectQLPlugin] sys_metadata_activation is registered but could not be read, the federated-binding ERROR for the showcase's two external objects, and Core service missing: auth, job. They come from the platform plugins on a database that does not exist yet, and this diff does not introduce them.
  • The blank scaffold borrowed the workspace's node_modules rather than installing published 17.6.0. That way the run measures this tree's CLI and connectors.

Generated by Claude Code

claude added 3 commits October 4, 2026 12:04
…r a connector hard-depends on

Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
Co-authored-by: Claude <noreply@anthropic.com>
…ma-migration boot

Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
Co-authored-by: Claude <noreply@anthropic.com>
… schema-migration boot

Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 4, 2026
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/protocol/kernel/lifecycle.mdx (via pluginName (symbol, a top-level function))
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 — 27 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 316be321ef2405a12e8d016f07b25fad7039b4f9 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 316be321ef2405a12e8d016f07b25fad7039b4f9

⚠️ 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 316be321ef2405a12e8d016f07b25fad7039b4f9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

This branch has not been deployed

No deployments
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/m tests tooling

Projects

None yet

2 participants