# Row 186: the activate plan's backup line says what the run will do Brief: `docs/plans/2026-10-20_plan-backup-line.md` (blob 9610b31d0c19, f5c72a5c3). Gitea #1684, P19 task 404. These are Filbert's N1 and N2 on row 185 (#1683 comment 29404). Owner Darkwing, reviewer Filbert, gate owner Sage. ## Files | File | What | |---|---| | `build.patch` | the change, `git diff 1d7fd75e2` over the three paths below | | `candidate-manifest.sha256` | one line per changed path | - Base: 1d7fd75e2. None of the three paths changed between the brief's b724b7b0b and 1d7fd75e2, so the brief's line numbers hold. - Changed: `scripts/stack-release.sh`, `packages/cli/tests/stack-release.test.mjs` and `docs/TOOLS.md`. Nothing outside the brief's list. ## Design ### The plan (change 1) `plan` now sets a local `early` in the migration block, in the two branches where `run_steps` (:846-847) rolls back before `backup_store` (:849): - in chain without `--migrate`: `[ "$MIGRATE" = 1 ] || early=1`, next to the `needs: --migrate` line that already says the run would roll back with `migration-needs-flag`; - outside the chain: `early=1`, next to the `store-newer` line. The backup block then picks one of four: | Case | Line | |---|---| | rollback | none (unchanged) | | activate, no store file | `backup: none; there is no store file` (unchanged) | | activate, `early` set | `backup: none; the run rolls back before the backup` (new) | | activate, otherwise | `backup: the run backs up the stopped store to /backups/stack// before any migration` (unchanged) | The no-store test now comes before `early`. With no store file `plan_store` falls back to the pointer's schema, and the run skips the whole store block (:839), so "there is no store file" is the right reason even when the pointer's schema is outside the target's chain. Row 185's BUILD.md said I did not add a third form for these two cases, since the migration lines already said the run would roll back. Filbert's N1 is right that the plan then states two things that can't both happen. This row adds that form. ### The S_CHANGED reading (change 2) When the bus stops during the held-store read, `plan_store` sets `S_CHANGED` and the store schema prints as `unknown ... run --plan again`. The migration block prints nothing and leaves `early` empty, so the plan keeps today's backup line (T29 pins it). That is the safe reading. The plan can't tell which of three things the run will do with the stopped store: back it up (same schema, or in chain with `--migrate`), or roll back with `store-newer` or `migration-needs-flag`. If the backup line turns out wrong, the run rolled back before it touched the store, so the store is as it was and nothing needed a backup. If a "none" line turns out wrong, the run backed up and went on to install, perhaps migrate, and the operator holds a plan saying there is no backup to restore from. The first mistake costs nothing. The second hides the one directory a `--restore` needs. The store schema line already tells the operator to plan again before running. ### A reading outside the brief: the store schema from the pointer `plan_store` also falls back to the pointer's schema when the bus is stopped and the store is unclean (a `-wal` or `-shm` beside it) or unreadable. The run then rolls back with `store-unclean` or `schema-unreadable` (:840-841), both before the backup, but the plan judges `early` by the pointer's schema and can print the backup line. The brief names only `store-newer` and `migration-needs-flag`, and the plan's store schema line already says the store was not read and why, so I left this alone. It's a bounded follow-up if Filbert or Sage want it: a `backup: none; the run rolls back with store-unclean` form for a stopped, unclean store. ### What does not change (change 3) `run_steps`, `backup_store`, every refusal code, exit code and flag. The diff to `scripts/stack-release.sh` is inside `plan` only. ### docs/TOOLS.md (change 4) "Pinned stack update" step 3 now lists the backup among what `--plan` names, and says the `backup:` line gives the directory or why the run takes none: no store file, or a rollback with `store-newer` or `migration-needs-flag` first. ## Tests `packages/cli/tests/stack-release.test.mjs`: - `planNoBackup`, the new line, sits beside `planBackup`. - T26: the not-given case now expects `planNoBackup` where it expected `planBackup`. The `--migrate` case and the same-schema check are unchanged. - T52 (new), six subtests on a fresh stand-in each. Each plans, checks the plan's only `backup` line and its migration or needs line, checks nothing was written (`fp`, the data root tree, no starts or stops, no log line, no `backups`), then runs the same activate and checks `backups/stack` exists, and the log line names the activation as its backup, exactly when the plan printed the backup line: | Subtest | Store | Target | Flags | Plan | Run exit | |---|---|---|---|---|---| | same schema | 3g | C1 (3g) | none | backup line | 0 | | same schema with --migrate | 3g | C1 | `--migrate` | backup line | 0 | | in chain with --migrate | 3g | C (3h) | `--migrate` | backup line | 0 | | in chain without --migrate | 3g | C | none | none, rolls back | 1 | | outside the chain | 3z | C | none | none, rolls back | 1 | | outside the chain with --migrate | 3z | C | `--migrate` | none, rolls back | 1 | Brief tests: 1 is T50 (unchanged) and T52's same-schema cases. 2 is T26's `--migrate` case and T52. 3 is T26's not-given case and T52. 4 is T52's two outside-the-chain cases. 5 is T51 (unchanged). 6 is T50, T51 and T52's write checks; T26 runs only `--plan`. No test reads `docs/TOOLS.md`, and no test pins a hash of any of the three files (grep of `packages/*/tests`, `scripts/test-*.sh` and every non-doc file for the paths and for 64-hex pins). ## Results Run from a detached worktree of 1d7fd75e2 with the change applied, `TMPDIR` a scratch directory outside `/tmp`, stamps from `date -u`. Nothing touched `~/.mosaic-dev` or a live unit. Every case uses the test's stand-in `systemctl`, units, stores and data root. - `node --test packages/cli/tests/stack-release.test.mjs`, whole file: 132 tests, 132 pass, 0 fail (08:45:15Z to 08:49:31Z). - Base control: the base `scripts/stack-release.sh` under the new tests fails T26 and T52's three rollback subtests (in chain without `--migrate`, outside the chain, outside the chain with `--migrate`). So the new expectations are not met by today's plan. Mutants: each is one exact replacement in the candidate's `scripts/stack-release.sh`, run against the plan tests (`^(T15|T23|T26|T27|T29|T50|T51|T52) `, 18 tests), then put back and checked by sha256. | Mutant (brief) | Edit | Result | Failing tests | |---|---|---|---| | m1 the plan ignores `--migrate` when it picks the backup line | drop `[ "$MIGRATE" = 1 ] \|\| early=1` | killed, 3 fail | T26, T52 in chain without `--migrate` | | m2 the plan prints the backup line for a store outside the chain | drop `early=1` in the `store-newer` branch | killed, 3 fail | T52 outside the chain, both subtests | | m3 the plan prints the `none; the run rolls back` line for a same-schema activate | add `early=1` in the same-schema branch | killed, 5 fail | T26, T50, T52 same schema, both subtests | (The fail counts include the T52 parent with its failing subtests.) Candidate (`candidate-manifest.sha256`, sha256 449e91219333…): ``` 6afc6bda76c450550ff6e0119b25dfafc20af219f7c512bc7d696c9d98b882fa docs/TOOLS.md 1d1e6e2260f0ee34ee5e140156238fc952afc6540034c88dff61a09b349ec3b7 packages/cli/tests/stack-release.test.mjs 983037ee7bb970de82a0448761c675904910de9dc256bfb19438349014668879 scripts/stack-release.sh ``` `build.patch` (sha256 75efa836…) applies with `git apply --check` on 1d7fd75e2. ## For Sage's gate The cli suite and every `scripts/test-*.sh` from a git worktree of the candidate, per the brief. I ran only `stack-release.test.mjs`. It is the one test that reads any of the three files. ## Review Filbert approved round 1 on digest 449e91219333 (#1684 comment 29419, rev 2198, 08:59:42Z). Their run of stack-release.test.mjs passed 132/132. Their M7 (an `S_CHANGED` read counts as early) was killed by T29, which pins the safe reading above. Two non-blocking findings stay as follow-ups and are not in this candidate: - N1: their M6 (the `early` test placed ahead of the no-store test) survives. No T52 case has a missing store with a pointer schema outside the target's chain. The order in the candidate is right. - N2: the unclean-store reading above, as a bounded follow-up. Use the `store-unclean` form only when the plan saw the stopped store unclean, since a held read that failed may read fine once the run stops the bus. Both fit one small follow-up row: the N2 form plus a T52 case for each.