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

9.3 KiB

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.