sage: row 163 BUILD.md (design, rulings mos-0961 to mos-0964, outbound calls, deviations, tests, mutants)

This commit is contained in:
jason.woltje committed 2026-10-20 09:44:45 -05:00
1 parent 5805006993
commit 4be3f6aece
1 file changed
+119
+119
View File
@@ -0,0 +1,119 @@
# Row 163 (D2): `stack-release.sh rehearse` and `bus start --no-launch`
Gitea #1660. Brief: docs/plans/2026-10-18_stack-release.md, "Stack rehearsal on a scratch root
(D2)", blob b1500d96ad27. Reviewer: Filbert. Rulings: mos-0961, mos-0962, mos-0963.
Base: 4f02486d5 (origin/refactor). D2 lands after the Part B window (P19 385), because Part B's
window file names the stack-release.sh blob (mos-0958, mos-0959).
## What it does
`rehearse ID BUSINESS --backup ACTIVATION|PATH --port N [--timeout S]` builds
`<dataRoot>/rehearsals/<UTC second>-<copy 12>/` (0700) from a backup, runs a guard on the scratch
config with the target copy's code, runs the migration checks when the schema differs, starts the
target's `bus start BUSINESS --no-launch` and Console on the scratch root, gates them with D1's
checks minus the PM's, checks that no session started, stops both, and compares the backup and the
live config and business files with sums taken first. One PASS or FAIL line per check, then
REHEARSAL_PASS (0) or REHEARSAL_FAIL (1). Usage exits 4. docs/TOOLS.md has the operator text.
`bus start --no-launch` (brief item 4): `start()` refuses `launches-disabled`, so every launch path
spawns nothing; a session's launch request is refused and logged; `listen()` starts no thread
ticker; `recover()` signals and ends nothing and logs what it left; the Registry is read-only, so
sessions.json stays as an earlier host left it and `add()` refuses. The host-up line ends
`, launches disabled`. `--pm` with it is a usage error. No unit passes it.
## Rulings carried
- mos-0961: `data-root-live` refuses a scratch dataRoot that resolves to the live data root;
`sessions-live` refuses when a sessions.json in the scratch data root names a running pid. The run
never deletes the file or retries. recover() under --no-launch is covered by the launcher test
"bus start --no-launch leaves an earlier host's live session alone: no signal, no end,
sessions.json as it was".
- mos-0962: every tracker.* variable is stripped, business and role level; `business.diff` keeps the
change; `tracker-configured` fails if bootConfig still sets trackers; notify.json is asserted to be
exactly {"notifyVersion":1,"binding":null} at 0600.
- mos-0963: every role credential reference and @sync's is moved to its own 0600 file under
`<rehearsal>/placeholders/` holding `rehearsal-placeholder-token`; `credential-ref-live` fails for
an env reference or a file whose real directory is not the placeholders directory. Test R9 leaves
one role ref on a file outside placeholders and gets the FAIL; mutant K1 drops the check and R9
fails.
## Outbound calls
The rehearsal reaches nothing off the host:
- reads: the live config.json, the live business file, the pointer, the backup, the copies
directory, the target copy's roles/, git in the canonical checkout (`copy_ok`'s merge-base);
- writes: only `<dataRoot>/rehearsals/<stamp>/`;
- processes: the target's bus host (`--no-launch`) and Console (`serve --port N --business B`, no
`--send`), both with `env -i HOME PATH MOSAIC_CONFIG=<scratch config>`;
- network: 127.0.0.1:N for /healthz, and the host's sockets under the scratch data root.
No model call: launches are disabled and no session starts (`no-session`). No Vikunja: tracker.*
is stripped and `tracker-configured` checks bootConfig. No Discord: notify binding null. No Gitea:
every credential ref is a placeholder file, and the git socket serves only bound sessions, of
which there are none.
`projects.stack.root` stays at the live checkout, /mnt/storage/src/mosaic-stack. The business
file's projects are copied unchanged. Under --no-launch nothing makes a checkout from it, because
checkouts are made only in `start()`, which refuses.
## Deviations from the brief
1. Item 1 says to copy "the business file, roles and organization file from the live config
directory". Roles come from the target copy's `roles/`, as the live bus reads them (rolesDir is
`<REPO>/roles` unless MOSAIC_ROLES_DIR is set, and `env -i` leaves it unset). The live config
directory has no organization file; bootConfig's organization entry is the config dir itself,
which here is the scratch config dir.
2. The business file is rewritten, not copied (mos-0962, mos-0963), and `business.diff` records
the change.
3. The `tracker` key stays and `tracker.sync.credentials` becomes a placeholder: the business
schema requires both, so stripping the key (mos-0962's wording) makes the loader refuse.
bootConfig sets trackers only with tracker.baseUrl and a project's tracker.project, and both are
stripped; `tracker-configured` checks the result. Authority: mos-0964 (2026-10-20T14:35:30Z),
which accepts this and replaces the "@sync still present" clause of mos-0963 item 2:
business.mjs:197 calls checkTracker on every load, and checkTracker requires tracker and
tracker.sync (:57, :59) and checks sync.credentials (:63). @sync stays with its ref on a
placeholder, and `credential-ref-live` covers it with the role refs. Evidence: R10 (a tracker
still set fails `tracker-configured`), K2 (the check dropped, R10 fails), K10 (the tracker.*
strip dropped, R1 fails on `tracker-configured`), and K9 (the @sync swap dropped, R1 fails on `credential-ref-live`).
4. mosaic-config already refuses a scratch config that names the live data root lexically, because
the rehearsal directory sits inside the live data root and a dataRoot may not contain the config
dir. The guard's `data-root-live` catches the form that reaches the live root through a link
(R11).
5. Files owned: the brief lists stack-release.sh, its test, TOOLS.md and the packet. Item 4's flag
adds packages/cli/src/cli.mjs, packages/cli/src/launcher.mjs, packages/seat/src/session.mjs and
their tests (host.test.mjs, launcher.test.mjs, session.test.mjs).
6. The brief's "free loopback port" is an operator-given `--port`, refused when reserved (7332,
7333, the pointer's --port and --tester-port) or in use, never replaced by another port.
7. Brief item 3's "in the foreground": the host runs as a child of the script for the length of
the gate, then gets SIGTERM; it is not a unit and not detached.
## Tests
packages/cli/tests/stack-release.test.mjs, rehearse section (stand-in host, Console, migrate and
Store in four fixture copies; real scripts/mosaic, mosaic-config, cli config and the business
loader):
- R1 passes on the same schema: files, modes, placeholders, business.diff, the host's meta row in
the scratch store and not in the backup, args logs, steps.log equal to the output, live files
byte-identical.
- R2 migration path 3g to 3h; R3 migrate fails.
- R4 host fails, status fails, status names another pid, host launches, host keeps writer.lock.
- R5 SIGTERM ignored: SIGKILL and FAIL host-stopped. R6 healthz. R7 copy-match.
- R8 port in use, and 7332, 7333, the pointer's --port and --tester-port reserved.
- R9 credential-ref-live (mos-0963). R10 tracker-configured (mos-0962). R11 data-root-live through
a link and sessions-live on a running pid (mos-0961). R12 notify-off.
- R13 old copy, backup missing, backup sums, no pointer, usage.
packages/cli/tests/launcher.test.mjs: recover() under --no-launch leaves an earlier host's live
session alone; every launch is refused and logged; no thread tick (and the same host with
launches runs it). packages/cli/tests/host.test.mjs: the host-up line, no sessions.json, --pm
with --no-launch is usage, and no unit renders --no-launch. packages/seat/tests/session.test.mjs:
a read-only Registry never writes and refuses add().
## Mutants
33, one per behaviour, each killed (mutants.py, mutants-run.log and mutants-rerun.log beside this file). The first run left K11
(bus-status pid), K21 (scratch store linked to the backup), K27 (ticker under --no-launch) and
K31 (cli drops launches); each got a test and the rerun killed all four. K21 now dies on
FAIL backup-unchanged, the check it exists for.