Files
jason.woltje 158205c5cc lead: commit darkwing's untracked evidence (348 files: queue-137 queue-141 queue-150 queue-162 queue-165 queue-166 queue-167 queue-169 queue-179 queue-180 queue-181 queue-182 queue-185 queue-186 queue-187 queue-188 queue-190 queue-93)
Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
2026-11-02 15:05:14 -06:00

178 lines
9.5 KiB
Markdown

# Row 185: every activate backs up the stopped store
Brief: `docs/plans/2026-10-20_activate-always-backs-up.md` (blob 2877ca401c2f, 52ce57dfe). Gitea #1683, P19
task 403. Owner Darkwing, reviewer Filbert, gate owner Sage. It is the precondition for mos-0928 Part A
(`agents/sage/work/window-plan/repin-rows-177-185.md`).
## Files
| File | What |
|---|---|
| `build.patch` | the change, `git diff 52ce57dfe` over the paths below |
| `candidate-manifest.sha256` | one line per changed path |
- Base: 52ce57dfe. Line numbers are at the base unless marked as the candidate's. `scripts/stack-release.sh`
has not changed since 9f5153deb, so the brief's numbers match.
- Changed: `scripts/stack-release.sh`, `packages/cli/tests/stack-release.test.mjs` and `docs/TOOLS.md`. Nothing
outside the brief's list.
## Design
### The run (change 1)
The migrate branch of `run_steps` (:835-842) now reads:
```bash
if [ "$STORE_SCHEMA" != "$T_SCHEMA" ]; then
in_chain "$STORE_SCHEMA" || rollback_in_run store-newer
[ "$MIGRATE" = 1 ] || rollback_in_run migration-needs-flag
fi
backup_store || rollback_in_run backup
if [ "$STORE_SCHEMA" != "$T_SCHEMA" ]; then
TAKEN="$ACT"
node .../migrate-schema.mjs ... || rollback_in_run migrate-failed
say "migrated the store to $T_SCHEMA"
fi
```
It sits inside `if [ -e "$STORE" ]`, after the `store_unclean` and schema reads (:831-833), so:
- a store with a `-wal` or `-shm` still rolls back `store-unclean` with no backup;
- a missing store takes no backup;
- a failed backup rolls back with `backup` as before. TAKEN is still empty then, so nothing is restored.
`store-newer` and `migration-needs-flag` still roll back before the backup. Those runs never touch the store,
so a backup there would only add a directory, and T6 and T9 keep asserting that no `backups/stack` appears. I
chose this over "backup first, then the checks" because it keeps the backup tied to a run that goes on to
install the target. The cost is one more `if` on the schema test.
### TAKEN and BACKUP (change 2)
Before this row `backup_store` set both (:367), because the only backup was a migration's. They now mean two
things:
- `BACKUP` is the backup the run names. `backup_store` sets it on every backup, so `log_line` (:52) writes it
into every line after the backup, and `from_log backup` (:146) finds it on the ok line that made the copy
live. `rollback_failed` (:571) names it too.
- `TAKEN` is the backup `rollback_in_run` (:543) restores. Only the migrate branch sets it, right before the
migration. After a gate failure on a same-schema activate the store is the one the run started from, plus
whatever the target's bus wrote while it ran. Restoring would throw those writes away for no reason, and
every restore moves the live store to a post-attempt directory. So that rollback puts back the units and
the wrapper and leaves the store.
The two comments at `backup_store` and `rollback_in_run` say this.
### `--plan` (change 3)
`plan` prints one line after the migration or restore block, for activate only:
- with a store file: `backup: the run backs up the stopped store to <dataRoot>/backups/stack/<activation>/
before any migration`, with the real data root;
- with no store file: `backup: none; there is no store file`.
The activation id is made by the run, so the plan prints `<activation>` as written. The migration lines are
unchanged. They already say "after a backup of the stopped store". The line is printed on the `S_CHANGED`
path too (the bus stopped during the read), since a store file is there. For `store-newer` and
`migration-needs-flag` the plan already says the run would roll back. I did not add a third form for those.
### Rollback and pm-on (change 4)
Unchanged. `do_rollback` already lets a same-schema rollback take `--restore` (:891-922): it checks the
backup's SCHEMA against the target's, its DATA_ROOT and STAMP, and the restore guard. A same-schema activate's
backup has SCHEMA equal to the store's, so test 3 needed no script change. pm-on does not call `run_steps
migrate`.
### docs/TOOLS.md (change 5)
"Pinned stack update" steps 5 to 7:
- step 5: every activate takes the backup before any migration, the log line names it, a missing store takes
none, and `store-newer` or a migration without `--migrate` rolls back first;
- step 6: the backup is taken on every activate and restored only when a migration ran; after a same-schema
activate the store stays as the failed run left it;
- step 7: a same-schema rollback can take `--restore` with the activate's backup.
Step 3 lists what the plan names and does not mention the new line. The brief assigns steps 5 to 7 only, so
I left step 3 for Sage.
## Existing tests that change
| Test | Why |
|---|---|
| T5 | asserted no `backups/stack` after the same-schema B to C1 activate. Now asserts the directory holds exactly the activation's backup (brief test 1 says to update it). |
| T11 | the `rollback-failed` message now ends `, backup <path>` after a same-schema activate. Also asserts the log's backup and that no post-attempt store appears. |
| T26 | exact plan output for B to C: the backup line after the `needs:` lines. The same-schema `includes` check has the line between `migration: none` and `commits:`. |
| T29 | exact plan output for the stopped-during-read case: the backup line after `target schema: 3h`. |
T6 and T9 are unchanged (above). T7, T8, T13, T14, T14b, T43 and T45 do migrating activates and still pass
as written: T8's migration case still restores. T23, T27, T36 and T44 read plan lines by offset from a line
the backup line comes after, or from the end. T43's `[c, c-1]` listing has only migrating activates, since
rollbacks take no backup.
The fixture gets one fault, `start-writes-row-<copy>`: the copy's bus start writes one event before the holder
opens the store. T48 uses it so the store after the failed gate differs from the backup, which makes a restore
visible by sha.
## New tests (candidate lines, after T46)
1. T47: a same-schema B to C1 activate leaves `backups/stack/<act>/` with SHA256SUMS, DATA_ROOT equal to the
data root, SCHEMA equal to the store's (3g) and a STAMP. The backup's sum is the store's sha before the run.
The run prints `backup: <dir>`, and the ok log line's `backup` is the activation.
2. T48: a gate failure (`healthz-C1`) on the same activate, with the target's start writing one row. It rolls
back `healthz`. The store sha after the rollback equals the store at the stop after the failed gate (the
fixture's stop snapshot), that sha differs from the backup's, and no post-attempt store appears. Units,
wrapper and pointer are back.
3. T49: a same-schema `rollback --restore <act>` with no rows after STAMP restores and goes live on B at 3g.
The store is the backup and the C1 store is kept as the post-attempt store. With one `instance_names` row
after STAMP it refuses `post-backup-events` with `0 event(s), 1 instance_names row(s)` and changes nothing,
and `--events 1` restores.
4. T50: `activate C1 --plan` prints exactly one `backup:` line, the store-file form, and writes nothing (units,
wrapper, pointer, store, the data tree and the log). A rollback plan has no backup line.
5. T51: with the bus stopped and the store files removed, the plan prints `backup: none; there is no store
file`, and the activate passes with no `backups` directory, no `backup:` line and a null `backup` in the log.
## Mutants
`run185.sh` runs `stack-release.test.mjs` whole, then each mutant against T5, T11 and T47 to T51
(`--test-name-pattern='^(T5|T11) |row 185'`). Each edits the worktree script after a checked backup and puts it
back by the backup's sha.
| Mutant | Expected killers |
|---|---|
| m1: the backup only when the schemas differ (today's code) | T5, T47, T49, T11 |
| m2: TAKEN set by every backup | T48, T11 |
| m3: BACKUP left empty on a same-schema activate | T47, T48, T11 |
## Runs
Slot from sage-1233, 2026-10-20T06:46:32.784Z to 06:51:52.217Z. TMPDIR `~/darkwing-scratch/r185/tmp`.
| Run | Result | Failing tests |
|---|---|---|
| `stack-release.test.mjs` | 125 tests, 125 pass, 0 fail | none |
| m1: the backup only when the schemas differ | killed | T5, T11, T47, T48, T49 (both subtests) |
| m2: TAKEN set by every backup | killed | T11, T48 |
| m3: BACKUP empty on a same-schema activate | killed | T11, T47, T48 |
T48 also fails under m1, at its log assertion: the line's `backup` is null. `stack-release.sh` was back at its
candidate sha after the mutants.
## Readers and hash pins
`git grep` over `packages/*/tests/**` and `scripts/test-*.sh` for `stack-release` and `TOOLS.md`, full output:
the only reader of `scripts/stack-release.sh` is `stack-release.test.mjs` itself (`SR`, :273, and the copied
script at :838). Nothing reads `docs/TOOLS.md`, and no test pins a sha256 of any of the three paths.
## Candidate
- Base 52ce57dfe. `52ce57dfe..origin/refactor` (b5d662cab) touches only
`agents/sage/work/window-plan/repin-rows-177-185.md`. `build.patch` applies cleanly to both
(`git apply --cached --check` on a scratch index).
- `build.patch`: sha256 8f2293de151c1c3b4d96ac6d494f7d9f67e849d8f51b804c6cdad799c4269ae6, patch-id (stable)
774cd23186d54eda08be028f7c39b5212cd1ed75, 3 files, 121 insertions, 16 deletions.
- `candidate-manifest.sha256`: sha256 bf87dbba645b5c4443670e4a81f44a99afedad6ab16ae0f125d6ab8de27bce3b.
| Path | sha256 |
|---|---|
| `scripts/stack-release.sh` | dccb4a10d5b9a6f680a5bdc06277abf39f7d438c0500cb44d5b1c45012de21bc |
| `packages/cli/tests/stack-release.test.mjs` | 94b398246cd87cec282e13147b244cdfa85ea5c98a7bf06a468e1b387692df08 |
| `docs/TOOLS.md` | a0ea0f560cd2683c2dd38bdeb88fd4bf339be771e0436e623ba554fcb34d6d54 |