162 lines
8.6 KiB
Markdown
162 lines
8.6 KiB
Markdown
# Row 63, task verbs across the tree (row B), build (Darkwing)
|
|
|
|
Issue #1556. Brief: `docs/plans/2026-10-11_multi-project-sync.md`, section
|
|
"Multi-project verbs: task verbs across the tree (row B)" (blob `9301de2e`).
|
|
Base `08941d8b`. No commit from the base to the packet's commit touches
|
|
the six paths.
|
|
|
|
`build.patch` is `git diff --cached --binary 08941d8b` of the six paths.
|
|
Apply it with `git apply --index` to a detached worktree of the base, and
|
|
`sha256sum -c candidate-manifest.sha256` then gives 6/6 OK.
|
|
|
|
| File | What changed |
|
|
|---|---|
|
|
| `packages/tasks/src/verbs.mjs` | verbs act in any installed project of the tree |
|
|
| `packages/tasks/tests/verbs.test.mjs` | six tests across a root, an installed child and a watch-only grandchild |
|
|
| `packages/tasks/tests/tree.test.mjs` | one assertion: the watch-only verb refusal code |
|
|
| `packages/tasks/README.md` | the new step, `project`, the new refusal |
|
|
| `packages/harness/src/tools.mjs` | `task_create` gets an optional integer `project` |
|
|
| `packages/harness/tests/tools.test.mjs` | asserts its type, that it isn't required, that only `task_create` has it |
|
|
|
|
## What the code does
|
|
|
|
- `installed(project)` is the one gate. A project the tree doesn't hold
|
|
refuses `task-project`, as a foreign ref always has. A project in the
|
|
tree with no view, a watch-only one, refuses with the new code
|
|
`project-not-installed`. With descendants off the tree holds the root
|
|
alone, so nothing changes for a business that hasn't opted in.
|
|
- `target()` returns `{project, view, id}` in place of a bare id, and
|
|
`read()`, `step()` and every `plan.check` take that. A bucket move, a
|
|
close and the board read all use the task's own project and view. No
|
|
`ctx.project` or `ctx.view` is left in `read()`, `task.update`,
|
|
`task.close` or `create()`'s writes; create's default project is the
|
|
one remaining use.
|
|
- `relation()` accepts a task in any installed project and keeps its
|
|
project. A ref's project is only the caller's claim, so `located()`
|
|
reads each related task once with the sync token before
|
|
`broker.authorize`. Missing gives the tracker's refusal
|
|
(`task-not-found`), a task in another project than its ref names gives
|
|
`task-moved`. This covers relations on `task.create` and both the add
|
|
and the remove lists of `task.schedule`. Without it, a relation to a
|
|
task the ref placed in the child but which sits in a watch-only project
|
|
would have been written.
|
|
- `task.create` takes an optional `project`, Mos's ruling as the brief
|
|
states it. Left out, it's the root. It must be a positive safe integer
|
|
(`invalid-request` otherwise, so `"1"`, `1.5`, `0` and `null` refuse
|
|
before any read) and an installed project of the tree
|
|
(`project-not-installed` for watch-only, unknown and outside alike).
|
|
|
|
## Choices to review
|
|
|
|
1. **One code for create's unknown project.** The brief asks for "a new
|
|
code the README names" for a watch-only or unknown project on create.
|
|
I used `project-not-installed` for both and for a project outside the
|
|
tree. A ref outside the tree keeps `task-project`, because a ref names
|
|
a task and that code already means "not this business's task". A
|
|
create's `project` names where to write, and the brief puts unknown
|
|
with watch-only.
|
|
2. **Relations are read before authority, which the brief doesn't ask
|
|
for.** The brief says `relation()` accepts any installed project's
|
|
task. Taking the ref's project on trust would let a caller write a
|
|
relation to a task in a watch-only or foreign project by naming an
|
|
installed one. One sync read per related task closes that. It costs a
|
|
read per relation (create caps relations at 50). The README's verb
|
|
steps gain a step 5 for it.
|
|
3. **`tree.test.mjs` is outside the brief's owned files.** One assertion
|
|
there expected `task-project` for a verb on a watch-only child's task,
|
|
which was row A's v0 behaviour. Row B changes that code by design, so
|
|
the assertion moves to `project-not-installed` and its comment says
|
|
where the installed cases are tested. Nothing else in the file
|
|
changed.
|
|
4. **The create decision still binds no target.** `task.create`
|
|
authorizes with `target: null`, as before, so a decision approving a
|
|
create doesn't name which project it approved. A decision raised for
|
|
the root can authorize a create into an installed child. The tree is
|
|
the business's own and installed projects are writable by design, so
|
|
I left it. Binding the project would change the decision record's
|
|
shape, which is the broker's and not this row's. It's a follow-up if a
|
|
reviewer wants it.
|
|
5. **`fake.mjs` is untouched.** `moveToProject` already existed from row
|
|
A, which was all the moved-task test needed.
|
|
|
|
## Tests
|
|
|
|
The brief asks for each verb across two installed projects, a watch-only
|
|
refusal and a relation between projects. The six new tests in
|
|
`verbs.test.mjs` run on a tree of root, installed child and watch-only
|
|
grandchild (the grandchild holds one task the UI made):
|
|
|
|
1. Every verb in the installed child, on the child's own board: create
|
|
into the child with a relation to a root task, create with the root
|
|
named explicitly, assign (and a reassign that refuses
|
|
`task-assigned`), `task.update` with a state on the child's view,
|
|
`task.scope.change` by the coder and `task.priority.change` by the pm
|
|
(each with a decision the cto resolves), reassign, schedule with a due
|
|
date and a relation removal, a root-to-child relation add, and close
|
|
into the child's done bucket.
|
|
2. `task.create` refuses the watch-only grandchild, a project outside
|
|
the tree and 999 with `project-not-installed`; `null`, `0`, `-1`,
|
|
`1.5`, `"1"` and `[id]` with `invalid-request`. No writes, no allowed
|
|
creates.
|
|
3. With descendants off, create takes only the root.
|
|
4. A verb on the watch-only task and a relation to it refuse
|
|
`project-not-installed`; `vikunja:999/1` still gives `task-project`.
|
|
5. Relations are read before authority: a ref claiming the child for a
|
|
root task gives `task-moved`, a missing child task gives
|
|
`task-not-found`, for schedule add, schedule remove and create, with
|
|
no writes; then a correct relation goes through.
|
|
6. A task moved to another project of the tree refuses its old ref with
|
|
`task-moved` and takes its new one.
|
|
|
|
## Gate
|
|
|
|
Row 63's gate (tasks, harness, every `scripts/test-*.sh`) plus row A's
|
|
business and bus, sequential in the candidate worktree, `TMPDIR` in my
|
|
scratch, `test-task.sh` with `DOCKER_HOST=unix:///nonexistent`. It ran
|
|
2026-10-12 19:39:19Z to 19:42:45Z, after Sage's row 81 gate and Rocko's
|
|
row 77 run had ended. Every suite exits 0 (`gate-summary.txt`).
|
|
|
|
| Suite | Result |
|
|
|---|---|
|
|
| tasks | 80 pass (74 at the base plus the six) |
|
|
| harness | 59 pass |
|
|
| business | 60 pass |
|
|
| bus | 95 pass |
|
|
| 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 (the live recall pair skips without Docker) |
|
|
|
|
## Mutants
|
|
|
|
`mutants.sh` with `agents/darkwing/work/queue-59-review/r1/mut.mjs`. Each
|
|
mutant edits one site (exactly one match), runs `verbs.test.mjs` and
|
|
`tree.test.mjs` (or the harness `tools.test.mjs`), and restores the file.
|
|
18 of 18 killed (`mutants.txt`, run 2026-10-12 19:20:11Z to 19:21:24Z).
|
|
|
|
| Mutant | What it undoes | Killed by |
|
|
|---|---|---|
|
|
| watch-only-writable | the watch-only refusal in `installed()` | assertion, 2 tests |
|
|
| create-root-only | create posts to the root whatever `project` says | assertion |
|
|
| create-watch-only-ok | `tree.has` in place of `tree.get` on create | assertion |
|
|
| create-project-any | the integer check on `project` | assertion |
|
|
| move-root-view | a state move looks up the root's bucket ids | `tracker-not-found` |
|
|
| move-path-root-view | the bucket move path uses the root's view | `tracker-not-found` |
|
|
| close-root-done | close moves to the root's done bucket | `tracker-not-found` |
|
|
| read-root-board | `read()` reads the root's board | `task-placement`, 2 tests |
|
|
| read-no-project | `read()` drops the project check | assertion (test 6) |
|
|
| relation-any-project | `relation()` drops `installed()` | assertion, 2 tests |
|
|
| handle-no-located | verbs skip the relation read | assertion |
|
|
| create-no-located | create skips the relation read | assertion |
|
|
| located-no-project | the relation read drops the project check | assertion |
|
|
| schedule-add-only | schedule reads only the add list | assertion |
|
|
| located-after-authorize | the relation read moves after authorize | assertion |
|
|
| tool-project-string | the tool's `project` becomes a string | assertion |
|
|
| tool-project-zero | the tool's minimum becomes 0 | assertion |
|
|
| tool-project-required | the tool requires `project` | assertion |
|