The task field-writer table as data: build.patch (12 files on 4f8ac1e7),
candidate manifest, gate summary (all green), mutants 33/33 killed.
The candidate is not committed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
162 lines
8.4 KiB
Markdown
162 lines
8.4 KiB
Markdown
# Row 61, the task field-writer table as data, round 1 (Darkwing)
|
|
|
|
Gitea #1550, Vikunja #208. Brief:
|
|
`docs/plans/2026-10-10_design-implementation.md` § "Task field-writer
|
|
table as data". Lead decision 113 settles the code and prose
|
|
disagreements I raised, and queue rev 672 adds the field table section of
|
|
`docs/plans/2026-10-04_slice-1.md` to the files owned. Base `4f8ac1e7`;
|
|
nothing from there to the packet commit touches the candidate's paths.
|
|
|
|
The candidate is `build.patch`, 12 files, applied with `git apply --index`
|
|
on `4f8ac1e7`. `candidate-manifest.sha256` lists the 12 files after the
|
|
apply. Nothing here is committed to `refactor`.
|
|
|
|
## What ships
|
|
|
|
**The table, `packages/tasks/fields.json`.** It has two layers, as
|
|
decision 113 rules.
|
|
|
|
- `verbs` maps each of the 8 broker task verbs to the writer `verbs.mjs`
|
|
checks first: `pm` (the pm definition, `field-writer` otherwise),
|
|
`assigned` (a role whose bot is an assignee, `not-assigned` otherwise)
|
|
or `any` (no writer check). Only `task.scope.change` is `any`, and only
|
|
`task.update.assigned` is `assigned`.
|
|
- `fields` lists the 11 fields in the slice 1 table. Each field names the
|
|
verbs that write it and the Vikunja calls each verb makes for it, with
|
|
the same placeholders as the prose.
|
|
|
|
Whether a write also needs a resolved decision isn't in the file. It comes
|
|
from the role's authority in `roles/*.json`, which the broker already
|
|
reads, so a second copy could only drift.
|
|
|
|
**The schema is code.** `packages/tasks/src/fields.mjs` has
|
|
`checkFields`, which refuses any other shape: unknown or missing keys, a
|
|
writer outside the three, a verb set that isn't exactly the broker's
|
|
`TASK_VERBS`, a field twice, a verb twice in one field, a call that isn't
|
|
a Vikunja write, a call twice, and a verb that writes no field. Labels are
|
|
capped at 80 characters, the source and notes at 256. `loadFields()`
|
|
reads the committed file, checks it and freezes it. Its errors name
|
|
`fields.json`, never a path. The repository has no JSON Schema validator,
|
|
and `packages/business` checks its files the same way, so I followed that
|
|
instead of adding a dependency.
|
|
|
|
**The test, `packages/tasks/tests/fields.test.mjs`.** It reads both
|
|
expectations rather than writing them out. The writer layer comes from
|
|
the table, and authority from `roles/*.json` through `packages/business`
|
|
`loadRole`, so the researcher's version 1 file grants nothing. Then it
|
|
drives every task verb as pm, coder, reviewer and researcher against the
|
|
fake Vikunja, with the cto as the technical arbiter:
|
|
|
|
- a role the table's writer excludes gets `field-writer` or
|
|
`not-assigned` and sends no write;
|
|
- a role without the verb within-role gets `decision-required` and sends
|
|
no write; the test raises a decision, checks it routes to the technical
|
|
arbiter when the role lists the verb cross-role and to the human
|
|
otherwise, resolves it and retries;
|
|
- every write that goes out is mapped back to the fields it carries
|
|
(a `PATCH` by its body keys, a bucket move into `done` as `done`), and
|
|
at the end each verb's writes must equal the table's calls for that
|
|
verb.
|
|
|
|
A change to the table, a role file or `verbs.mjs` that leaves them
|
|
disagreeing fails this file. Two smaller tests check that the committed
|
|
table loads frozen with exactly the broker's verbs, and the schema cases.
|
|
|
|
**The inspector.** The task page (`#/tasks/<ref>`) has a "Who may write"
|
|
section before the trail, with one row per field: the label, its note,
|
|
and each verb with its writer in words. Its intro says the decision layer
|
|
comes from the role's authority on Business, and that edits made in
|
|
Vikunja aren't checked against this table. A table that fails to load
|
|
shows the read error in that section only; the rest of the page renders.
|
|
|
|
To get the table to the browser, `serve.mjs` has `GET /api/task-fields`,
|
|
and `reads.mjs` has `fieldTable(file)`, named so it doesn't read as the
|
|
`taskFields` digest in `packages/tasks`. The table is repository data, so
|
|
the route answers without `--business` or a system config and carries no
|
|
`at`. A table that fails its check is 503 `read-failed` with the loader's
|
|
message and no path; other methods are 405. `startServer` takes a
|
|
`fieldsFile` option for tests only. I count `serve.mjs` and `reads.mjs`
|
|
as part of "the webui Tasks inspector": the page can't read the table
|
|
without them. If reviewers read the files owned more narrowly, that's the
|
|
place to say so.
|
|
|
|
**The prose.** Only the field table section of the slice 1 plan changes,
|
|
with the five corrections from decision 113 and a paragraph saying where
|
|
the data lives. One wording differs from what I proposed: the priority
|
|
row says "a change after create needs a resolved decision (cross-role)",
|
|
because `task.create` is within-role for pm, so priority at create needs
|
|
none. The READMEs for `packages/tasks` and `packages/webui` describe the
|
|
file, the route and the section.
|
|
|
|
Out of scope and unchanged: who may write a field, `roles/*.json`, and
|
|
close not checking the verdict (Vikunja #218).
|
|
|
|
## This lands on the live Console
|
|
|
|
Since the 10-12 cutover the Console runs from the canonical checkout
|
|
(decision 109). Landing this changes `serve.mjs`, `reads.mjs` and
|
|
`public/bus.js` there, so Sage notifies Mos before it lands, and the
|
|
status and healthz checks follow. I don't start or restart the Console.
|
|
|
|
## Overlap with row 81
|
|
|
|
Dewey's row 81 (#1575, in review) also changes `serve.mjs`, `public/bus.js`
|
|
and `packages/webui/README.md`. The two patches touch different hunks. On
|
|
`4918d410` I applied both with `git apply --3way --index`, in each order,
|
|
and both went in cleanly both times. Whichever row lands second still needs
|
|
a new round, because its manifest names the other's lines.
|
|
|
|
## Tests
|
|
|
|
In `~/darkwing-scratch/r61/gate`, a detached worktree at `4f8ac1e7` with
|
|
`build.patch` applied by `git apply --index` and `node_modules` linked.
|
|
Node v26.8.1, run in order from 18:39:31Z to 18:42:55Z on 2026-10-12
|
|
(`gate-summary.txt`). Every one exits 0:
|
|
|
|
| Suite | Result |
|
|
|---|---|
|
|
| `packages/tasks` (`node --test tests/*.test.mjs`) | 74 pass, 0 fail |
|
|
| `packages/webui` (`node --test tests/`) | 82 pass, 0 fail |
|
|
| `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` | 150 node tests, 27 suite checks |
|
|
| `test-release` | 14 passed |
|
|
| `test-task` | 26 passed, run with `DOCKER_HOST=unix:///nonexistent`, so its 4 live checks skip |
|
|
|
|
Afterwards I removed the `gate_default` Docker network the gate
|
|
left behind, which had no containers.
|
|
|
|
## Mutants
|
|
|
|
`mutants.sh WORKTREE [browser|light|NAME,...]` makes 33 mutants, one site
|
|
each, runs the named test file in that package and restores the file from
|
|
a backup. The edits use `queue-59-review/r1/mut.mjs`. The light 30 ran to
|
|
18:39:18Z (`mutants-light.txt`), the 3 browser ones to 18:38:38Z
|
|
(`mutants-browser.txt`). All 33 are killed, and `build.patch` still hashes
|
|
the same in the worktree afterwards.
|
|
|
|
| Group | Mutants | Killed by |
|
|
|---|---|---|
|
|
| Schema (`fields.mjs`) | 15: unknown key, optional key, any writer string, field twice, any field name, unbounded label, any note, verb twice, any call, call twice, unused verb, verbs a subset, any source, no freeze, wrong not-JSON code | the schema cases; `no-freeze` by the load test |
|
|
| Table (`fields.json`) | 8: scope to pm, priority to any, update to any, close to assigned, reassign without its delete, create without the due date, comments by close, done by update | the verb walk; `done-to-bucket` by the schema |
|
|
| Code (`verbs.mjs`) | 3: priority without a writer check, scope as pm, the assignee check gone | the verb walk |
|
|
| Route and read | 4: any method, the route ignoring `fieldsFile`, a partial table on error, any message | `reads.test.mjs` |
|
|
| Section (`bus.js`) | 3: a throwing section, the two writers swapped, no note | `bus-browser.test.mjs` |
|
|
|
|
`done-to-bucket` moves the `done` write from `task.close` to
|
|
`task.update.assigned`. The verb walk never runs on it: the load refuses
|
|
first with "verbs.task.close: writes no field", which fails the whole
|
|
file. That's the schema doing its job, but it means the walk's own check
|
|
of the `done` mapping is only shown by `comments-by-close`.
|
|
|
|
An earlier light run left 4 survivors: `required-key-optional`,
|
|
`label-unbounded`, `verbs-subset` and `source-any`. I added six schema
|
|
cases: a blank source, a source that isn't a string, a 257-character
|
|
source, a task verb missing along with the field it writes, an
|
|
81-character label and a 257-character note. All 4 died
|
|
(`mutants-light-r2.txt`).
|