diff --git a/docs/00-status.md b/docs/00-status.md index c69f319..8cc5b55 100644 --- a/docs/00-status.md +++ b/docs/00-status.md @@ -320,7 +320,7 @@ The C proving-ground work (`exploration/c-runtime/`, phases A–F: 859k reads/s, | 9f | io_uring group-commit write path — batched durability overlapped on shard threads, fsync fallback | **no spec yet** — brainstorm after iterations 8 + 9e | | 9g | Query grammar from real embedded-DB corpora — whole-query count + correlated exists, driven by the skillhost SQL catalogue; add only what a corpus uses | **no spec yet** — three forks; may collapse to "confirm len(query) + add exists" | | 14 | skillhost host workload — port skillhost (MCP host + confined script runner) to writeonce; drives the missing host capabilities into the open (bounded subprocess, stdin/stdout transport, fs metadata, FFI-vs-out-of-process) | **no spec yet** — gaps recorded in the iteration; each gap brainstormed on demand, bounded-subprocess first | -| 16 | web framework — `.wo` library (HTTP/1.1 keep-alive behind a TLS-terminating proxy), Handler/Middleware interfaces, @table data layer; web-app sample consumes via [deps] | [spec §B](superpowers/specs/2026-08-18-web-framework-design.md) — depends on 15; h2c parked (§C) behind 8/9f/11 | +| 16 | web framework — `.wo` library (HTTP/1.1 keep-alive behind a TLS-terminating proxy), Handler/Middleware interfaces, @table data layer; web-app sample consumes via [deps] | [spec §B](superpowers/specs/2026-08-18-web-framework-design.md) · [plan](superpowers/plans/2026-08-19-web-framework.md) — 15 landed; h2c parked (§C) behind 8/9f/11 | | 10 | HTTP service layer | [plan 6](superpowers/plans/2026-08-01-http-service-layer.md) | | 11 | Fibers | vision §3, [blue-green exploration](plan/exploration/blue-green-vm/00-vision.md) | | 12 | Blue-green deploy | [spec](superpowers/specs/2026-08-03-blue-green-vm-design.md) — plan authored after iterations 9–10 | diff --git a/docs/stories/language-runtime-database/16-web-framework.md b/docs/stories/language-runtime-database/16-web-framework.md index d47b804..bb0cbb4 100644 --- a/docs/stories/language-runtime-database/16-web-framework.md +++ b/docs/stories/language-runtime-database/16-web-framework.md @@ -11,7 +11,13 @@ > when multiplexing has a scheduler to pay off on). > > **Spec exists:** [`2026-08-18-web-framework-design.md`](../../superpowers/specs/2026-08-18-web-framework-design.md) -> sections B (normative) and C (the parked h2c successor). +> sections B (normative) and C (the parked h2c successor). **Plan:** +> [`2026-08-19-web-framework.md`](../../superpowers/plans/2026-08-19-web-framework.md) +> (6 tasks: types/builders; HTTP/1.1 parse+serve; router+interfaces+App; +> the web-app storefront; the `just web-app` gate; docs closeout). Two +> enabling risks retired before planning: interface-field dispatch (probe) +> and the route-table owned-move double-free (fixed, pinned by +> `run/container-owned-move`). ## Goals diff --git a/docs/superpowers/plans/2026-08-19-web-framework.md b/docs/superpowers/plans/2026-08-19-web-framework.md new file mode 100644 index 0000000..3d52383 --- /dev/null +++ b/docs/superpowers/plans/2026-08-19-web-framework.md @@ -0,0 +1,179 @@ +# Iteration 16 — the web framework + web-app: implementation plan + +> **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`](../specs/2026-08-18-web-framework-design.md) +section B (normative; §C's h2c stays parked). Story: +[`16-web-framework.md`](../../stories/language-runtime-database/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`), 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 = , + 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.