8.8 KiB
Row 56 (#1545): runs and releases reader module, candidate packet, round 1
Author: Rocko. Reviewers: Darkwing and Filbert. Brief:
docs/plans/2026-10-10_design-implementation.md, section "Runs and releases
reader module". Base: dc96f87b (base.txt). build.patch also applies
cleanly to 923957e2 (origin/refactor at packet time). No file in this
candidate changed between the two (out/apply-*.txt). The packet is
uncommitted. I made no commits, pushes or Gitea calls, and read no token or
private binding.
Files (files.txt, 15)
| File | Change |
|---|---|
packages/runs/package.json |
new: @mosaic/runs, private, no dependencies |
packages/runs/src/errors.mjs |
new: RunsError(message, exitCode = 4) |
packages/runs/src/paths.mjs |
new: resolveInside containment, readJsonObject |
packages/runs/src/runs.mjs |
new: isRunId, listRunIds, listRunRecords, readRunDocument, readRunRecord |
packages/runs/src/state.mjs |
new: readActivePointer, readActivationLog |
packages/runs/src/index.mjs |
new: re-exports |
packages/runs/README.md |
new: what it reads, the limits, the deliberate deltas |
packages/runs/tests/*.mjs |
new: 37 tests (runs 22, state 10, task-cli 5) plus helpers |
scripts/mosaic-task.mjs |
list and show read through the module, +18/−27 |
agents/rocko/work/queue-56/{seed,byte-check,delta-check}.sh |
the byte-identity and delta checks |
build.patch is git diff --cached --binary dc96f87b over those files:
+1021/−27. candidate-manifest.sha256 hashes the 15 files.
Q14 freeze: nothing under packages/bus, packages/tasks, packages/cli,
scripts/bus-service.sh or scripts/mosaic changes. Apart from the import,
scripts/mosaic-task.mjs is the only file under scripts/ that changes. The
module covers the runs view's needs, but the view itself, release.sh and
pruning are untouched.
Design
- Containment.
resolveInsiderunsrealpathSyncon the data root and the target, then applies apath.relativecheck:..,../…or an absolute relative path counts as outside;- the configured data root is trusted even when it is itself a link;
- links that stay inside it are followed.
- Outside paths.
runs/,state/, a run directory read byreadRunRecord,active.jsonand the activation log refuse when they resolve outside.- A run document that resolves outside reads as
null, the way an unreadable one always has.
- Missing versus broken. ENOENT and ENOTDIR read as missing. Anything else (ELOOP, EACCES) refuses with the error code instead of reading as empty.
- Documents. A run document is a JSON object or
null. Run records are not validated further, because older records carry older shapes. - Release pointer.
active.jsonis strict: exactly the version 1 shaperelease.shwrites. Bad data exits 2, and a read error exits 4. - Activation log. The log is lenient per line, like
release.sh rollback. It returns{ entries, malformed }, andlastkeeps the newest entries. - Errors. Every refusal is a
RunsErrorwith exit code 2 or 4.mosaic-task.mjsmaps it tofail(exitCode, message)throughreadRuns. - Read-only. No function writes. The "readers write nothing" test and byte-check's before/after snapshot of the data root both check this.
Gate (out/gate/, sequential, scratch worktree of the candidate)
| Suite | Result |
|---|---|
| packages/runs tests | 34/34 at gate time; 37/37 after the mutant round (out/runs-tests-final.log) |
| test-auth | 15 passed, 0 failed |
| test-conductor | 17 passed, 0 failed |
| test-config | 24 passed, 0 failed |
| test-discord | first run 57 passed, 1 failed; rerun 66 passed, 0 failed (below) |
| test-extension-package | 18 passed, 0 failed |
| test-foundation | 44 passed, 0 failed |
| test-queue | 27 passed, 0 failed |
| test-release | 14 passed, 0 failed |
| test-task | 98 passed, 0 failed |
test-discord's first-run failure was environmental. The failing case was
"pi binary present at node_modules/.bin/pi for the extension checks". The
scratch worktree has no node_modules, and the base worktree lacks it too.
I symlinked the checkout's node_modules into the scratch tree and reran
the suite: 66 passed, 0 failed. The extra 8 cases are the extension checks
that the missing binary had skipped. Then I removed the symlink
(out/gate/test-discord-r2.log). Nothing in this candidate touches
packages/discord. Sage's rerun in a tree that has node_modules should see
66.
After the gate, the only changes were tests, the README table row and the
check scripts. No src/ or scripts/mosaic-task.mjs change came after it.
Byte identity (out/byte-check.txt)
byte-check.sh BASE CAND WORK seeds a data root with seed.sh. The seed
holds:
- runs a–f;
- a run that is a file;
- an internal link and a dangling link;
r-zz.weird_name-1;- a non-
r-name,.pruned.logand a stray file.
It runs 16 cases from both trees: list on seeded, empty and absent roots,
and show on each run, a missing run, ../escape, no argument and an
absent root. It diffs stdout, stderr and exit codes.
byte-check: 16 cases, stdout, stderr and exit identical
byte-check: data root unchanged
Correction to my own harness. My first rerun in this round passed a
relative WORK_DIR. Every redirect inside the cd subshell failed, both trees
recorded exit 1, and the diff still reported "identical": a false pass.
Earlier runs used absolute paths and were not affected. Both scripts now
make WORK_DIR absolute before doing anything else. byte-check.sh also
exits 4 if any case leaves no stdout or stderr file. The output above
comes from the fixed script.
Deliberate deltas (out/delta-check.txt)
These cases fall outside the byte-identity domain: a link out of the data
root, a document that is JSON but not an object, a run that is a file, a
link loop, and an unreadable directory. Each one changes on purpose. The
README table lists them, and delta-check.sh prints base against candidate:
- run directory linked out:
listshowsunknown;showrefuses, exit 4. result.jsonlinked out:listshowsunknown;showprints(missing or unreadable).runs/linked out:listandshowrefuse, exit 4.result.jsonis5: baselistcrashed (exit 1); baseshowprintedundefinedfields. The candidate givesunknownand(missing or unreadable).task.jsonis[]: base printed the snapshot line; the candidate omits it.- run is a regular file: base
showcrashed inreaddirSync; the candidate givesrun not found, exit 4. - link loop: base said
run not found; the candidate refuses with ELOOP, exit 4. runs/unreadable: baselistprinted nothing, exit 0; the candidate refuses with EACCES, exit 4.
Mutants (out/mutants.sh, out/mutants.txt)
I ran 32 single-line mutants of src/ and scripts/mosaic-task.mjs
against the packages/runs tests.
First round: 26 killed, 4 survived and 2 didn't apply. Three of the survivors were real gaps, and I added a test for each:
- M02 dropped the exact
..check. A link to the data root's parent got through. New test: "a link to the data root's parent is outside it". - M09 dropped the sort. Node returned this directory already sorted,
so no fixture could catch it. New test: it mocks
fs.readdirSyncto return the names reversed. - M15 read every readdir error on a run directory as missing. New test: "an unreadable run directory refuses instead of reading as missing".
Final round: 30 killed, 2 survived. Both survivors are equivalent:
- M04 dropped
!path.isAbsolute(relative). On POSIX,path.relativebetween two absolute paths is never absolute, so the branch is unreachable on Linux. I kept it as a guard for other platforms. - M31 dropped the CLI's
isRunIdpre-check inshow.readRunRecordthrows the same message with exit 4, andreadRunsmaps it to the samefail. The pre-check stays so that an invalid id refuses before the config loads, as it did before.
Follow-up (not fixed, out of scope)
This defect predates the candidate and is unchanged: list crashes with
a padEnd TypeError when result.json is an object without a string
status or taskId. Base and candidate behave the same here, so
byte-identity holds. A fix would change list output and belongs in its
own row.
Reproduce
git worktree add --detach /tmp/r56-base dc96f87b
git worktree add --detach /tmp/r56-cand dc96f87b
(cd /tmp/r56-cand && git apply --index <packet>/build.patch && sha256sum -c <packet>/candidate-manifest.sha256)
env -u NODE_TEST_CONTEXT node --test '/tmp/r56-cand/packages/runs/tests/*.test.mjs'
bash /tmp/r56-cand/agents/rocko/work/queue-56/byte-check.sh /tmp/r56-base /tmp/r56-cand /tmp/r56-bc
bash /tmp/r56-cand/agents/rocko/work/queue-56/delta-check.sh /tmp/r56-base /tmp/r56-cand /tmp/r56-dc