Files
stack/agents/rocko/work/queue-56/r1/BUILD.md
T

8.8 KiB
Raw Blame History

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. resolveInside runs realpathSync on the data root and the target, then applies a path.relative check:
    • .., ../… 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 by readRunRecord, active.json and 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.json is strict: exactly the version 1 shape release.sh writes. 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 }, and last keeps the newest entries.
  • Errors. Every refusal is a RunsError with exit code 2 or 4. mosaic-task.mjs maps it to fail(exitCode, message) through readRuns.
  • 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.log and 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: list shows unknown; show refuses, exit 4.
  • result.json linked out: list shows unknown; show prints (missing or unreadable).
  • runs/ linked out: list and show refuse, exit 4.
  • result.json is 5: base list crashed (exit 1); base show printed undefined fields. The candidate gives unknown and (missing or unreadable).
  • task.json is []: base printed the snapshot line; the candidate omits it.
  • run is a regular file: base show crashed in readdirSync; the candidate gives run not found, exit 4.
  • link loop: base said run not found; the candidate refuses with ELOOP, exit 4.
  • runs/ unreadable: base list printed 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.readdirSync to 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.relative between 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 isRunId pre-check in show. readRunRecord throws the same message with exit 4, and readRuns maps it to the same fail. 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