187 lines
8.8 KiB
Markdown
187 lines
8.8 KiB
Markdown
# 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
|
||
```
|