Repository navigation
Commit 40eaae7
feat(cloud-connection)!: refuse a managed upgrade that renames or retypes a stored field until it is declared or confirmed (#31243)
<!-- claude-projects-attribution: {"github_login":"marchtian"} -->
_Requested by **田騏** · [project
thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiWDNwmTd9esHVSgrxXCmHkf)_
Fixes #30884
Clause-②: yes (narrowing) — a managed re-install that succeeds today is
refused `409 SCHEMA_UPGRADE_CONFIRMATION_REQUIRED` on an undeclared
suspected rename or a `needs_confirm` / `destructive` type change, and a
declared rename is refused `501 NOT_IMPLEMENTED` until #30885; the
widening half is the new ledger code and the `confirmSchemaUpgrade` body
key / `--confirm-upgrade` flag. Ruling record 6709061297.
Metadata-protocol: none
Skills-docs: updated here — content/docs/deployment/cli.mdx
(`--confirm-upgrade` paragraph beside `--confirm-access-upgrade`),
content/docs/data-modeling/fields.mdx (the `renamedFrom` row: such an
upgrade is refused 501 until #30885); `skills/**` teaches neither flag
nor `renamedFrom`, so no skill change.
> **BREAKING** — accept-set narrowing at `POST
/api/v1/marketplace/install-local` for a managed re-install. A
re-install whose package upgrade plan lists an undeclared suspected
rename, or a type change that is not `safe`, is now refused `409
SCHEMA_UPGRADE_CONFIRMATION_REQUIRED` until the same call carries the
finding set's token; one that declares a field rename (`renamedFrom`) is
refused `501 NOT_IMPLEMENTED` until the runtime can carry the column
(#30885).
>
> **Migration.** Declare the rename on the new field (`renamedFrom:
['<old key>']`) and rebuild, or confirm the findings as listed: `os
package install <artifact> --confirm-upgrade <findingSet>` (`os deploy
--confirm-upgrade <findingSet>` against a self-hosted runtime), or
`"confirmSchemaUpgrade": "<findingSet>"` in the install-local body.
`--dry-run` previews the plan first.
>
> **ADR-0087 disposition** (the changeset carries the one marker):
not-required (no-migration-prescription) — no schema key is added,
removed or re-shaped; the refusal judges two package bodies at the
install door and prints its own way through.
## 维护者速读
-
改了什么:受管软件包重装(升级)时,在写入任何东西之前先比较已装版本和新版本。没声明的疑似改名、或会改动已存数据列的类型变化(needs_confirm
/ destructive),返回 409
`SCHEMA_UPGRADE_CONFIRMATION_REQUIRED`,附每条发现和一个确认令牌;带上令牌(`--confirm-upgrade`)重发即安装,并在安装记录里留下谁在何时确认了哪一组发现。声明了
`renamedFrom` 的改名在搬列能力(#30885)落地前回 501。`safe` 类型变化、删字段、种子变化不拦。
- 为什么改:今天改名会静默变成一个新的空列,旧数据还在旧列里但看不到;改类型也是静默的。这是 Q3 「默认拒绝,需显式映射」 的落实,田騏
已对本 PR 裁定 A。
- 风险与回滚:只影响受管包的重装;首次安装、unmanaged
安装、无发现的升级行为不变。误报(真的删一个、加一个无关字段)代价是一次确认。回滚即还原本 PR;ledger 里新增的码是纯加法。
- 先看:`packages/cloud-connection/src/marketplace-install-local-plugin.ts`
的 `schemaUpgradeVerdict`(哪些发现要确认、令牌怎么算)和 `evaluateSchemaUpgradeGate`(501
/ 409 / 放行三条路);`packages/spec/src/api/error-code-ledger.zod.ts` 的新码一行。
- 两分钟验证:`pnpm --filter @objectstack/cloud-connection exec vitest run
src/marketplace-install-local-upgrade-plan.test.ts`,预期全部通过(step 2c′
一组,含孤儿安装、记录损坏或缺失、声明改名 501 等情形)。
- 需要你做的:无,席位落地。
## What changes
PR 2 of 2 on #30884 (PR 1 #30983 landed the comparison, the environment
half and `dryRun`; handoff 6688284693). Under the ruling record
6709061297 (the maintainer chose 「A 全部同意(推荐)」; C1–C4 all A):
- **The ledger** (`packages/spec/src/api/error-code-ledger.zod.ts`): one
code, `SCHEMA_UPGRADE_CONFIRMATION_REQUIRED`, under
`@objectstack/cloud-connection` (C1), in that array's alphabetical
order. No other `packages/spec/src/**` hunk.
- **The verdict** (`schemaUpgradeVerdict`, module-level in
`marketplace-install-local-plugin.ts`): reads `comparePackageVersions`
alone (C3), each side folded by `resolveStackCollection` as
`planUpgrade` folds it. Declared renames are listed on their own; a
suspected rename always needs confirming; a type change needs confirming
when its category is `needs_confirm` or `destructive`, on a declared
rename too (scope note 6685166393). The token is `schema-upgrade:` plus
32 hex of a SHA-256 over the findings' identities, sorted, never their
sentences or row counts: the same two versions always give the same
token, any change to the set gives another.
- **The gate** (`evaluateSchemaUpgradeGate`, step 2c′ after the access
gate 2c, C4), managed re-install only (`conflict === 'marketplace'`),
before any write:
- a declared rename ⇒ `501 NOT_IMPLEMENTED`, naming each `object.old →
object.new`, token or not (C2); the findings a token would still owe are
listed in `details.findings` beside it;
- findings ⇒ `409 SCHEMA_UPGRADE_CONFIRMATION_REQUIRED` with
`details.findingSet`, `details.findings` (each with `kind` and a
one-line `describe`), `details.confirmWith`; the message names both ways
forward (declare `renamedFrom` and rebuild, or confirm with the token);
a token from another set gets the stale-token sentence;
- the matching token ⇒ installs, and the ledger entry records
`schemaUpgradeConfirmation` (`findingSet`, `confirmedBy`,
`confirmedAt`);
- no finding ⇒ unchanged, `planUpgrade` never asked.
- When the `manifest` service answers `planUpgrade`, each refused
finding carries `rows` (the installed column's non-null rows) and its
`describe` says so (C3); without it, or when the reading throws, the
refusal is the same without counts.
- **The CLI**: `os package install --confirm-upgrade TOKEN` sends
`confirmSchemaUpgrade`, and the 409 prints every finding, the
`renamedFrom` way forward and the exact retry. `os deploy
--confirm-upgrade TOKEN` passes it through to the self-hosted install,
as `--confirm-access-upgrade` is (see Acceptance notes: a file-surface
extension).
## Tests
Each under `os-verify-lock.sh` with its `VERDICT command-exit` line:
- `@objectstack/cloud-connection`
`marketplace-install-local-upgrade-plan.test.ts`: 16/16, ten of them the
new `step 2c′` describe — the 409 with findings, token, both ways
forward and row counts, nothing written; the token installing and the
ledger entry's `schemaUpgradeConfirmation`; another finding set refusing
the old token; a `needs_confirm` retype refused; a declared rename
refused 501 by name with and without a token, nothing written; a
declared rename that also retypes `needs_confirm` still listing the type
change; a `safe` retype, a removed field and a seed change installing
unrefused; a no-finding re-install unchanged (`planUpgrade` never
asked); no `planUpgrade` ⇒ the same refusal without `rows`; a first
install never compared. **Round 2 (seat r1 6712105906, Fable r2
6714005316)** adds, across `marketplace-install-local-upgrade-plan` +
`-corrupt-ledger` + `-conflict` (37/37): an orphan's retype refused 409
against the registry body, with a clean-orphan 200 control; an orphan
row without a body refused 409 `installed_record_missing`, its token
installing; a corrupt ledger entry refused 409
`installed_record_unreadable` (file, cause), its token installing; a
corrupt entry plus a declared `renamedFrom` refused 501 with and without
the record's token, nothing written; a `destructive` text→formula retype
refused 409; the 501 tests with real tokens; `true` confirms nothing.
`marketplace-install-local-conflict.test.ts`'s orphan upgrade is
unchanged (200, assertion untouched).
- PR 1's control test now upgrades without a finding (V1 → V2 is a
suspected rename, so it is refused by this PR — a stale fixture,
re-judged, not weakened).
- `@objectstack/cli`: `package-install-dry-run.test.ts` gains the
`--confirm-upgrade` describe (the flag reaches the body as
`confirmSchemaUpgrade`, absent without it; the 409 prints each finding,
the `renamedFrom` way and the exact retry, exit 1); `deploy.test.ts`
gains the pass-through (retry line names `os deploy --confirm-upgrade`,
the token reaches the second POST only) and the Cloud-target refusal.
<details><summary>Suite readings</summary>
- cloud-connection `pnpm test` on `39a525c236`: Test Files 73 passed
(73), Tests 969 passed (969), VERDICT command-exit 0; `typecheck` exit
0.
- cli `vitest run --project unit` on `a274e36e`: Test Files 399 passed
(399), Tests 5637 passed (5637), VERDICT command-exit 0; `typecheck`
VERDICT command-exit 0. No cli file changed after `a274e36e`.
- spec: the only hunk since round 1 is a clause in the ledger comment;
spec `pnpm test` (round 1): Test Files 821 passed, Tests 25964 passed, 1
todo, VERDICT command-exit 0.
- Merges of `origin/main` at `02481a8990`, `d04e0e2c88` and `39a525c236`
(the last brings #31284); no rebase, no force push.
</details>
<details><summary>Ablations (each via scripts/ablation-replace.mjs,
anchor hit once, restored to the HEAD blob with git diff HEAD
empty)</summary>
The door's tests import the plugin from `src/` (relative import), so no
build sits between a mutation and the run.
| # | Mutation in `marketplace-install-local-plugin.ts` | Result |
|---|---|---|
| A1 | suspected renames dropped from the verdict | 4 failed / 12 passed
|
| A2 | the `needs_confirm`/`destructive` filter removed (a `safe` retype
refused) | 1 failed / 15 passed (`a safe type change … install
unrefused`) |
| A3 | any string token accepted | 1 failed / 15 passed (`a different
finding set refuses the old token`) |
| A4 | the 501 branch disabled | 2 failed / 14 passed (both
declared-rename tests) |
| A5 | a type change riding on a declared rename skipped (scope note) |
1 failed / 15 passed (`… still owes the type change its confirmation`) |
| A6 | `schemaUpgradeConfirmation` not written to the ledger entry | 1
failed / 15 passed (`the same call with the token installs …`) |
| A7 | the record-unavailable branch returns no finding | 2 failed |
| A8 | the `declaredRenames.length === 0` guard removed | 2 failed (both
declared-rename cases) |
| A9 | `destructive` dropped from the confirmed set | 1 failed |
| A10 | `manifestId` dropped from the token digest | 2 failed |
| A11 | `registeredPackageBody` returns `undefined` | 2 failed (conflict
orphan upgrade + orphan compare) |
| A12 | `declaredRenamesOfIncoming` replaced by `[]` | 1 failed (corrupt
entry + rename 501) |
Each restored, `git diff HEAD` empty, suites green again.
</details>
## Acceptance notes
- **File surface, one extension to ratify:**
`packages/cli/src/commands/deploy.ts` (+ `deploy.test.ts`) is outside
the claim's list. `os deploy` runs `os package install` in-process with
`retryCommand = 'os deploy'`, so without it the new refusal would print
`os deploy --confirm-upgrade TOKEN`, a flag `os deploy` would reject:
the self-hosted dev loop (the epic's target) would have no way through.
The change mirrors `--confirm-access-upgrade` exactly.
`local-manifest-source.ts` gains the `schemaUpgradeConfirmation` type on
`InstalledEntry` (the ledger entry the claim names).
- **No installed body asks, never fails open.** When the gate has no
installed body to compare (the ledger file will not parse, the entry
carries no body, or an orphan's registry row carries none), it answers
`409 SCHEMA_UPGRADE_CONFIRMATION_REQUIRED` with one finding,
`installed_record_unreadable` (file, cause) or
`installed_record_missing`; that set's token installs and records
`schemaUpgradeConfirmation`.
- **An orphaned install** (no ledger file, the registry row found by
`findConflict` step 3) is compared against the body the registry serves.
For a multi-package artifact this reads one row; the JSDoc states the
limit.
- **A declared rename is 501 on every path**, including the
record-unavailable one, where the renames are read from the incoming
version alone (`declaredRenamesOfIncoming`); the record finding is
listed beside it, with or without a token.
- **Reported, not refused**: removed fields and objects, `safe` type
changes and seed changes reach the installer through `dryRun: true` (PR
1); a successful install's answer gains nothing.
- **Two refusals in a row are possible** (C4's accepted cost): an
upgrade can answer the access 409 first, then the schema 409.
- **The dry run carries no schema token**: an installer reads the token
off the 409, not off the plan. Adding it to the dry run would be a
widening of its own.
- **The 501 names the way to install now** (drop `renamedFrom`, confirm
the suspected rename) because it exists; it says the values stay in the
old column.
- **Ledger position**: the code sits in
`@objectstack/cloud-connection`'s array in alphabetical order (after
`RESEED_SKIPPED`); its comment names
`ACCESS_UPGRADE_CONFIRMATION_REQUIRED` as its sibling.
- **Out of scope, noted**: cloud's hosted install door (sibling card
01c) reads the same 409.
## Verification
All on `39a525c236` (after merging `origin/main` incl. #31284
`59d9815a03`):
- Derived gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` → 116 families, each exit captured before
any pipe; `--ran`: "116 derived famil(ies) accounted for — 116 run, 0
NOT-MEASURED". All 116 exit 0. `pnpm lint` exit 0. (`check:slot-lookup`
was red on `a274e36e` for an `any`-typed `ctx.getService`; typed in
`5c6cd1cfd7`, holds since.)
- Full build `pnpm turbo run build --filter=!@objectstack/docs`: exit 0.
- **Dogfood** (narrowing arm), full battery under the lock: VERDICT
command-exit 0 (27m32s) · Test Files 435 passed, 3 skipped (438) · Tests
3610 passed, 41 skipped (3651) · no failure.
- PR check-runs on `39a525c236`: 57 success, 8 skipped, none red.
- Dev report: #30884 comment 6714956140.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017ihL2dvnT51fbSebUyE3zW)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- signed: domain:server#2 · marchtian ·
session_017ihL2fBJ6xsLWJsnNJGMMx -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 9020358 commit 40eaae7
15 files changed
Lines changed: 1028 additions & 16 deletions
File tree
- .changeset
- content/docs
- data-modeling
- deployment
- references/api
- packages
- cli
- src/commands
- package
- test
- cloud-connection/src
- spec/src/api
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
478 | 478 | | |
479 | 479 | | |
480 | 480 | | |
481 | | - | |
| 481 | + | |
482 | 482 | | |
483 | 483 | | |
484 | 484 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3518 | 3518 | | |
3519 | 3519 | | |
3520 | 3520 | | |
| 3521 | + | |
3521 | 3522 | | |
3522 | 3523 | | |
3523 | 3524 | | |
| |||
3549 | 3550 | | |
3550 | 3551 | | |
3551 | 3552 | | |
| 3553 | + | |
| 3554 | + | |
| 3555 | + | |
| 3556 | + | |
| 3557 | + | |
| 3558 | + | |
| 3559 | + | |
| 3560 | + | |
| 3561 | + | |
| 3562 | + | |
| 3563 | + | |
| 3564 | + | |
| 3565 | + | |
| 3566 | + | |
| 3567 | + | |
| 3568 | + | |
| 3569 | + | |
| 3570 | + | |
| 3571 | + | |
3552 | 3572 | | |
3553 | 3573 | | |
3554 | 3574 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
417 | 417 | | |
418 | 418 | | |
419 | 419 | | |
420 | | - | |
| 420 | + | |
421 | 421 | | |
422 | 422 | | |
423 | 423 | | |
| |||
724 | 724 | | |
725 | 725 | | |
726 | 726 | | |
| 727 | + | |
727 | 728 | | |
728 | 729 | | |
729 | 730 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
| 31 | + | |
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| |||
342 | 342 | | |
343 | 343 | | |
344 | 344 | | |
| 345 | + | |
345 | 346 | | |
346 | 347 | | |
347 | 348 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
509 | 509 | | |
510 | 510 | | |
511 | 511 | | |
| 512 | + | |
512 | 513 | | |
513 | 514 | | |
514 | 515 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
61 | 61 | | |
62 | 62 | | |
63 | 63 | | |
64 | | - | |
65 | | - | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
66 | 67 | | |
67 | 68 | | |
68 | 69 | | |
| |||
102 | 103 | | |
103 | 104 | | |
104 | 105 | | |
| 106 | + | |
105 | 107 | | |
106 | 108 | | |
107 | 109 | | |
| |||
140 | 142 | | |
141 | 143 | | |
142 | 144 | | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
143 | 155 | | |
144 | 156 | | |
145 | 157 | | |
| |||
151 | 163 | | |
152 | 164 | | |
153 | 165 | | |
154 | | - | |
| 166 | + | |
155 | 167 | | |
156 | 168 | | |
157 | | - | |
| 169 | + | |
| 170 | + | |
158 | 171 | | |
159 | | - | |
| 172 | + | |
160 | 173 | | |
161 | 174 | | |
162 | 175 | | |
| |||
285 | 298 | | |
286 | 299 | | |
287 | 300 | | |
288 | | - | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
289 | 306 | | |
290 | 307 | | |
291 | 308 | | |
| |||
317 | 334 | | |
318 | 335 | | |
319 | 336 | | |
320 | | - | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
321 | 341 | | |
322 | 342 | | |
323 | 343 | | |
| |||
0 commit comments