Repository navigation
Commit 9f0de32
fix(cli): os migrate meta --write rewrites the declared protocol range the load still refuses (#22252)
Fixes #22219
Clause-②: no
## What changed
`os migrate meta --from N --write` now rewrites the manifest's declared
protocol range when the load would still refuse the migrated source
under it. Before, it wrote the chain's mechanical edits and left
`engines: { protocol: '^16' }` as it was, so the load refusal that names
this command as its remedy came straight back.
- **Which range:** `--to`, capped at the protocol this runtime
implements, in the scaffold's spelling (`'^17'` on this build). See H5
for why the cap is needed.
- **Where:** in place, under the key the handshake read the range from
(`engines.protocol`, else `engines.platform`, else the legacy
`engine.objectstack`). See H2.
- **How:** the edit rides the chain's own plan, write and re-check. The
planner treats it as one more applied entry with one more change. A
re-check that disagrees restores it with every other file, and a range
edit that does not land is caught by that same re-check. See H3.
- **Nothing to convert:** the range edit is written on its own. See H4.
- **Left alone:** a range the load admits (`'>=16'`), an absent or
unparsable range, and a range at or above the target (a range is never
lowered). A range that `--from` starts above is also left, with a line
naming the `--from` that would move it.
- **Output:** the human report names the file, the line and the from →
to text. A dry run names the edit `--write` would make. `--json` reports
the edit as `write.range`, a NEW key (see H6).
## Measured on `main` `7b926f76` (H1, premise holds)
The CLI was built from that tree and each load went through `os serve`,
with `--from 16`:
- **Fixture A:** `^16` plus a tombstoned `views[0].list.striped`.
- Before: the schema refuses it. `✗ views.0.list.striped: … Run os
migrate meta --from 16 …`
- `--write` reported `Wrote 1 of 1 mechanical change(s) into 1 file(s)`.
The reload was then refused: `✗ package 'com.example.h1' targets
protocol ^16 (engines.protocol) but this runtime is protocol 17.0.0.
This is a major-version break. Run: objectstack migrate meta --from 16`
- **Fixture B:** `^16` with nothing to convert.
- Before: the same refusal at the handshake.
- `--write` reported `--write: the chain made no mechanical change here,
so no file was written.` The reload returned the same refusal.
## The PM's readings, measured
- **H2: the three homes.**
- No chain step reads or writes any of the three keys (no conversion or
migration entry names `engines` or `engine`).
- The schema accepts all three (`engines.platform: '^16'` and
`engine.objectstack: '^16.0.0'` both parse).
- The handshake still reads each one when `engines.protocol` is absent.
Both are refused with `migrateCommand: objectstack migrate meta --from
16`.
- Moving a range between keys would be a conversion the chain has not
declared. The key the handshake read is the key it reads again on
reload. So the edit is **in place**, and both legacy homes are pinned.
- One measured wrinkle: `engine.objectstack: '^17'` is refused at parse
(`invalid_format`, because the schema wants a full version). That home
is therefore written `'^17.0.0'`.
- **H3: the plan.** The range is a `RangeRewrite` input to
`planAuthoredSourceWrite`. It joins as the last entry and is reported
apart (`plan.range`). `verifyAuthoredSourceWrite` takes the range the
written sources still owe:
- when the range was written, it must be absent from the re-run;
- when the range was left, it must still be there.
The restore case is pinned. Its error reads `still converted:
manifest.engines.protocol (declared protocol range)`.
- **H4: no conversions.** Measured above (`no file was written`). The
range edit is now written alone, and the report says so.
- **H5: which range.**
- `os init`, the `create-objectstack` blank template and `os lint`'s fix
all write `'^' + PROTOCOL_MAJOR`, and that form is used here. A range
that already admits the target (`'>=16'`) is pinned as a no-op.
- **`--to`:** on this build `--to` defaults to 18 (the chain's terminus)
while the runtime implements 17.
- A literal `'^18'` is refused by this runtime. Measured through `os
serve`: `targets protocol ^18 … Run: objectstack migrate meta --from
18`. So the written major is `min(--to, PROTOCOL_MAJOR)`: `--to` above
the runtime writes the runtime's major, and `--to` below it writes
`--to`.
- **H6: `--json`.** The edit does not fit the existing `write.written` /
`write.manual` lists. Those list exactly the chain's `applied` entries,
an invariant already pinned (`writes the applied set and nothing else`),
and the range is not one of them. It is reported as a **new key**,
`write.range`: `status`, `path`, `from`, `to`, plus `file` / `line` when
written or `kind` / `reason` when left. Per the claim, the seat amends
the Clause-② line if this key needs it; line 2 above is `no` as
dispatched. A dry run's `--json` carries no range, because that would be
a new top-level key.
## Pins (`packages/cli/test/migrate-meta-protocol-range.test.ts`, unit
tier)
The pins run in-process over `MigrateMeta.run`, and each reload goes
through the load's own door: the CLI's `loadConfig` (whose `defineStack`
runs the schema), then `AppPlugin.init` (whose handshake runs before the
manifest registers).
- `--from 16` with a converted key:
- Control: the load refuses it (`STACK_SCHEMA_INVALID`, naming `migrate
meta --from 16`).
- The dry run names the edit and writes nothing.
- `--write` rewrites the range and the key, and only those bytes change.
- The reload is **not refused**.
- `--from 16` with nothing to convert:
- Control: the load refuses it at the handshake
(`OS_PROTOCOL_INCOMPATIBLE`, status 422, `migrateCommand: objectstack
migrate meta --from 16`).
- `--write` writes the range alone and says so.
- The reload is not refused.
- `'>=16'` is left byte for byte, and `write.range` is absent.
- Legacy homes: `engines.platform` and `engine.objectstack` are
rewritten in place, and each reload is not refused.
- `--to` is capped at the runtime's major. A `--from` above the range is
left, and the report names `--from 16`. The planner never lowers a
range, and never touches an absent, unparsable, admitted or non-string
range.
- Codemod:
- a forced re-check mismatch restores the range with the chain's edit;
- a range edit that does not land is caught and everything is restored;
- a shared range literal is refused (`shared`) while the chain's edit
beside it is written.
- The 17 → 18 shape is NOT added here (it waits for #22215). It is one
more `SHAPES` row:
```ts
{ from: 17, member: "dashboards: [{ name: 'ops', label: 'Ops', widgets: [], refreshInterval: 300 }]", converted: ['dashboard-refresh-interval-to-refresh-interval-seconds'], before: 'refreshInterval: 300', after: 'refreshIntervalSeconds: 300' },
```
## Ablation (one-off, `meta.ts` restored to a blob equal to HEAD
`ecbcd924`, `git diff HEAD` empty)
The test imports `meta.ts` relatively, so it resolves to source and no
rebuild applies. Each mutation went through
`scripts/ablation-replace.mjs`, which reported `anchor 1 → 0` and a
changed blob. Both legs ran with a trap that restores from HEAD.
- **A: the range taken out of the plan.** The `range` argument to
`planAuthoredSourceWrite` was deleted.
- Result: **8 failed, 6 passed.**
- With a converted key, the re-check caught the missing range and
restored every file. The reload is then red with the schema refusal,
because the write was undone. This is a different direction from B, and
it is the H3 guarantee doing its job.
- With nothing to convert, nothing is written.
- **B: no range owed at all**, which reproduces the world before this
fix.
- Result: **11 failed, 3 passed.**
- The reload pin turns red with exactly the measured refusal:
`ProtocolIncompatibleError … targets protocol ^16 (engines.protocol) but
this runtime is protocol 17.0.0 … Run: objectstack migrate meta --from
16`.
## Verification (at `9578a2af`, `main` `f4bed583` merged in)
- New pin file: 14/14 passed. It is a member of the unit project
(`vitest list --project unit`).
- `@objectstack/cli` unit project: **264 files, 3888 tests passed**.
- The `migrate meta` and codemod files (7 files): 75 passed, plus 1
pre-existing `skipIf` skip.
- `pnpm --filter @objectstack/cli typecheck`: exit 0. The `tsc --noEmit`
and `check:test-typecheck` steps both pass, and the new test is in
`tsconfig.test.json`'s program.
- `pnpm lint` (full `eslint . --no-inline-config`): exit 0.
- `dispatch-gates --ran`: 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN.
The list is identical to the dispatched one.
- `check:dual-build-cjs-loads` and `check:i18n-coverage` first exited 3
(PREREQUISITE NOT MET: dist absent for packages outside the CLI
closure). Re-run once those dist trees existed, both exit 0.
- Integration tier: declared to CI. The diff touches no integration-tier
file and no spawn entry.
## Acceptance notes
- **Docs.** `content/docs/upgrading.mdx` (the `--write` row and callout)
and `skills/objectstack-data` (`--write applies the proven sites`,
landed in #22199) do not mention the range rewrite. Both are outside
this card's file surface (`content/docs`) or a governed surface
(`skills/**`).
- **`--out` snapshot.** The `--out` snapshot (`result.stack`) keeps the
authored range. The docs already call it an oracle to diff against, not
a file to ship.
- **Duplicate narrowing.** `handshakeSlice` in `meta.ts` repeats the
type narrowing that `toHandshakeManifest` in
`utils/protocol-version-gap.ts` does. That file is not on this card's
surface, so it was not exported from there.
- **Load-path conversions are never written.** A key that a load-path
conversion converts (`retiredFromLoadPath: false`, e.g. `driver:
'mongo'`) in a stack the schema otherwise accepts is converted by
`defineStack` during the authored-source load. The chain therefore never
sees it, and `--write` never writes it. Measured: `--from 16 --write
--json` gives `applied: []` while the source keeps `'mongo'` and the
load warns. Reported to the PM; not changed here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 3513ac7 commit 9f0de32
4 files changed
Lines changed: 895 additions & 45 deletions
File tree
- .changeset
- packages/cli
- src
- commands/migrate
- utils
- test
| 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 | + | |
0 commit comments