Board row 16 -> landed (web-app 14/0), pending row removed; story header records the landing + the two as-built discoveries (idle-keep-alive starvation policy; the two compiler gaps the chain exposed and fixed); README gains the framework+web-app sample entry; plan checkboxes ticked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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-app14/0. Deviations from the plan as written, recorded honestly: (1) registration isapp.add(Route { method, pattern, h })— the get/post/put helper fns were dropped because the ctor-literal- plus-takeshape 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 isframework(hyphens are not identifier characters inusepaths); (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 internalusepaths resolve dep-relatively; (6) Dispatcher takesmut reqso :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
useover 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.tomlcarries the framework's FUTURE GitHub URL as documentation; the acceptance gate substitutes a run-timefile://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.lockartifacts and CI never touches the network. - Gates:
just web-app(the new acceptance), plus the standingjust 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. Reqrecord: method, path, params (:paramcaptures), query, headers (allmap<Text, Text>), body Text.Resprecord: status, headers, body. Builder free fns: ok_text, ok_json, not_found, bad_request, server_error, redirect — each returns a fully-formedResp(content-type set).- Verify: the framework directory typechecks standalone (
wocon it — nofn 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
netconnection: request line (method, target — split path from query string, decode%-escapes in both), header lines to the blank line, then exactlyContent-Lengthbytes 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-Lengthalways computed from the body,Connection: keep-aliveunless 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 intryso 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
.womain 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}.
Handlerinterface (handle(req) -> Resp) andMiddlewareinterface (before(req) -> ?Resp, nil = continue) — the spec's shapes verbatim.- Route table:
Appholdsmultiof route records (method, the pattern split into segments, the handler value). Matching walks segments; a:namesegment captures intoreq.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 (aRespshort-circuits —?Respnarrowing), then route, then the matched handler, all inside the Task-2 loop's try boundary.- Verify with the throwaway main: two routes incl.
/things/:idechoing 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 afile://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@uniqueand 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/(tagv0.1.0), copydocs/examples/web-app/to a temp dir, substitute thefile://URL into its manifest, then: fetch+build (lock written); serve on a scratch port withWO_DATAset; 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-inWA_SOAKmirroring 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-appstill green; commit.
Success criteria (spec §Success criteria, restated)
- web-app builds purely through
[deps]+wo.lock; no path references; offline once locked. - The storefront serves with
@tablepersistence across restart; nginx in front gives browsers h2 while the backend speaks HTTP/1.1 (documented, demonstrated manually). - A non-conforming handler is WO-E205 at compile time; a trapping handler answers 500 and the server survives (gate-proven).
- 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.