Files
stack/agents/darkwing/work/queue-58/BUILD.md
T
jason.woltjeandClaude Opus 5.5 7f92a1039b review(darkwing): row 58 round 1 packet, task priority and requirement on the bus read (#1547)
Candidate build.patch on ae428a54, manifest e63e4c4f. Gate on f2f2861b: bus 83,
tasks 71, webui 40, every scripts/test-*.sh exit 0. Mutants 16 of 17 killed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 17:23:08 -05:00

201 lines
9.3 KiB
Markdown

# Row 58 (#1547): bus read, task priority and requirement
Brief: `docs/plans/2026-10-10_design-implementation.md`, section "Bus read:
task priority and requirement" (blob `a360554c`). Owner Darkwing; reviewers
Dewey and Filbert.
The candidate is `build.patch` against the base in `base.txt`, 8 files in
`files.txt`. `candidate-manifest.sha256` holds the sha256 of each file after
the patch.
I built it on `72b16a5b`. Row 57 (#1546) then landed as `f78ba57d` and
changed four of these files: `broker.mjs`, both READMEs and the webui
`bus.test.mjs`. `git apply --3way` put the patch on `ae428a54` with no
conflict. Row 57 adds the decisions reads and this row changes the
`tasks` rows, so the hunks sit in different places; I read the merged
`broker.mjs` and both changes are intact. `build.patch`, the manifest and
the mutants are on `ae428a54`. The gate ran on `f2f2861b`, after row 62
landed; no candidate file changed between the two.
## The problem
The brief says priority isn't in `task_current.fields`. It is:
`taskFields()` in `packages/tasks/src/digest.mjs` puts `priority` in every
live snapshot, `checkTask()` refuses a task without an integer priority, and
the Tasks table already showed it from the fields. What the read model
lacked was a named priority field, and the requirement, which sat only in
the `task.created` body (`packages/tasks/src/verbs.mjs`). The list never
reads that body, since the list has no trail. So for priority this row
lifts an existing value to a top-level key; the requirement is new data in
the list. The brief asks for both fields in the task read model, a
projection rebuild test, and the two columns in the Tasks view.
## What changed
### The bus `tasks` rows
`Broker.#taskRows()` in `packages/bus/src/broker.mjs` builds the rows the
`tasks` verb returns. Each is `task_current`'s row as before, with two keys
added:
- `priority` is the current row's `fields.priority`. It is null for a
tombstone, whose fields are only `gone` (and `project` on a move), and for
a read with no priority. Vikunja's 0 stays 0.
- `requirement` is the requirement in the first `task.created` for the same
Vikunja task id in the business. A move to another project changes the
project in the ref but not the task id, so the moved task keeps the
requirement it was created with. It is null for a task the stack didn't
create.
Both are computed from append-only rows at each read; nothing new is stored.
I chose that over a schema change. `store.mjs` checks the DDL against
`SCHEMA_SHA256` and an open bus against the reference digest, so a new
column or a changed `task_current` view needs a schema version and a
migration of every existing `bus.sqlite`. A read-time projection needs
neither, and a reopened bus gives the same rows by construction.
`taskView()`, the trusted S3 read, is unchanged and still returns
`task_current`'s rows only. Its comment now names `#taskRows` as the verb's
read.
The requirement read is one query per `tasks` call over the business's
`task.created` events. It isn't indexed by kind; a business with many
thousands of created tasks would make each Tasks refresh scan them. That is
fine at slice 1 sizes, and a later row can add an index if a real business
needs one.
### The Tasks view
`packages/webui/src/public/bus.js`:
- `task()` takes `requirement` from the bus row first, then from the trail's
`task.created`, so the list, which reads no trail, now has it. Priority is
the row's, then the last snapshot that read the task (a tombstone's page),
then null. Before, a missing priority became 0 and showed "unset". A
poller read always has one, so "none" shows only for a row written without
it, like the browser test's #9.
- The Tasks table gains a Requirement column after Task. Requirement and
Priority show a muted "none" when the row has none, in the list and on the
task page.
- `bus.css`: the tasks table's minimum width goes from 900px to 1020px for
the extra column. The browser test's overflow checks still pass.
For a live row the bus `priority` and the fields' `priority` are the same
value, so preferring the row's changes nothing visible today. I kept it so
the view reads the field the brief puts in the read model; the mutant that
drops it survives for that reason (below).
### Docs
`packages/bus/README.md` describes the two keys under "Shared reads and
adapter boundary". `packages/webui/README.md` adds one bullet on the
columns to the S5 section.
## Scope
The brief owns `packages/bus/`, `packages/tasks/`, the webui Tasks endpoint
and tests. The webui `bus.js`, `bus.css` and README edits are the Tasks view
the brief asks for. Nothing in `packages/tasks/` changed: the poller already
writes `priority` in each snapshot's fields, and `task.create` writes
`requirement` in `task.created`. Field writers are row 61.
## Tests
New `packages/bus/tests/task-read.test.mjs`, 3 tests. The seed has six demo
tasks: created by the stack, changed outside it since, never seen by the
stack, created then gone, created then moved to another project, and a
second `task.created` for the moved task's new ref. A second business reuses
one task id with its own requirement.
1. "tasks rows carry priority and requirement for every session that reads
tasks": the human, a reader and a role session get the same rows. A later
poll's priority wins over the stack's write, 0 stays 0, tombstones have
none, the moved task keeps its first requirement, and the other business
sees only its own. Each row minus the two keys equals `taskView(...,
'current')`'s row, and `taskView` has neither key.
2. "the read model rebuilds from the ledger": the rows after a close and
reopen equal the rows before. Each row's priority equals its current
snapshot's in `trail`, and its requirement equals the first `task.created`
for its task id in `events`. A read writes nothing.
3. "an empty business and one with snapshots but no task.created read
cleanly".
All three fail on the base broker (`out/task-read-on-base.log`).
`packages/webui/tests/bus.test.mjs`: the endpoint test asserts the row's
`priority` and `requirement` reach `/api/bus/tasks` as null, not missing.
`packages/webui/tests/bus-browser.test.mjs`:
- The done task (#9) is now written with no priority.
- The Tasks list check reads each table's header and each row's Requirement
and Priority cells: #8 `none`/`high`, #7 `REQ-SLICE-1`/`high`, #9
`none`/`none`. It replaces the `/high/` match.
- #7's page shows `Priorityhigh`. The moved task's page (#10, a tombstone)
shows `Priorityhigh` from its last snapshot.
- #9's page shows `Requirementnone` and `Prioritynone`. This visit comes
after the "never read" failure check, which needs #9's trail unread; an
earlier visit made that check fail.
## Mutants
`out/mutants.sh` edits one source file per mutant, runs the one test file
meant to fail it, and restores the file from the build worktree's index,
which holds the candidate. Output: `out/mutants.txt`, from the run on `ae428a54` at 22:22 UTC on
2026-10-11. 16 of 17 fail, as they did on `72b16a5b`; each webui kill
failed on the intended assertion.
| Mutant | Fails |
|---|---|
| `tasks-verb-unchanged`: the verb returns `task_current` as before | task-read 1, 2, 3 |
| `no-priority`: priority always null | task-read 1, 2 |
| `zero-priority-null`: `\|\| null`, so 0 becomes null | task-read 1, 2 |
| `tombstone-priority-zero`: `?? 0` | task-read 1, 2, 3 |
| `requirement-by-ref`: keyed by the whole ref | task-read 1, 2 |
| `requirement-last-wins`: the last `task.created` wins | task-read 1, 2 |
| `requirement-any-business`: events from every business | task-read 1 |
| `requirement-undefined`: no `?? null` | task-read 1, 2, 3 |
| `task-view-adds-keys`: `taskView` grows the keys too | task-read 1 |
| `endpoint-drops-keys`: the rows lose both keys | webui bus endpoint test |
| `list-requirement-from-trail`: requirement only from the trail | browser |
| `no-requirement-column` | browser |
| `priority-default-zero`: `?? 0` in the view | browser |
| `tombstone-no-fallback`: no snapshot fallback | browser |
| `detail-priority-raw`: the task page prints the raw value | browser |
| `list-priority-raw`: the list prints the raw value | browser |
| `ignore-row-priority`: the view reads only the fields' priority | survives |
`ignore-row-priority` is equivalent today, as said above.
## Gate
`out/gate/gate.sh` ran on 2026-10-11 from 21:57 to 22:02 UTC. Its worktree
was at `f2f2861b` with `build.patch` applied (`out/gate/apply.txt`) and
`node_modules` linked to the main checkout's. It ran each suite in turn and
wrote each log to `out/gate/`. The brief names bus, tasks and webui plus
every `scripts/test-*.sh`.
| Suite | Result |
|---|---|
| packages/bus | 83 pass, 0 fail |
| packages/tasks | 71 pass, 0 fail |
| packages/webui | 40 pass, 0 fail, the browser tests included |
| test-auth | 15 passed |
| test-conductor | 17 passed |
| test-config | 24 passed |
| test-discord | 66 passed |
| test-extension-package | 18 passed |
| test-foundation | 44 passed |
| test-queue | 27 passed |
| test-release | 14 passed |
| test-task | 26 passed |
`test-task` ran with `DOCKER_HOST` set to a missing socket and skipped its
live recall pair. `test-queue` skipped its canonical-root check, since the
gate worktree isn't the queue's root. Sage's gate rerun covers both.
After the gate a `gate_default` Docker network was left with no
containers, and I removed it.
Candidate manifest sha256:
`e63e4c4f756229a9e5bba276c908e272fae0a71ae4f0a1c47e7e47b630bc63e6`.