Candidate build.patch onae428a54, manifest e63e4c4f. Gate onf2f2861b: 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]>
201 lines
9.3 KiB
Markdown
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`.
|