diff --git a/docs/superpowers/plans/2026-08-29-porch-store-backed-middleware-rulings.md b/docs/superpowers/plans/2026-08-29-porch-store-backed-middleware-rulings.md new file mode 100644 index 0000000..d3745b2 --- /dev/null +++ b/docs/superpowers/plans/2026-08-29-porch-store-backed-middleware-rulings.md @@ -0,0 +1,56 @@ +# porch 1 — decisions taken during execution + +Fifteen rulings made while executing +[the plan](2026-08-29-porch-store-backed-middleware.md) without stopping to ask. +Each says what was decided, why, and what it costs if wrong. **One is known +wrong and was overturned mid-flight (R9/R14).** + +| # | Ruling | Cost if wrong | +| --- | --- | --- | +| — | Work on `dev`, not a fresh worktree — `dev` is the project's designated development branch, so this is not the "never implement on master" case | Interleaves with other dev commits; separable by the `porch-store` prefix | +| R1 | The pool takes ONE message class with a `kind: Int` discriminator, not a `receive` per message type. An actor handle is typed to a single message class, so a second `receive` compiles but is unreachable. Verified by fixture before ruling | Task 2's protocol restructured; contained to `keypool.wo` | +| R2 | Task 4's file list had to include `keypool.wo` — its step 4 edits the actor arm that lives there | None; a documentation correction | +| R3 | "Never delete-then-insert" bans that pair as an UPDATE. Deleting an expired row is required and is not covered | An implementer leaves rows unpruned, leaking one per key ever seen | +| R4 | `call`'s reply must be a copyable scalar (WO-E226) and every `receive` program-wide must agree, so **the response travels through the `@table`, not the mailbox** | Task 4's protocol changes shape again | +| R5 | Task 3 calls the `pool_count` wrapper, never `call()` directly | Fails to typecheck immediately — cheap to detect | +| R6 | `trust_proxy: true` with an absent `X-Forwarded-For` collapsed every client onto the key `"ip:"`. Falls back to `net.peer`. **My brief caused this** | None identified; the fallback is strictly safer than a shared bucket | +| R7 | Kept the `Limiter.after()` added beyond brief scope — "headers on both paths" cannot be met by stashing values nothing consumes | A little surface to remove later | +| R8 | Kept `curl` in the gate legs, with an up-front presence check so its absence fails loudly | One more host dependency for the gate | +| R9 | **WRONG — see R14.** Ruled that `Pool` being traced forces a one-slot re-wrap | The feature's headline property was unreachable as documented | +| R10 | The hardcoded `Retry-After: 1` on 503 stands; the brief requires the header present, not computed | Clients retry marginally too eagerly under sustained saturation | +| R11 | `Idempotent` becomes a `Handler` decorator, not a `Middleware` — the actor needs the route's handler, and only that slot exposes it | Users register it differently; caught at compile time | +| R12 | Overrode a reviewer's **Minor** to Important: caching any status meant a transient 500 replayed for 24h and a retry could never succeed | One extra fix round | +| R13 | Sent two Minors to a fix round against the usual rule, because one inflated the check count with an assertion that could never fail | One cheap extra round | +| R14 | **Overturns R9.** WO-E222 fires on the class `Pool`, not on `multi`; an actor can hold `slots: multi PoolSlot`. Tested by the final reviewer | Already paid: a README shipped prescribing a permanent 1-actor pool | +| R15 | **PARKED**: `pool_slots`/`pool_of` are compile-proven and precedent-backed but have zero call sites, so real N-actor sharding is gate-unproven | The sharding path could hold a runtime defect nobody has exercised | + +## The gate is not stably green + +Measured over ten consecutive runs of `scripts/web-app-accept.sh` after the +final fix wave: most runs report 0 or 1 failures, and the failing check is +almost always `idempotent-stop` / `idempotent-stop-2` — the SIGTERM teardown +assertions. One run lost **six** checks at once, with `000` status codes +meaning the server returned nothing at all. + +Both symptoms trace to the C-runtime defect below, not to this feature's logic: +the failures are teardown timeouts and, when the runtime crashes, whole +sections. **Do not read "83 checks, 0 failures" as a stable result** — it is one +outcome of a distribution. + +## Three C-runtime defects found, none fixed + +They live in the C runtime, not in porch, and each is worked around here: + +1. `try EXPR catch (e) nil` cannot distinguish a literal `Int 0` reply from a + trap. Worked around by never packing a zero outcome code. +2. A `Text`/map value read off `json.decode(...) as T` is **corrupted** once + embedded in a struct crossing a function-return boundary. Worked around by + forcing fresh text with `.. ""` on every field copied out of a decoded record. +3. Under concurrent `call()`-parked callers with real table I/O, the process + sometimes hangs after `main()` returns, with an observed **SIGSEGV**. + Localised by `gdb` to `wo_arena_alloc` / `wo_str_new` / `vm_run`, and it + fires more readily at higher sequential insert+delete volume against one key + (N=4/5 crashed; N=1-3 clean over 12+ trials). + +The third is the reason the gate flakes, and it is the highest-value thing on +this branch to fix next — ahead of any remaining porch work.