Repository navigation
Commit 10454b3
Fixes #21528
Clause-②: no
## What changed
`runMigrationJournal` (`packages/core/src/utils/migration-journal.ts`)
recomputed a resumed run's chunk plan from the rows `load()` returns at
resume time, at the resumed plan's chunk size, and refused
`PLAN_CHANGED` when that plan's hash differed from the one `run_started`
recorded. A resume now reads the chunk plan back from `run_started`,
which has carried it since the runner's first commit (ADR-0119 D2 item
2: "carrying the plan hash and chunk plan"):
- **Identity, the one place the journal hashes.** `hashMigrationPlan` is
unchanged. On a resume it hashes the RECORDED chunk boundaries with the
plan's declared id and step names, so it compares the plan against what
the run started over. The run's chunk size comes back from the journal.
A plan whose id or steps changed still refuses `PLAN_CHANGED`.
- **Rows.** Per step, with N rows started over and K of them in
committed chunks: a `load()` that returns N rows binds positionally, as
before. One that returns exactly N − K rows (it selects only the
remaining work, as `recorded-by`'s does) binds those rows, in order, to
the chunks not yet committed. Any other count refuses `PLAN_CHANGED` and
names the step. Every resume that passed before binds exactly as before
(N rows, the same boundaries).
- **Unwind after such a resume.** A chunk an earlier process committed,
whose rows `load()` no longer returns, cannot be compensated. The unwind
compensates this process's chunks newest-first, then halts with
`run_failed` at that chunk with a `reason`. It does not hand
`compensate()` other rows and journal a clean unwind.
- A `run_started` with no recorded chunk plan (only a hand-written
journal) keeps today's check: reproduce the recorded hash from the
current rows.
No published member is added. `MigrationPlan`, `MigrationPlanStep`,
`MigrationJournalEvent` and the `run_started` payload keep their shapes.
`MigrationPlanStep.load` and `RunMigrationJournalOptions.chunkSize` gain
TSDoc for what a resume does with them. The plan
(`recorded-by-sentinel.ts`), `os migrate resume`'s source and the list
mode's `resumable` are untouched. No new error code: both new refusals
are `PLAN_CHANGED`, the code the same inputs drew before.
## The public door, before and after
`packages/cli/src/commands/migrate/resume.recorded-by.integration.test.ts`
now makes three more interrupted runs the way a crash does (a child
process runs the recorded-by plan under the real runner and is SIGKILLed
inside a chunk's transaction) and drives the real `os migrate resume
--json`:
- **(a) 203 sentinel rows, killed in chunk 1 after chunk 0 committed.**
The list says `resumable: true`, `committedChunks: [0]`, `unknownChunks:
[1]`. Before (core built from `1ac7308d7a`): `--run RUN_ID --yes` exited
1 with `Refused (PLAN_CHANGED): ... plan hash
037ebfcc70ee54096b8eb2a6aed8cd49 does not match the journal's
f36863e5fee4bb4e40093c66a0b3096f`. After: exit 0, `completed`, 2 of 2
chunks, no row left holding the sentinel, `chunk_started` indices `[0,
1, 1]` (chunk 0 is not run again).
- **(b) 3 rows started at chunk size 2, killed in chunk 0.** The list
says `resumable: true`. Before: exit 1, `PLAN_CHANGED`. After: exit 0,
`completed`, `chunksTotal: 2` (the journal's size; the registered plan's
default 200 would make one chunk).
- **(c) The control.** A run started by a plan whose step had another
name: exit 1, `Refused (PLAN_CHANGED)`, before and after. The sentinel
rows and the journal are untouched.
Before the fix that file read 2 failed / 7 passed (the two acts above);
after, 9 passed.
## Tests (at `14e5287fa8`, this branch's head)
- `packages/core/src/utils/migration-journal.test.ts`: 29 passed (23
before + 6 new: a shrinking load resumes after a committed chunk; a
non-shrinking load resumes positionally from a runner-written journal;
the journal's chunk size wins over the plan's; the changed-plan control,
three ways (step renamed, plan id changed, step added), each refused
with `code: 'PLAN_CHANGED'` and zero journal writes; a row count that is
neither N nor N − K is refused, naming the step; the unwind halt). The
crash helper runs the REAL runner and stops a forward inside its chunk,
so every resume reads a journal the runner wrote.
-
`packages/metadata-protocol/src/migrations/recorded-by-sentinel.test.ts`:
8 passed (1 new: the real plan, started at size 2 and killed after chunk
0 committed, resumes with the plan the owner registers at its default
size, 2 of 2 chunks).
- The door file above: 9 passed (`--project integration`, run locally
because this diff edits that file).
- Typecheck: `@objectstack/core` (with `check:test-typecheck`: 4 files /
4 errors held, unchanged), `@objectstack/metadata-protocol`,
`@objectstack/cli` (test layer: 3 files / 28 errors held, unchanged),
all exit 0.
- Full suites on `ae27f00812` (this diff before the merge of `main`,
which touched none of these files): `@objectstack/core` 79 files / 2220
tests passed; `@objectstack/metadata-protocol` 205 files passed, 3
skipped / 3152 tests passed, 19 skipped. `@objectstack/cli`'s unit tier:
only the tier-partition pin (`test/vitest-tiers-partition.test.ts`, 22
passed); this diff changes no CLI source.
- Gates: `node scripts/pm/dispatch-gates.mjs --commands` on `14e5287fa8`
derived 67 commands; all 67 ran, reconciled with `--ran` (each line
carrying its exit code): 0 NOT MEASURED, 66 exit 0, and one exit 1,
`node scripts/check-empty-changeset.mjs --base origin/main`, explained
in the next section. Four roster gates the derivation flags as sharing a
directory with these paths also ran green: `check-changeset-fixed`,
`check:authz-resolver`, `check:error-code-casing`,
`check:filter-alias-parity`.
- Lint, narrowed: the population is the 4 changed `.ts` files, none
ignored by `eslint.config.mjs`. `eslint --no-inline-config --format
json` read 4 files, 0 errors, 0 warnings. That config never enables
type-aware linting (no `parserOptions.project`), so this diff cannot
move the verdict of any file it does not touch. The repo-wide `pnpm
lint` is CI's.
## Reverse verification and ablation
- **Reverse verification**, with the fix committed:
`migration-journal.ts` restored to `1ac7308d7a` in the working tree
only, blob `df8d8d5009` checked equal to the base's, then core rebuilt
and `node scripts/ablation-dist-preflight.mjs @objectstack/core
planResumedRun --absent` passed. Red as predicted: core unit 4 failed /
25 passed (the four resume pins; the control and the positional-resume
pin stay green, as they should on both trees), the plan pin 1 failed / 7
passed, the door 2 failed / 7 passed (a and b; the control green).
Restored with `git checkout HEAD --` under an EXIT/INT/TERM trap, proven
by the blob (`361d75e7ee` == HEAD) and an empty `git diff HEAD`, then
rebuilt, with the preflight in default mode showing the marker back in 4
built files and a clean tree.
- **Ablation of the unwind guard**, which reverse verification cannot
isolate (the old runner refuses before reaching it): `node
scripts/ablation-replace.mjs` planted the naive positional fallback
(`rowsByChunk.get(c.index) ?? rowsByStep[...].slice(offset, offset +
length)`). The landing was shown by the anchor count 1 → 0 and the blob
change. The unwind pin went red: `expected 'compensated' to be
'failed'`, which is the run handing chunk 0 other rows and journalling a
clean unwind. Restored by the tool: blob == HEAD, `git diff HEAD` empty.
The core unit suite imports the runner by relative path, so no build leg
applies.
## The pending #21498 changeset: a correction to confirm
This PR changes `.changeset/21498-cli-compose-migration-recovery.md`,
which it did not add. That note's "Still refused" bullet said the runner
refuses these two kinds of run with `PLAN_CHANGED`. This change makes
that false, and both notes are still pending, so they would ship in one
release. The bullet now says these runs reach the runner too and points
to the `@objectstack/core` entry for #21528. `check-empty-changeset`
stays red on this by design: it is the gate's DELIBERATE CORRECTION
class, and its remedy is to say so here and get the correction
confirmed. Restoring the old bullet would publish a sentence this PR
makes false. **Please confirm the correction.**
The new changeset (`.changeset/21528-core-resume-started-over-plan.md`)
is an `@objectstack/core` patch with `Clause-②: no`. No member is added
to a published contract. Resume now accepts the runs its list mode
already advertises as resumable, as ADR-0119 D2 item 5 declares.
## Acceptance notes
- **The list's `resumable` is plan presence only** (`resume.ts`:
`Boolean(plans?.get(r.planId))`). So a run whose plan genuinely changed,
or whose rows moved, is still listed `resumable: true` and then refused.
The triage ruling keeps the list's wording out of this card. I read this
from source; I did not measure the list for the control run.
- **A forward resume skips compensated chunks.** The forward loop skips
every chunk with a `chunk_done`, and a chunk that was committed and then
compensated has one. So resuming forward a run whose in-run unwind
failed partway would skip the chunks that unwind had undone. For
`recorded-by` (a shrinking `load()`), that run's row count matches
neither binding, so it is refused `PLAN_CHANGED`, as it was before. Only
a plan whose `load()` does not shrink would take the skip, and no such
plan is registered on this tree. I read this from source and did not
measure it. Carrier: none.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent ce53218 commit 10454b3
6 files changed
Lines changed: 647 additions & 79 deletions
File tree
- .changeset
- packages
- cli/src/commands/migrate
- core/src/utils
- metadata-protocol/src/migrations
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | | - | |
| 13 | + | |
| 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 | + | |
Lines changed: 202 additions & 44 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
38 | | - | |
39 | | - | |
40 | | - | |
41 | | - | |
42 | | - | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
43 | 53 | | |
44 | 54 | | |
45 | 55 | | |
| |||
97 | 107 | | |
98 | 108 | | |
99 | 109 | | |
100 | | - | |
101 | | - | |
102 | | - | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
103 | 115 | | |
104 | 116 | | |
105 | 117 | | |
| |||
126 | 138 | | |
127 | 139 | | |
128 | 140 | | |
129 | | - | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
130 | 144 | | |
131 | | - | |
132 | | - | |
133 | | - | |
134 | | - | |
135 | | - | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
136 | 155 | | |
137 | 156 | | |
138 | 157 | | |
| |||
232 | 251 | | |
233 | 252 | | |
234 | 253 | | |
235 | | - | |
236 | | - | |
237 | | - | |
238 | | - | |
239 | | - | |
240 | | - | |
241 | | - | |
242 | | - | |
243 | | - | |
244 | | - | |
245 | | - | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
246 | 260 | | |
247 | | - | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
248 | 272 | | |
249 | 273 | | |
250 | 274 | | |
251 | 275 | | |
252 | 276 | | |
253 | 277 | | |
254 | | - | |
| 278 | + | |
255 | 279 | | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
256 | 283 | | |
257 | 284 | | |
258 | 285 | | |
259 | 286 | | |
260 | | - | |
| 287 | + | |
261 | 288 | | |
262 | 289 | | |
263 | | - | |
| 290 | + | |
264 | 291 | | |
265 | 292 | | |
266 | | - | |
| 293 | + | |
| 294 | + | |
267 | 295 | | |
268 | | - | |
269 | | - | |
270 | | - | |
271 | | - | |
272 | | - | |
273 | | - | |
274 | | - | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
275 | 302 | | |
276 | | - | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
277 | 306 | | |
278 | 307 | | |
279 | 308 | | |
280 | 309 | | |
281 | 310 | | |
282 | | - | |
283 | | - | |
284 | | - | |
285 | | - | |
286 | | - | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
287 | 315 | | |
288 | 316 | | |
289 | 317 | | |
290 | 318 | | |
291 | 319 | | |
292 | 320 | | |
293 | 321 | | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
294 | 378 | | |
295 | 379 | | |
296 | 380 | | |
297 | 381 | | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
298 | 402 | | |
299 | 403 | | |
300 | 404 | | |
| |||
350 | 454 | | |
351 | 455 | | |
352 | 456 | | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
0 commit comments