writeonce/docs/superpowers/plans/2026-08-29-porch-store-backed-middleware-rulings.md
shoney.arickathil 5e619cf9de docs(porch-store): the fifteen rulings taken during execution
- records every decision made without stopping to ask, with what each
  costs if wrong, since the SDD workspace is deleted on completion
- R9 is marked WRONG and overturned by R14: WO-E222 fires on the class
  Pool, not on multi, so an actor can hold slots: multi PoolSlot. My
  ruling shipped a README prescribing a permanent 1-actor pool
- R6 records that my own brief caused a security bug: trust_proxy with
  an absent XFF collapsed every client onto one shared bucket
- R15 parks the one residual: pool_slots/pool_of have zero call sites,
  so real N-actor sharding is compile-proven but gate-unproven
- measured the gate over 10 runs: it is NOT stably green. Most runs
  fail idempotent-stop; one lost 6 checks with 000 status codes
- traces the flake to the C-runtime hang/segfault, now localised by gdb
  to wo_arena_alloc / wo_str_new / vm_run

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a919ab104ce44754949d364e35c88b6964b81fee)
2026-09-15 01:15:30 +02:00

4.9 KiB

porch 1 — decisions taken during execution

Fifteen rulings made while executing the plan 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.