# 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`.