plan: task 11 is landed, and what it taught task 12
Ticks the boxes and records the two departures from the plan, both of which task 12 inherits: the refusal mapping belongs in a table because its most important arms are unreachable through the door, and the consent chain is exported rather than re-derived.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ExnZtUYmt2h8QFcymjPaSL
1 file changed,
+22
−4
docs/superpowers/plans/2026-09-02-scan-on-demand.md+22 −4
CI passed — run details
diff --git a/docs/superpowers/plans/2026-09-02-scan-on-demand.md b/docs/superpowers/plans/2026-09-02-scan-on-demand.md| index a0dd8036a..7019e2d25 100644 |
| --- a/docs/superpowers/plans/2026-09-02-scan-on-demand.md |
| +++ b/docs/superpowers/plans/2026-09-02-scan-on-demand.md |
| @@ -381,16 +381,16 @@ Absence is `os.ErrNotExist` (`trash.go:64`), never an error. |
| **Files:** `internal/console/apiscan.go`, `authcli.go`, |
| `internal/console/scanhook.go` (new — the path validator only). |
| -- [ ] One test per refusal: `not_granted`, `not_requested`, `encrypted`, |
| +- [x] One test per refusal: `not_granted`, `not_requested`, `encrypted`, |
| `not_enabled`, `disabled`, 429. |
| -- [ ] `TestEnqueueRefusesWithoutAttestation` — **the consent hole review 3 |
| +- [x] `TestEnqueueRefusesWithoutAttestation` — **the consent hole review 3 |
| found.** The sweep refuses without the member's attestation |
| (`sweep.go:151-160`) and grant refuses without a request |
| (`apiscan.go:268-285`); a door checking only the deployment switch went |
| straight through it. |
| -- [ ] `TestEnqueueRequiresServiceRole` — `serviceToken`; `Admin` reaches it by |
| +- [x] `TestEnqueueRequiresServiceRole` — `serviceToken`; `Admin` reaches it by |
| construction (`svcrole.go:109`), which the test states rather than fights. |
| -- [ ] **`TestCallbackPathValidation`** — the §6.2 hostile table lands **here**, |
| +- [x] **`TestCallbackPathValidation`** — the §6.2 hostile table lands **here**, |
| not in the webhook task: absolute URLs, `//evil/x`, `/\evil/x`, |
| `@`-authority, traversal, raw and encoded CRLF, fragments, >1KB. Draft 1 |
| accepted the field in this task and validated it four commits later, so |
| @@ -398,6 +398,24 @@ Absence is `os.ErrNotExist` (`trash.go:64`), never an error. |
| **Commit:** `console: the scan enqueue door, behind the consent chain` |
| +**Landed** as `console: the scan enqueue door, behind the consent chain`. |
| +Two things came out differently from this plan, both worth carrying forward: |
| + |
| +- The refusal mapping was moved OUT of the handler into `scanRefusalAnswer`. |
| + Two of its arms are unreachable through the door — a trashed app is a 404 at |
| + `apiAccountApp` before the chain is consulted, and an undefined refusal name |
| + cannot be sent — and both survived mutation testing through HTTP for that |
| + reason alone. Task 12's drain has the same pair of unreachable conclusions |
| + (§7.5's keep-or-delete) and should be written as a table from the start |
| + rather than as a switch inside the goroutine. |
| +- `objstore.queuePermitted` is now exported as `QueuePermitted`, because the |
| + door is in another package and §5.1.1 forbids a second derivation. |
| + |
| +`TestCallbackPathValidation` was already landed with the validator; what this |
| +task added on top is the door-level proof that the door CALLS it, which is a |
| +different claim and the one that keeps hostile paths out of the bucket. |
| + |
| + |
| --- |
| ## Task 12: the drain goroutine and the boot rebuild |