docs: brief for two stack-release rows after the merged B+C window: row A (P19 391 activation ids, Dewey row 176 N1/N2), row B (P19 390 restore guard, after row A)

This commit is contained in:
2026-10-19 21:29:19 -05:00
parent c5ae4d7ac4
commit 236af52710
@@ -0,0 +1,127 @@
# Two stack-release.sh rows after the merged B+C window
Sage, 2026-10-20. Both rows come out of the merged B+C window and its rehearsals, and both change
`scripts/stack-release.sh`. Row A goes first. Row B starts from row A's landing, so neither one builds
on a stale base (the row 68 drift rule). Line numbers are at c5ae4d7ac.
Nothing here runs against the live stack. Every test uses the stand-in `systemctl`, units, stores and
data root that `packages/cli/tests/stack-release.test.mjs` already builds. Q173 makes the live stack a
test system, but these rows don't need it.
# Brief: activation ids and plan-mode refusals (row A)
Vikunja P19 391, plus N1 and N2 from Dewey's row 176 review. Owner Darkwing, reviewer Dewey, gate owner
Sage. Base: the refactor head when the row moves to briefed.
## Items
1. Unique activation ids within one second (P19 391). `new_activation()` (:662) names the activation
`$(date -u +%Y%m%dT%H%M%SZ)-<12-char copy prefix>`, and the post-attempt store path uses the same
second. An activate followed by a pm-on on the same copy in the same second makes the second run's
`mkdir -m 0700 "$RS"` (:373) fail, and capture refuses `capture-failed` (exit 3). It fails closed,
but it failed row 176 gate run 1 in T34 (stack-release.test.mjs:931, subtest :955), and a pinned clock
reproduces it every time.
The fix keeps the id sortable by time and keeps the current shape as its prefix, so the ids
already in `stack-rollback/`, `backups/stack/` and the pointer still parse. Either add milliseconds
or add a suffix on collision. A suffix is chosen while holding the lock, against both
`stack-rollback/` and `backups/stack/`. The POST path follows the same rule. Adopt's id (:680)
uses the same helper. `--restore ACTIVATION` accepts both the old and the new form, and
`docs/TOOLS.md` describes the new one.
Test: a `date` stand-in on the test PATH that prints one fixed second. Activate, then pm-on on
the same copy, both pass with distinct ids, rollback dirs and POST paths. A `--restore` of an
old-form id from the fixture still works.
2. Plan-mode refusals in `console_for_target()` (N1). Under `--plan`, `valid_tester_port ... || usage`
(:636) and `refuse tester-port-target` (:637) end the run at the first problem. Every other
precheck under `--plan` goes through `check_refuse` (:265), so the plan lists all refusals. With
PLAN=1, both lines report through `check_refuse` with a named code. A bad port uses a new code,
`tester-port-invalid`, and gives the same reason text usage gives today. Without `--plan`,
behavior doesn't change.
Test: `--plan --tester-port 80` and `--plan` against a copy whose Console lacks `--tester-port`
each list their refusal next to a second, independent refusal from the same run, and exit as a
plan with refusals does today. The same flags without `--plan` give today's output byte for byte.
3. Tester port length before zero stripping (N2). `tester_port_ok()` in `scripts/webui-service.sh`
(:123) strips leading zeros and only then checks for five digits, so
`render --tester-port 007333` passes and `serve` refuses the same value. Check the raw length
first (at most five digits), then strip and compare, which is what `valid_tester_port` (:62) in
stack-release.sh already does.
Test: in packages/webui/tests/service.test.mjs, `render --tester-port 007333` refuses with the
usage text and `07333` still renders.
## Acceptance
- Each item has the test above and a mutant that the gate kills (mos-0862: any gate test may kill it).
- Suites green on Sage's gate, run from a git worktree of the candidate.
- Dewey approves the manifest.
- Touched files: `scripts/stack-release.sh`, `scripts/webui-service.sh`, their test files,
`docs/TOOLS.md`, and the packet under `agents/darkwing/work/queue-<row>/`. Anything else gets named
in BUILD.md with the reason.
## Out of scope
The restore guard (row B). Any change to the pointer format, the backup layout or a refusal code
other than the new `tester-port-invalid`. Any run against `~/.mosaic-dev` or the live units.
# Brief: the restore guard counts every row a restore drops (row B)
Vikunja P19 390. Owner Darkwing, reviewer Filbert, gate owner Sage. Base: row A's landing. The row
waits on row A done.
## Why
`rollback --restore` refuses `post-backup-events` unless `--events N` names the number of events
written after the backup STAMP (:875 before the stop, :805 again in the run). The count in STORE_JS
(:289, `count` and `heldcount` at :298) reads only `events WHERE at > ?`. Some writers skip events.
`usage_records` (usage.mjs:92) and `instance_names` (broker.mjs:2237) do, per mos-0838 D3. Darkwing's
P3 rehearsal on 8e888014d ran a `name --tester` after the stamp. It got instance_names rowsAfter 1
and eventsAfter 0. The restore printed "0 event(s)" and dropped the row from the live store with no
refusal, and only the post-attempt copy kept it. The merged window stopped before any restore on a
nonzero snapshot rowsAfter as a stand-in for this fix.
## Items
1. Find every table a restore can lose rows from without an event. Read the writers in
`packages/bus/src/` at the base and list, in BUILD.md, each table that has rows written without an
event in the same transaction, with the writer's file and line. usage_records and instance_names
are on that list. The threads tables from rows 175 and 177 get checked too. The list goes in
stack-release.sh as one constant next to STORE_JS.
2. Count them. STORE_JS `count` and `heldcount` return the rows after STAMP for events and for each
listed table, keyed by table name, from the same read in the same open as today (immutable on a
stopped store, readOnly on a held one, with the row 166 is-active recheck kept). A listed table
the store doesn't have counts 0, because stores before 3g have no instance_names and stores
before 3h have no usage_records. A table that exists but can't be read is `restore-mismatch`, as
a failed count is now.
Test: a fixture store with one event, one usage_records row and one instance_names row after the
STAMP gets counts 1, 1 and 1 from both modes. A 3f fixture store with no usage_records table
counts 0 for it and doesn't refuse.
3. Refuse on any of them. The guard refuses `post-backup-events` unless every count is zero or
`--events N` equals the total. The message names each nonzero table, for example
`1 event(s), 1 usage_records row(s), 1 instance_names row(s) were written after the backup`. The
flag and the code keep their names, so no caller breaks. TOOLS.md and the usage text say that N
counts rows in all listed tables. The in-run recount (:805) compares the same total and refuses
`post-backup-events` the same way.
Test: instance_names rowsAfter 1 with events 0 refuses before any `systemctl stop` in the
stand-in log, the stack stays up, and `--events 1` then restores. The same holds for one
usage_records row. T14 (:565) passes unchanged for an events-only store.
4. A guard test for new tables. A test reads every table with an `at` column from the target
store's schema. It fails when a table is neither events, nor in the constant, nor in a short
reviewed list of tables whose rows always come with an event (deliveries, for example, named in
BUILD.md with the writer that shows it). A later schema row that adds such a table then has to
classify it.
## Acceptance
- Each item 2 to 4 has its test and a killed mutant: one per listed table that drops it from the
count, one that compares only events in the in-run recount, and one that adds a table with an
`at` column to the fixture schema without classifying it.
- Suites green on Sage's gate, run from a git worktree of the candidate.
- Filbert approves the manifest.
- Touched files: `scripts/stack-release.sh`, `packages/cli/tests/stack-release.test.mjs`,
`docs/TOOLS.md`, and the packet. Anything else gets named in BUILD.md with the reason.
## Out of scope
Restoring the dropped rows into the restored store, or merging the post-attempt copy back. A new
refusal code or flag name. Any change to the bus writers so they also write events. Any run against
`~/.mosaic-dev` or the live units.