Repository navigation
Commit 3c38025
feat(cli)!: os migrate refuses a config whose AI plugin only os serve would mount, reading serve's own auto-inject rule (#31446)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X2kM3DQTy5XByDHKN47ZrVA)_
domain:server#5 · os-project-manager · session_01FEkQ5mbAyTfp7gbjowRA48
Fixes #31411
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no file under `skills/**` teaches that `os serve`
auto-injects the AI plugin or that `os migrate` composes it:
`skills/objectstack-platform` names `AIServicePlugin` as cloud / EE only
and mounted by the cloud host, which is the explicit mount the refusal
asks for; `content/docs/` does not describe the migrate composition's AI
handling either.
## 维护者速读
- **改了什么**: `os serve` 的 AI 自动注入规则搬进一个导出的谓词 (`resolveAiServiceLoad` +
`mountsAiServicePlugin`)。`serve` 调它决定加载什么 (对每种输入结果不变);`os migrate plan`
/ `apply` / 归属仪式的 `--apply` 调同一个谓词,在“serve 会自动注入 AI 而配置没挂 AI
插件”时于规划前拒绝,并给出补救:在配置里显式挂 AI 插件。
- **为什么改**: migrate 不组合 serve 自动注入的 AI 插件,`requires: ['ai']` 的配置被规划时缺少
serve 声明的 `ai_*` 表,`apply` 照样执行 (#31411,裁定 B,「以上都同意」)。
- **风险与回滚**: 收窄:依赖自动注入又跑 migrate 的应用会被拒绝 (退出 1,附原因与补救)。规则只有一处,serve 的决定有
40 组输入的对照钉。回滚即 revert 本 PR。
- **先看**: `packages/cli/src/utils/ai-service-auto-inject.ts` 与
`schema-migration-plugins.ts` 中 `composeServedPlatform` 的拒绝块。
- **两分钟验证**: `cd packages/cli && npx vitest run --project unit
src/commands/migrate/plan.ai-auto-inject-refusal.test.ts
src/utils/ai-service-auto-inject.test.ts
test/serve-optional-plugin-intent.test.ts`
- **需要你做的**: 无,席位落地。
## What changes
- **One reading of the rule.** New
`packages/cli/src/utils/ai-service-auto-inject.ts` exports
`resolveAiServiceLoad({ declaredRequires, aiTierEnabled,
hostDeclaresDependency })` (`required` when `requires` names `ai` or
`ai-studio`, `auto` when the app's `package.json` declares
`@objectstack/service-ai` or `-studio`, `off` when the `ai` tier is
off), `mountsAiServicePlugin(plugins)` (by registered name or class
name, as serve read it), and the refusal text.
`Serve.resolveOptionalPluginLoad` delegates to the same module.
- **serve calls it.** `serve.ts` drops its inline `wantsAiService` /
`aiRequired` / `hasAIPlugin` reading and calls the two functions. Its
decision is unchanged for every input (pinned below). The Studio half
still reads `hostDeclaresDependency`, and Studio still loads only once
the base did.
- **The migrate composition refuses with it.** `composeServedPlatform`
resolves the boot's tiers as serve does and asks the predicate. When the
answer is `required` or `auto` and none of the base, host or composed
plugins is the AI plugin, it throws before planning: "`os serve`
auto-injects the AI plugin (@objectstack/service-ai) for this config —
{reason} and the config mounts no AI plugin — and `os migrate` does not
compose it … Remedy: mount the AI plugin explicitly in the config
(`plugins: [new AIServicePlugin()]`, imported from
@objectstack/service-ai), then re-run." plan / apply answer it as `{
"error": … }` with exit 1, as their other boot refusals do; the
ownership ceremony answers it as `reason: "boot-failed"` with the text
as `detail`.
- **stdout is handed back on a refused composition.** `bootSchemaStack`
reserved stdout for the JSON document before composing, and a
composition that threw left the reservation in place, so the next writer
in the same process lost its document. It now releases stdout before
rethrowing. This applies to every composition throw, not only this one.
- Changeset `31411-migrate-refuses-serve-only-ai-plugin.md`:
`@objectstack/cli` patch (the level follows the metadata protocol, and
this PR declares `Metadata-protocol: none`), with a BREAKING section
(Why / Before / Now / Who moves / Unchanged) and the ADR-0087
disposition `not-required (no-migration-prescription)`.
## Tests
-
`packages/cli/src/commands/migrate/plan.ai-auto-inject-refusal.test.ts`
(unit tier, runs the real commands against a temp project and SQLite
file):
- ⭐ `requires: ['ai']`, no AI plugin: `plan` and `apply` exit 1, naming
`requires`, `@objectstack/service-ai` and the remedy.
- ⭐ `package.json` declares `@objectstack/service-ai`, no AI plugin:
`plan` exits 1, naming `package.json` and the remedy.
- ⭐ the ownership ceremony's `--apply` with `requires: ['ai-studio']`:
exit 1, `reason: "boot-failed"`, detail carries the remedy.
- Controls: an explicit AI plugin (with `requires: ['ai']` and the
dependency) plans as today; the `ai` tier off (`tiers` without `ai`,
dependency declared) plans as today; a config with no AI intent plans as
today.
- `packages/cli/src/utils/ai-service-auto-inject.test.ts`:
- ⭐ the predicate answers what serve's old inline reading answered for
all 40 combinations of `requires`, tier and declared dependency (the
oracle is that reading, kept in the test).
- the mounted-plugin reading, and the refusal text names the reason and
the remedy.
- ⭐ no second copy: `serve.ts` and `schema-migration-plugins.ts` call
the predicate, and neither restates `has('ai')` / `has('ai-studio')` or
the plugin names.
- The existing `test/serve-optional-plugin-intent.test.ts` passes
unchanged.
## Acceptance notes
- Red on main: the three ⭐ refusal pins fail on `origin/main` (3 failed
/ 3 passed; the controls pass).
- Ablation: with the refusal's condition replaced by `false`
(`scripts/ablation-replace.mjs`, anchor hit ×1 → ×0, blob `814b130b8ddc`
→ `d0210f7486c9`), the same three pins go red (3 failed / 3 passed);
restored to HEAD, `git diff HEAD` empty.
- The `auto` case (dependency declared, nothing in `requires`) is
refused too, because serve injects the plugin for it as well. Only the
`ai` tier being off, or the plugin mounted, lets it through.
- `os migrate unmapped-columns` shares the same composition, so it
inherits the refusal.
- `requires: ['ai']` always opens the `ai` tier (capability → tier
mapping), so the tier-off control uses a declared dependency, which is
the only intent the tier can deny.
## Verification
- `vitest run --project unit` on the three files above: 3 files, 21
tests passed.
- `tsc --noEmit` (cli) and `check:test-typecheck`: clean. `eslint` on
the six touched files: clean.
- Head `6c9a7c5d` (the commits after code head `ea6bebb6` touch only the
changeset: its Skills-docs line, then its release level set to patch);
origin/main merged at `0647dabb` before opening.
- Gates from `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 72 derived, 72 run, all exit 0 (`--ran`
reconcile: 0 NOT-MEASURED, 0 UNRUN). Two were red on the first pass and
fixed in this branch: the ADR-0087 disposition was missing from the
changeset, and `check:test-source-alias` asked for the commands' dynamic
imports at module top in the new test.
- Full cli unit tier: 403 files, 5697 tests passed.
- Full dogfood battery (owed: `!` title, `(narrowing)` arm), on code
head `ea6bebb6`, closure built with `pnpm --filter
'@objectstack/dogfood^...' run build`, then `pnpm --filter
@objectstack/dogfood test` under `os-verify-lock`: `VERDICT command-exit
0`, Test Files 438 passed | 3 skipped (441), Tests 3622 passed | 41
skipped (3663), no failures.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(not touched)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes. (eslint on the six
touched `.ts` files: exit 0)
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
(`.changeset/31411-migrate-refuses-serve-only-ai-plugin.md`,
`@objectstack/cli` patch, BREAKING section)
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (not touched)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English. (the 维护者速读 block is
the lane's required Chinese summary)
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01FEkQ5mbAyTfp7gbjowRA48
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5mbAyTfp7gbjowRA48)_
<!-- signed: domain:server#5 · os-project-manager ·
session_01FEkQ5mbAyTfp7gbjowRA48 -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent f67f3b3 commit 3c38025
7 files changed
Lines changed: 511 additions & 23 deletions
File tree
- .changeset
- packages/cli/src
- commands
- migrate
- utils
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| 13 | + | |
13 | 14 | | |
14 | 15 | | |
15 | 16 | | |
| |||
1650 | 1651 | | |
1651 | 1652 | | |
1652 | 1653 | | |
1653 | | - | |
1654 | | - | |
1655 | | - | |
1656 | | - | |
| 1654 | + | |
| 1655 | + | |
1657 | 1656 | | |
1658 | 1657 | | |
1659 | 1658 | | |
| |||
4970 | 4969 | | |
4971 | 4970 | | |
4972 | 4971 | | |
4973 | | - | |
4974 | | - | |
4975 | | - | |
4976 | | - | |
| 4972 | + | |
4977 | 4973 | | |
4978 | 4974 | | |
4979 | 4975 | | |
| |||
4996 | 4992 | | |
4997 | 4993 | | |
4998 | 4994 | | |
4999 | | - | |
5000 | | - | |
5001 | | - | |
5002 | | - | |
| 4995 | + | |
| 4996 | + | |
| 4997 | + | |
| 4998 | + | |
5003 | 4999 | | |
5004 | 5000 | | |
5005 | 5001 | | |
5006 | | - | |
5007 | | - | |
5008 | | - | |
| 5002 | + | |
5009 | 5003 | | |
5010 | 5004 | | |
5011 | 5005 | | |
| |||
5069 | 5063 | | |
5070 | 5064 | | |
5071 | 5065 | | |
5072 | | - | |
5073 | | - | |
5074 | | - | |
5075 | | - | |
5076 | | - | |
5077 | | - | |
| 5066 | + | |
| 5067 | + | |
| 5068 | + | |
| 5069 | + | |
| 5070 | + | |
| 5071 | + | |
5078 | 5072 | | |
5079 | 5073 | | |
5080 | 5074 | | |
| |||
0 commit comments