writeonce/docs/superpowers/plans/2026-08-01-http-service-layer.md
shoney.arickathil 5521d21a84 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

11 KiB
Raw Blame History

HTTP Service Layer Implementation Plan

Status: ⬜ pending (story iteration 25) — service blocks route to VM methods; REST parity with the shipped Rust Stage 2 runtime. Board: 00-status.md

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Style rule (user convention): concept, reason, and required behavior in words only; the executor writes the code.

Goal: Sub-project 4 of the OOP spec — service rest blocks become live routes on the C runtime: generated CRUD over the DB binding, method-RPC endpoints, one trap-to-HTTP mapping, hand-rolled JSON — the blog sample's REST surface served by the new stack.

Architecture: Plan 6 of 7. Depends on plans 1–5. The HTTP substrate is shipped C: runtime/wo-rt.c phase C's io_uring loop with keep-alive and a per-connection write queue (strace-verified: zero epoll/recv/send syscalls on the request path). This plan ports that pattern into runtime/src/ and mounts a router on it, per shard (routers are shard-local state, mirroring the Rust runtime's thread-local routers — HandlerFn not shared). The compiler stops skipping service blocks and compiles them into a route section the runtime loads. JSON is hand-rolled per the existing dependency-removal doc (docs/plan/05-hand-rolled-json.md) — the C sibling of what the Rust track already specified.

Tech Stack: C11 + libc (io_uring per the shipped phase-C pattern, SO_REUSEPORT per phase A). OCaml (service-block parsing + route emission). No TLS in this plan — stated non-scope, terminate elsewhere.

Global Constraints

  • All plan-1/4/5 constraints carry over (libc only, no commits — drafts to .dev/commit.md, ASan/TSan gates, docs under docs/).
  • Routers are shard-local — built per worker at boot from the module's route section; no shared routing state.
  • Reads and acks never leave the shard's serial stream — handlers run as jobs on the owning shard; cross-shard point ops use the plan-4/5 hop-once machinery.
  • The .wob format change (route section) bumps the version to 2 — one coordinated change to the format doc, loader, builder, and emitter, in one task, with the loader still rejecting v1-invalid images identically ("format changes go through the format doc" rule honored by doing it once, visibly).
  • Stage-3 endpoints stay honest: /api/<type>/live returns 501 exactly as .dev/reference/rest/*.rest documents — LIVE lands in plan 7; don't "fix" the 501s.
  • JSON is hand-rolled — no library, bounded depth, UTF-8 safe, byte-length (not char-length) content framing.

File Structure

runtime/src/
  http.c http.h          request parse, keep-alive state machine, response write (Task 1)
  json.c json.h          hand-rolled encoder/decoder (Task 2)
  router.c router.h      per-shard route table, path/method match (Task 3)
  handlers.c handlers.h  CRUD + method-RPC handlers over db.h (Tasks 4–5)
compiler/src/            service-block parsing + route-section emission (Task 3)
tests/corpus/http/       request/response fixtures driven over real sockets (Task 6)
docs/plan/oop-vm/05-http-service.md   route section format, trap→HTTP table, JSON subset (Task 1)

Task 1: HTTP module port

Concept & reason: lift phase C's proven connection machine out of the single file into a module the scheduler mounts per shard: multishot accept, keep-alive request parsing (method, path, headers subset, content-length bodies), per-connection write queue, pipelined-tail carry-over — behaviors phase C already measured; the port's test evidence must match (single connection serving many requests, syscall profile clean). The doc opens with the layer's contract: how handlers receive a parsed request and return status/headers/body, and the trap→HTTP table Task 5 implements.

  • Failing tests: socket-level harness — keep-alive sequence on one connection, bad-request handling (malformed start line → 400 and close), body framing by content-length, oversized request → 413 and close.
  • Implement the port; green; syscall profile spot-checked against the phase-C evidence.
  • Record commit draft: feat(runtime): http module — phase-C io_uring keep-alive machine as a per-shard module (multishot accept, write queues, pipelined tails); socket-harness tests; docs/plan/oop-vm/05-http-service.md contract.

Task 2: Hand-rolled JSON

Concept & reason: the codec both directions of every endpoint use, built to the existing plan-05 doc's discipline: encoder streams rows/objects using the six field kinds (scalars, texts with escaping, containers as arrays/objects, ids as numbers); decoder parses request bodies into field initializers with bounded nesting depth, exact UTF-8 validation, and duplicate-key rejection; numbers are i64-safe (no doubles in milestone types). Errors carry positions for 400 responses that name the byte offset. Content-Length is computed from encoded byte length — the log-watcher-documented non-ASCII truncation bug class, prevented by rule here.

  • Failing tests: round-trip across all kinds; escaping/UTF-8 edges (embedded quotes, multibyte, invalid sequences rejected); depth bomb rejected; duplicate keys rejected; byte-length framing with multibyte content.
  • Implement; green.
  • Record commit draft: feat(runtime): hand-rolled JSON — kind-driven encode, strict decode (UTF-8, depth, dup keys) with byte-offset errors, byte-length framing; edge-case battery.

Task 3: Service blocks compile to a route section

Concept & reason: the compiler's skip-on-block for service rest "..." expose list, get, create, update, delete ends: the parser builds a service AST (path prefix, exposed verbs, owning class), the typechecker validates verbs against the class (subscribe stays legal to declare — it routes to the 501 stub), and the emitter writes a route section into the .wob — the coordinated format v2 change: header gains the section, builder and loader gain support with full validation (paths well-formed, class ids in range, verbs known), version bumps, format doc updated, and every plan-1 loader test re-run to prove v1-shaped rejects still reject. The runtime's router loads the section per shard into a match table (exact-prefix plus :id segment).

  • Failing tests: compiler goldens (service AST, route section in the disassembler); loader validation of malformed route sections; router unit tests (match/miss/verb table incl. 405-shaped responses per the .rest conventions).
  • Implement across compiler, builder, loader, router; all prior gates re-run green.
  • Record commit draft: feat: service rest compiles — service AST + .wob v2 route section (coordinated builder/loader/emitter bump, format doc updated), per-shard router with :id matching and 405 semantics.

Task 4: Generated CRUD over the DB binding

Concept & reason: the six-endpoint contract the README promises, on the new stack: list (shard-local per the plan-5 scope, documented), get by id (hop-once cross-shard), create (JSON body → insert path with defaults, 201 with the row), update (PATCH partial-set → point update), delete (row remove through the choke-point API), each encoding responses through Task 2 and running as a job on the owning shard. Behavior parity target is the Rust runtime's Stage-2 semantics as documented by .dev/reference/rest/blog.rest — including auto-id, default seeding, and partial-update PATCH.

  • Failing tests: socket-level CRUD round-trips against a compiled blog-shaped fixture; PATCH partial semantics; 404 on missing ids; create-on-foreign-shard impossible by construction (create is local — test proves ids from the accepting shard).
  • Implement; green.
  • Record commit draft: feat(runtime): generated CRUD — list/get/create/update/delete over the row API with hop-once foreign reads, Stage-2 parity semantics (auto-id, defaults, PATCH).

Task 5: Method RPC + trap→HTTP mapping

Concept & reason: the 13b story on the new stack: exposed methods get POST routes (path per the Rust runtime's method-RPC convention), the handler decodes arguments, runs the method on the row's owning shard as a row-scoped job, and encodes the return value. The trap table becomes the error contract, implemented once in the handler layer and documented in the Task-1 doc: BOUNDS/KEY on missing rows or fields → 404/400 as appropriate; BORROW (residual aliasing) → 409; DIV0 and EXPLICIT → 500 with the structured {code, method, line, message} body the spec promised in section 6; DB → 501 (anything still unbound); STACK/OOM → 500 with no body detail. The subscribe verb's /live route returns the honest 501.

  • Failing tests: method round-trip on the pricing fixture (set_price over HTTP mutates, current_price reads back); each trap class mapped (fixtures rig each trap) with the structured error body asserted; /live 501.
  • Implement; green.
  • Record commit draft: feat(runtime): method RPC + trap contract — exposed methods as shard-local POST jobs, one trap→HTTP table (409 borrow, 404/400 bounds, structured 500 bodies), honest /live 501.

Task 6: Blog-sample smoke + acceptance

Concept & reason: the end-to-end proof the stack means something: the blog sample's milestone-compatible subset (types + service blocks; policies/triggers still parse-and-discard) compiles with woc, serves with the sharded runtime, and a scripted run of the .dev/reference/rest/blog.rest request sequence (curl-driven, per that directory's README) passes — expected statuses including the documented 501s and policy-shaped 405/404s where applicable. A light bench recipe (reusing the phase-C bench harness shape) records requests/sec for the record, not as a gate. just oop-accept gains the HTTP corpus and the blog smoke; CLAUDE.md/kanban sync.

  • Wire smoke + bench + gate; green; docs synced.
  • Record commit draft: test: blog-sample smoke on the C stack — scripted blog.rest sequence green (incl. honest 501/405 semantics), bench recipe for the record; oop-accept gains the HTTP gate; docs sync.

Plan self-review notes

  • Spec coverage (sub-project 4): service blocks routed, trap surface → HTTP exactly as spec section 6 promised ("one trap surface forever"), CRUD parity with Stage 2, method RPC, shard-local routers — all tasked. Non-scope, stated: TLS, LIVE/WebSocket (plan 7), policies/triggers (still parse-and-discard), scatter-gather list.
  • Order rationale: transport before codec before routing before handlers; the format-v2 bump isolated in one task with full regression re-run; end-to-end smoke last.
  • Consistency check: the trap→HTTP table lives in one doc section and one handler-layer implementation; CRUD and RPC both route through it. Route section validated with the same loader rigor as every other section.