writeonce/docs/examples/writeonce-framework
shoney.arickathil a798cf6698 docs: pending iterations renumbered by dependency + priority
- developer directive: pending iteration IDs now ARE the priority order;
  LANDED iterations keep historical numbers (code comments and commit
  history cite them — records, not a queue); 8/11 (the half-landed
  arc), 17 (parked, artifacts on a branch), 18 (next, artifacts named)
  also frozen
- mapping (recorded in 00-story): 19<-20 Float+Bytes, 20<-9c attach,
  21<-9d keypair, 22<-9e benchmarks, 23<-9f io_uring WAL, 24<-19 chat,
  25<-10 services, 26<-12 blue-green, 27<-9g query corpus,
  28<-14 skillhost, 29<-13 metaprogramming
- 11 story files renamed; every doc reference re-numbered (word-boundary
  sweep for the lettered 9x ids, phrase-level for numeric ones); the
  iterations table rewritten with Seq == priority and "(was N)" notes;
  story-scoped link check: zero broken
- merge-recovery folded in: the partial master merge had dropped the
  chat story, the fibers exploration note, the arc spec+plan, the
  framework-v2 plan, and the iteration-17 spec+plan — all restored from
  their branches and renumbered consistently

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:31:09 +02:00
..
http feat(framework): multipart/form-data parsing — Part, multipart_parts, part_named 2026-08-20 03:33:14 +02:00
router feat(framework): auth in core — Bearer/Basic mechanism + principal slot 2026-08-20 03:15:16 +02:00
app.wo feat(framework): v1 polish — helpers, 405+Allow, HEAD, Logging, set_header 2026-08-20 03:04:57 +02:00
README.md docs: pending iterations renumbered by dependency + priority 2026-08-20 14:31:09 +02:00
wo.toml feat(examples): framework skeleton + web-app scaffold (iter 16 Task 1) 2026-08-19 19:43:19 +02:00

writeonce-framework

A web framework written in writeonce, consumed as a [deps] dependency (iteration 15). Spec: docs/superpowers/specs/2026-08-18-web-framework-design.md §B.

[deps]
writeonce-framework = { git = "https://github.com/shoneyj/writeonce-framework", rev = "v0.1.0" }

What it is

  • HTTP/1.1 server core (http/): request parsing (Content-Length bodies, %-decoded paths and query strings), response serialization, a blocking serve loop that answers 400 to malformed requests, 500 to trapping handlers (and survives), closes every fd, and honors SIGTERM. Connection policy: pipelined requests are served on one connection; idle connections close after the response — on a single-threaded server a parked keep-alive connection would block accept and starve every other client, so closing is the correct shape until shards/fibers (8/11). A proxy in front simply reconnects.
  • Router (router/): method + path table with :param captures into req.params; first match wins; a known path with the wrong method is 405 with the Allow header (registration order); no matching path is the framework's 404. HEAD is served free: routed as GET, body suppressed, Content-Length still names the body a GET would carry.
  • Registration helpers: app.get/post/put/delete_(pattern, handler) push the route for you (delete_ because delete is the query keyword); app.add(Route { ... }) stays for anything else. A request-line Logging middleware ships in router/, and set_header(resp, name, value) is the escape hatch for headers the builders don't set.
  • Handlers without closures: the language has no function values by doctrine, so a route handler is a class satisfying the Handler interface (fn handle(req: Req) -> Resp), dispatched structurally — a non-conforming handler is a compile error (WO-E205). Middleware is its own interface (fn before(req: Req) -> ?Resp; nil = continue, a Resp short-circuits).
  • Auth mechanism in core (http/auth.wo): Authorization header parsing (scheme split, case-insensitive), pure-.wo base64, a constant-time comparator (ct_eq, no early exit), and the blessed principal slot — req.principal is "" until an auth middleware authenticates, then downstream handlers read who it is. BearerAuth and BasicAuth (with the WWW-Authenticate challenge) ship as middlewares; POLICY — which routes, which users, where secrets live — stays in the app, on top of bearer_token/basic_credentials/ct_eq.
  • Data layer for free: handlers use @table + the query surface directly — durable, compiler-checked persistence in the same binary. No ORM, no database server.

Honest limits (v1, all deliberate)

  • Single-threaded, blocking — one request at a time. Concurrency arrives underneath this same surface with the shard/fiber iterations (8/11).
  • TLS: none, anywhere. Deploy behind nginx/caddy; the proxy terminates TLS+ALPN and gives browsers HTTP/2 while this backend speaks HTTP/1.1 keep-alive. See the web-app sample's README for the nginx sketch.
  • Content-Length bodies only (no chunked encoding), no WebSockets/SSE, JSON-first (no templates). Form-encoded bodies parse through form_values(req) (+ and %XX decoded, nil on any other content-type); multipart/form-data through multipart_parts(req) (whole-body, bounded by BODY_MAX — no streaming uploads until fibers/shards) with part_named for fields; media_type(req) names the body's media type for content negotiation.

The v1 surface — status ledger (2026-08-20)

The target surface of framework v1, tracked per item. The memory-rich features (TTL cache, feature flags, durable job queue, transaction { }) are framework v2 — iteration 18, spec written, NOT part of v1. Legend: ✅ shipped · 🔶 partial (gap named) · ⬜ candidate slice · ⏸ parked behind a runtime iteration · 🔧 needs a runtime/compiler seam first (pure .wo cannot express it yet).

Transport

Item State
HTTP/1.1 parsing 🔶 parses + 400-and-survive; STRICT ambiguity rejection (duplicate/conflicting Content-Length, oversize checks beyond BODY_MAX) not audited — hardening slice
Keep-alive ✅ pipelined-serve / close-when-idle (starvation-honest until 8/11)
Read/write/idle timeouts 🔧 net has no timeout surface — runtime seam, then a framework knob
Request size limits ✅ BODY_MAX bounds headers AND body
Unix socket binding 🔧 net.listen is TCP-only — runtime seam
Graceful SIGTERM ✅ in-flight request completes (blocking model), listener + fds closed, storage is per-commit durable (WAL fdatasync — nothing to checkpoint)

Routing

Item State
Path matching 🔶 linear scan, first-match-wins; a radix tree is a performance slice that waits for iteration 22 to MEASURE it first
Method dispatch · path params · 404 · 405+Allow ✅
Wildcards ⬜ only :param today; *rest capture is a candidate slice
Precedence rules 🔶 registration order IS the rule (documented); specificity-based precedence unneeded until wildcards exist
Route groups ⬜ candidate slice (prefix + per-group middleware)

Request/response

Item State
Case-insensitive headers · query parsing ✅ (names lowercased on read)
JSON · form-urlencoded · multipart ✅ all three hooks (json.decode, form_values, multipart_parts)
Content negotiation 🔶 media_type(req) covers the request side; Accept-driven response negotiation ⬜
Trusted-proxy client IP 🔶 X-Forwarded-For/-Proto parsing is expressible (candidate slice); VERIFYING the peer is the trusted proxy needs a peer-address runtime seam 🔧
Status/header setting · redirects ✅ builders + set_header
Lazy body streaming + backpressure · streaming responses · explicit commit point ⏸ 8/11 — whole bodies, one write, by design
ETag + conditional requests ⬜ candidate; wants the crypto slice's hashing

Context & middleware

Item State
Ordered middleware chain ✅ registration order, ?Resp short-circuits
Request-scoped context 🔶 req.params + req.principal are the context today; a general req.ctx bag is a candidate slice
Guaranteed teardown 🔶 every fd closes on every path (gate-proven); no user teardown hooks yet
Cancellation into pending storage ops ⏸ fibers (11)
Panic recovery 🔶 trap = 500 and the server survives ✅; "rolls back the transaction" is framework v2 (needs transaction { }, iteration 18)

Storage integration (the differentiator — framework v2 territory)

Item State
Transaction-per-request middleware (commit on 2xx, roll back otherwise) ⏸ v2 — needs iteration 18's transaction { }
Cancellation → rollback ⏸ fibers (11) + v2
Migration generation + review workflow ⬜ recorded future story (script-based destructive migrations)
Eager-loading API (N+1) ⬜ query-surface work (9-series), not framework code
Tenant-scoped query roots ⬜ future; wants the query surface to grow scoped roots first

Security

Item State
Constant-time comparison · Authorization parsing · Basic auth · principal ✅ http/auth.wo, req.principal
CORS ⬜ candidate slice (middleware + preflight answers)
Security headers ⬜ candidate slice (one middleware, a header set)
Host validation ⬜ candidate slice (middleware against a host allowlist)
Strict parsing 🔶 same item as Transport's hardening slice

Crypto (self-written, hard-stop after JWT HS256)

Item State
base64 ✅ pure .wo (http/auth.wo)
SHA-256 · SHA-512 · HMAC · CRC32 🔧 the language has NO bitwise operators — these are C runtime builtins (libc-only doctrine permits hand-rolled crypto in the runtime) or the language grows bit ops first; the fork goes to a brainstorm before the slice
Unlocks (signed cookies, CSRF, session integrity, webhook verification, JWT HS256) ⬜ framework slices AFTER the hash primitives exist; hard stop there — no RS256, no JOSE zoo

The consuming sample

docs/examples/web-app — a small storefront importing this framework through [deps]. Its acceptance (just web-app) exercises the whole chain: fetch → lock → build → serve → durable restart.