- 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>
11 KiB
HTTP Service Layer Implementation Plan
Status: ⬜ pending (story iteration 25) —
serviceblocks 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 underdocs/). - 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
.wobformat 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>/livereturns 501 exactly as.dev/reference/rest/*.restdocuments — 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
.restconventions). - 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.