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

187 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```sh
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
```