179 lines
9.2 KiB
Markdown
179 lines
9.2 KiB
Markdown
# Row 57 (#1546): the bus reads decisions routed to roles and recent decisions, candidate packet
|
||
|
||
Author: Filbert. Reviewers: Darkwing and Dewey. Brief: "Bus read: decisions
|
||
routed to roles and recent decisions" in
|
||
`docs/plans/2026-10-10_design-implementation.md`. Base: `958668a8476250edab8025a48a5c553dfd75f99b` (`base.txt`).
|
||
The feature is uncommitted; only this packet is committed. I pushed nothing.
|
||
|
||
## Files
|
||
|
||
`files.txt` lists the 8 repository paths. `candidate-manifest.sha256` has
|
||
their sha256. `build.patch` is `git diff --cached --binary 958668a8` over them:
|
||
8 files, +297/−17.
|
||
|
||
| File | Change |
|
||
|---|---|
|
||
| `packages/bus/src/broker.mjs` | new verb `decisions`, `{list: 'roles' \| 'recent'}`; `RECENT = 50`; the reader allowlist adds it |
|
||
| `packages/bus/src/views.mjs` | `decisions(list)` in the Q1 client module |
|
||
| `packages/bus/tests/decisions.test.mjs` | new: 6 tests, listed below |
|
||
| `packages/bus/README.md` | "Human and reader paths" says what the verb returns and who may call it; the "Shared reads" example |
|
||
| `packages/webui/src/serve.mjs` | `GET /api/bus/decisions?list=roles\|recent`; any other list is 400 and never reaches the bus |
|
||
| `packages/webui/src/bus.mjs` | the header comment only; the reader already passes any `views()` verb through |
|
||
| `packages/webui/tests/bus.test.mjs` | one new test for the route; the 405 test and the `busReader` test cover it too |
|
||
| `packages/webui/README.md` | the bus read list and the port exposure note |
|
||
|
||
## Scope
|
||
|
||
The brief owns `packages/bus/`, `packages/webui/src/bus.mjs` and its tests.
|
||
Two files outside that list changed:
|
||
|
||
- `packages/webui/src/serve.mjs`. The brief's "its webui endpoint" is a
|
||
route, and the routes live there, not in `bus.mjs`. The change adds the
|
||
verb to `BUS_VERBS` and checks `?list=` before the bus is called.
|
||
- `packages/webui/README.md`. It lists every `/api/bus/` route and what the
|
||
port exposes, so a new route without it would leave the README wrong.
|
||
|
||
No Inbox tab uses the endpoint yet. The brief ships the read and the
|
||
endpoint; the tabs are the design rows' work.
|
||
|
||
## How it works
|
||
|
||
`decisions` is a broker verb like `inbox`, in the same read transaction.
|
||
|
||
- Who: human and reader sessions. A role session gets `human-required`,
|
||
claimed or not, and the refusal is recorded as `action.refused` like
|
||
every refusal. A reader still gets `read-only` for every write.
|
||
- Arguments: `{list}` and nothing else (`keys(a, ['list'], ['list'])`).
|
||
Any other list is `invalid-request`.
|
||
- `roles`: the business's open decisions with `route_to <> 'human'`, in
|
||
`seq` order. Open means no `resolved`, `withdrawn` or `expired` event,
|
||
the same rule as `inbox` and the schema's `inbox` view. The human's own
|
||
open decisions stay in `inbox`. Unlike `inbox`, gated decisions are not
|
||
put first: the list spans roles, and `route_to` is on each row.
|
||
- `recent`: the business's closed decisions of every route, newest close
|
||
first (`decision_events.seq DESC`), at most `RECENT` (50). Each row has
|
||
`closed`, the closing `decision_events` row, from the same `#closed()`
|
||
that the authority check and `decision.resolve` use. The schema trigger
|
||
`decisions_resolve_once` allows one close per decision, so the join
|
||
can't repeat a row.
|
||
- Each row is `#decisionView(d)`, the shape `inbox` returns: options
|
||
parsed, `blocking` a boolean, and the authorization context.
|
||
|
||
### What a reader may read
|
||
|
||
The brief says the reader's authority doesn't widen. Per row, it doesn't:
|
||
`trail(<decision id>)` already gives a reader or human session the same
|
||
decision row and its closing event, and test 4 checks every listed row
|
||
against it. What is new is the listing. Before this row a reader needed
|
||
the id. Decision ids are random UUIDs. A decision with a `task_ref` could
|
||
already be found through `tasks` and then the task's trail, but one
|
||
without (a within-role decision, or a scope change with only a target)
|
||
could not.
|
||
|
||
The WebUI port is where that matters. F1 on #1522 recorded that anything
|
||
that can connect to the Console's port reads what a reader capability
|
||
reads, including a T3 seat with `curl` and a managed S6 session, and slice
|
||
1 accepted that. With this row that includes both lists, so a role session
|
||
that can reach a running Console's port can list other roles' open
|
||
decisions there, though not through its own bus capability. The README's
|
||
exposure note now says so. The brief's test is on the bus capability
|
||
(test 5); closing the port is outside this row, as it was for S5.
|
||
|
||
## Tests
|
||
|
||
`packages/bus/tests/decisions.test.mjs`, each for both a human and a
|
||
reader session where it applies:
|
||
|
||
1. `roles` lists the open decisions routed to cto and pm, never the
|
||
human's, with the `inbox` shape; `inbox` still lists only the human's.
|
||
2. `recent` lists closed decisions of every route, newest close first,
|
||
with how each closed: resolved by the human (via `cli`, with its
|
||
note), resolved by a role, withdrawn, expired, and within-role.
|
||
`roles` is then empty.
|
||
3. `recent` stops at `RECENT` rows, newest first.
|
||
4. Every row of both lists equals the decision row, and its `closed` the
|
||
closing event, that `trail` gives the same session for that id.
|
||
5. A role session (cto, pm, coder, and an unclaimed launch) gets
|
||
`human-required` for `roles`, `recent` and an unknown list, and each
|
||
refusal is recorded. Each role's `inbox` stays its own. A role in
|
||
another business is refused too.
|
||
6. One business only (a reader of `other` sees only `other`'s rows), and
|
||
strict arguments (`{}`, `all`, `limit`, an array, `Recent`). A
|
||
successful read writes no row in `events`, `decisions`,
|
||
`decision_events`, `messages`, `deliveries` or `role_claims`. A reader
|
||
still can't send `decision.seen`.
|
||
|
||
`packages/webui/tests/bus.test.mjs`:
|
||
|
||
- New: the route serves both lists from a real broker through the reader
|
||
view, with `no-store`, `{rows, at}`, and a hostile question unescaped
|
||
in the JSON. A missing, empty, unknown, wrong-case or NUL-suffixed list,
|
||
or `?subject=` instead, is 400 and never calls the bus.
|
||
- The 405 test sends every non-GET method to the new route too.
|
||
- The `busReader` test checks `decisions('recent')` sends
|
||
`{business, verb: 'decisions', args: {list: 'recent'}}` to the transport.
|
||
|
||
### Mutants
|
||
|
||
`out/mutants/run.mjs` edits one source file in a second worktree, runs the
|
||
bus and webui suites, and restores the file (`out/mutants/result.txt`):
|
||
|
||
| Mutant | Change | Killed by |
|
||
|---|---|---|
|
||
| `role-allowed` | a role session may call `decisions` | bus test 5 |
|
||
| `roles-with-human` | `roles` includes the human's decisions | bus test 1; webui route test |
|
||
| `roles-with-closed` | `roles` includes closed decisions | bus tests 1, 2, 6; webui route test |
|
||
| `roles-other-business` | `roles` ignores the business | bus test 6 |
|
||
| `recent-other-business` | `recent` ignores the business | bus test 6 |
|
||
| `recent-oldest-first` | `recent` oldest close first | bus tests 2, 3 |
|
||
| `recent-by-raise` | `recent` ordered by raise, not close | bus test 2 |
|
||
| `recent-no-limit` | no `LIMIT` | bus test 3 |
|
||
| `recent-limit-plus-one` | `LIMIT RECENT + 1` | bus test 3 |
|
||
| `recent-resolved-only` | `recent` drops withdrawn and expired | bus test 2 |
|
||
| `recent-no-closed` | rows lose `closed` | bus test 2; webui route test |
|
||
| `loose-keys` | `limit` accepted | bus test 6 |
|
||
| `unknown-list-empty` | an unknown list returns `[]` | bus test 6 |
|
||
| `reader-refused` | the reader allowlist drops `decisions` | bus tests 1-4 and 6; webui route test |
|
||
| `web-no-list-check` | the route passes any `?list=` to the bus | webui route test |
|
||
| `web-no-verb` | `decisions` not in `BUS_VERBS` | webui route test and 405 test |
|
||
| `views-fixed-list` | `views().decisions` ignores its argument | webui route test and `busReader` test |
|
||
|
||
All 17 are killed. `roles-other-business` first matched the `inbox` query
|
||
too; I narrowed the pattern and reran it with `roles-with-human`
|
||
(`result-rerun.txt`). In the first run, `roles-with-human`'s webui run also
|
||
hit the 600-second timeout in unrelated conversation-view tests while the
|
||
machine was loaded; the rerun shows the route test failing on its own.
|
||
|
||
## Gate
|
||
|
||
Run on `958668a8` with the candidate applied, sequentially, `TMPDIR` in my
|
||
scratch and `DOCKER_HOST` pointed at a missing socket (`out/gate/`):
|
||
|
||
| Suite | Result |
|
||
|---|---|
|
||
| `node --test packages/bus/tests/*.test.mjs` | rc 0, 80 pass, 0 fail |
|
||
| `node --test tests/` in `packages/webui` | rc 0, 40 pass, 0 fail |
|
||
| `test-auth.sh` | rc 0, 15 passed |
|
||
| `test-conductor.sh` | rc 0, 17 passed |
|
||
| `test-config.sh` | rc 0, 24 passed |
|
||
| `test-discord.sh` | rc 0, 66 passed |
|
||
| `test-extension-package.sh` | rc 0, 18 passed |
|
||
| `test-foundation.sh` | rc 0, 44 passed |
|
||
| `test-queue.sh` | rc 0, 27 passed |
|
||
| `test-release.sh` | rc 0, 4 passed; the Docker cases skip without a daemon |
|
||
| `test-task.sh` | rc 0, 26 passed |
|
||
|
||
The manifest checks OK after the gate. Sage's rerun with Docker is the
|
||
gate of record.
|
||
|
||
## Things a reviewer should know
|
||
|
||
- `RECENT` is fixed at 50 and the verb takes no limit. The design's Recent
|
||
tab shows one page; a limit argument would be a new input to validate
|
||
for no current caller.
|
||
- The `recent` query reads `decision_events` once per row through
|
||
`#closed()`, as `inbox` does. At 50 rows that is 51 indexed reads in one
|
||
transaction.
|
||
- A decision raised and closed in the same call (within-role with a
|
||
`choice`) appears only in `recent`, never in `roles`.
|