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

8.6 KiB

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 9610b31d0c, 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 <dataRoot>/backups/stack/<activation>/ 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.