writeonce/docs/superpowers/plans/2026-08-19-web-framework.md
shoney.arickathil 57b4b56eae docs: stories foldered by state — done/ and refine/
- done/ (11): 1, 2, 3, 4, 6, 7, 7b, 9, 9b, 15, 16 — landed iterations
  (9/9b remainders live in the post-12 drain list, not in the files)
- refine/ (8): 9c, 9d, 9e, 9f, 9g, 11, 13, 14 — everything marked
  "no spec yet / brainstorm before planning"
- root keeps: 00-story (index), 05 (partial, plan 8 open), 8/10/12
  (specs or plans exist), 17 (parked, spec+plan approved), 18 (next)
- every cross-reference re-pathed and VERIFIED resolving: board, specs,
  plans, employee-list README, story table, intra-story links (moved
  files' relative links deepened one level; done/7b's 9e pointer now
  crosses to refine/)
- pre-existing dead link noted, not touched: refine/11-fibers.md points
  at docs/plan/exploration/fibers/00-fibers.md which does not exist
  (predates the move)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 04:33:24 +02:00

10 KiB

Iteration 16 — the web framework + web-app: implementation plan

Status: COMPLETE (2026-08-19) — all tasks landed on branch web-framework; just web-app 14/0. Deviations from the plan as written, recorded honestly: (1) registration is app.add(Route { method, pattern, h }) — the get/post/put helper fns were dropped because the ctor-literal- plus-take shape is the ownership pattern the corpus pins (run/container-owned-move); a helper taking an interface-typed parameter is unproven ground, deferred; (2) show/delete key on :name (the @unique index probe, the employee-proven pattern), not :id — row-id lookup is not in the query surface; (3) the [deps] key is framework (hyphens are not identifier characters in use paths); (4) connection policy became pipelined-keep-alive/close-when-idle after a probe showed an idle parked connection starves accept on a single-threaded server; (5) two compiler gaps surfaced and were fixed en route: owned-values-in-containers now MOVE, and a dep's internal use paths resolve dep-relatively; (6) Dispatcher takes mut req so :param captures land on the borrowed request — the borrow checker correctly refused rebuilding a Req from borrowed maps.

For agentic workers: use superpowers:executing-plans (inline) or subagent-driven-development. Steps are checkboxes. Per repo rule, this plan carries actions in words + verification commands, no code blocks — the normative design is the spec, which travels with this plan.

Goal: docs/examples/writeonce-framework/ — a web framework written in writeonce (HTTP/1.1 keep-alive server core, router with :param captures, Handler/Middleware structural interfaces, Req/Resp records) — and docs/examples/web-app/, a storefront that consumes it through [deps] (iteration 15) and persists through @table. The app, not the framework, owns the entry, the data model, and the routes.

Architecture: pure .wo — no compiler or runtime changes are expected (the two enabling probes already pass: interface-typed fields dispatch via ICALL, and the route-table pattern is pinned ASan-clean by tests/corpus/run/container-owned-move). The server core is the proven log-watcher mcp pattern (blocking net loop, fd-clean, stop-clean), generalized. TLS/h2 live at the reverse proxy; the backend speaks HTTP/1.1 keep-alive with Content-Length bodies only.

Spec: ../specs/2026-08-18-web-framework-design.md section B (normative; §C's h2c stays parked). Story: 16-web-framework.md.

Global Constraints

  • Framework and app are SEPARATE projects: the framework never references the app; the app reaches the framework only via use over a [deps] fetch — never a relative path. Extraction to its own repo must be a URL change.
  • Single-threaded blocking serving, stated in the framework README with the nginx upstream-keepalive deployment story; no chunked encoding, no WebSockets, JSON-first (all disclosed).
  • A handler trap answers 500 and the server survives; a malformed request answers 400 and closes; every accept path closes its fd; env.stopping() honored — the same discipline log-watcher's soak enforces.
  • The committed web-app/wo.toml carries the framework's FUTURE GitHub URL as documentation; the acceptance gate substitutes a run-time file:// remote (a temp git repo built from the framework directory) into a temp copy of the app, so the repo never contains .wo-deps/wo.lock artifacts and CI never touches the network.
  • Gates: just web-app (the new acceptance), plus the standing just woc-test / just oop-e2e / sample gates stay green untouched.

Task 1 — framework skeleton: Req/Resp, builders, project shape

Files: create docs/examples/writeonce-framework/{wo.toml,README.md, http/types.wo}.

  • Project manifest (name = "writeonce-framework"); README states what it is, the single-thread/proxy deployment story, and the disclosed limits up front.
  • Req record: method, path, params (:param captures), query, headers (all map<Text, Text>), body Text. Resp record: status, headers, body. Builder free fns: ok_text, ok_json, not_found, bad_request, server_error, redirect — each returns a fully-formed Resp (content-type set).
  • Verify: the framework directory typechecks standalone (woc on it — no fn main, so check-only) with zero diagnostics. Commit.

Task 2 — HTTP/1.1: parse, serialize, keep-alive serve loop

Files: create docs/examples/writeonce-framework/http/{parse.wo,serve.wo}.

  • Request parsing from a net connection: request line (method, target — split path from query string, decode %-escapes in both), header lines to the blank line, then exactly Content-Length bytes of body (missing length = empty body; a non-integer length or an oversized one is a 400). Header names lowercase on read so lookups are predictable. Anything malformed: respond 400, close, continue serving.
  • Response serialization: status line with reason text, headers, Content-Length always computed from the body, Connection: keep-alive unless the request asked to close.
  • The serve loop: net.listen, accept, then per connection read requests until EOF/close/stop — the keep-alive inner loop; the dispatch callback boundary is a structural interface the router provides (Task 3), wrapped in try so a trapping handler answers 500 and the loop lives. Close the connection fd on every exit path and the listener on stop.
  • Verify with a throwaway .wo main beside the framework (not committed): serve one echo handler, curl matrix — GET with query, keep-alive reuse (two requests, one connection), 400 on garbage, SIGTERM stops cleanly. Commit.

Task 3 — router, Handler/Middleware, the App assembly

Files: create docs/examples/writeonce-framework/{router/router.wo,app.wo}.

  • Handler interface (handle(req) -> Resp) and Middleware interface (before(req) -> ?Resp, nil = continue) — the spec's shapes verbatim.
  • Route table: App holds multi of route records (method, the pattern split into segments, the handler value). Matching walks segments; a :name segment captures into req.params. First match wins; no match is the framework's 404. Registration helpers: get/post/put/delete_ plus a generic route(method, pattern, handler).
  • App.serve(port): run middleware in order (a Resp short-circuits — ?Resp narrowing), then route, then the matched handler, all inside the Task-2 loop's try boundary.
  • Verify with the throwaway main: two routes incl. /things/:id echoing the capture, a header-checking middleware that short-circuits 401, 404 for unknown paths, 500 for a deliberately trapping handler with the server surviving. Commit.

Task 4 — the web-app storefront

Files: create docs/examples/web-app/{wo.toml,README.md,types.wo,main.wo} (+ a module justfile mirroring the other samples).

  • wo.toml: [deps] writeonce-framework = { git = <future GitHub URL>, rev = "v0.1.0" } (documentation value; the gate substitutes a file:// remote), [build] runtime unpinned (portable, per the log-watcher precedent).
  • Data model: @table Product (name @unique, price, stock) and @table Order (ref Product, qty) — small, honest, exercising @unique and FK restrict through web routes.
  • Routes: list products (query + json encode), show by :id, create product (json decode body — the checked decode's nil path is a 400), create order (FK), delete product (FK restrict surfaces as a 409-style error body, caught via try). One auth middleware (a shared-token header, 401 otherwise) registered before the routes.
  • fn main: build the App, register middleware + routes, serve(port) with the port from args. README documents the curl matrix and the nginx h2-in-front config sketch.
  • Verify by hand end to end once (fetch via a local file:// remote, serve, curl, restart, drop). Commit.

Task 5 — the acceptance gate

Files: create scripts/web-app-accept.sh; modify justfile (web-app recipe).

  • The gate builds the whole chain at run time: git-init a temp remote from docs/examples/writeonce-framework/ (tag v0.1.0), copy docs/examples/web-app/ to a temp dir, substitute the file:// URL into its manifest, then: fetch+build (lock written); serve on a scratch port with WO_DATA set; curl matrix — 401 without the token, list empty, create product, list shows it, show by id, unknown path 404, malformed json body 400, create order, delete-restricted product answers the error body while the server keeps serving; keep-alive reuse; kill -TERM stops cleanly; restart and the product list still answers (WAL persistence); fd/resident flatness via an opt-in WA_SOAK mirroring log-watcher's.
  • Wire just web-app; run it plus the standing gates (just woc-test, just oop-e2e, just deps-accept, just log-watcher, just employee) — all green.
  • Commit.

Task 6 — docs closeout

Files: modify docs/00-status.md (iteration 16 → done with what landed), story 16-web-framework.md (landing note), README.md (one paragraph + pointer under the samples list), docs/08-project-structure.md (the two new sample entries).

  • Apply; just web-app still green; commit.

Success criteria (spec §Success criteria, restated)

  1. web-app builds purely through [deps] + wo.lock; no path references; offline once locked.
  2. The storefront serves with @table persistence across restart; nginx in front gives browsers h2 while the backend speaks HTTP/1.1 (documented, demonstrated manually).
  3. A non-conforming handler is WO-E205 at compile time; a trapping handler answers 500 and the server survives (gate-proven).
  4. Extraction = changing the app's [deps] URL only.

Self-review notes

  • Spec §B coverage: types/builders → T1; parse/serialize/loop → T2; router/interfaces/App → T3; storefront + data model → T4; the gate → T5; docs → T6. §C (h2c) deliberately absent. No placeholders; interface names and record shapes match the spec exactly.
  • Two risks retired BEFORE this plan: interface-typed fields dispatch (probe passed) and the route-table double-free (fixed + pinned by run/container-owned-move).
  • Open risk, disclosed: %-escape decoding and header-case handling are easy to get subtly wrong — T2's verify step includes them explicitly, and the gate's curl matrix covers a query with an encoded space.