- `readiness: ready | refine` is a SECOND axis, orthogonal to status.
`ready` = the brainstorm is complete and the decisions are LOCKED (a spec
approved, or the forks explicitly confirmed). `refine` = open forks remain
and it cannot be planned yet
- `status: refine` RETIRED because it carried both meanings at once, so a held
iteration with an approved spec (language 18, 26) was indistinguishable from
one nobody had thought about. status is now purely where the WORK is:
done | in-progress | pending | hold — `pending` was already the board's own
rendering word, so nothing new was invented
- all 47 iterations classified from EVIDENCE in their own text, not by guess:
"the four forks are SETTLED" / "spec + plan approved" / "Approved spec:" for
ready; "Forks the spec must settle" / "no spec exists yet" for refine. Every
shipped iteration is ready by definition. 19 done, 5 in-progress, 15
pending, 8 hold; 27 ready, 20 refine
- two iterations moved refine -> in-progress rather than -> pending: language
31 and 34 are absorbed into 24 and work on them is literally happening, which
the board already showed as 🔄 while their frontmatter said otherwise. That
disagreement is now gone
- board legend, board-views' frontmatter contract, and two new Dataview
queries updated — the useful one being `readiness: ready AND status:
pending`, the startable set
WHAT THE NEW AXIS IMMEDIATELY SURFACED: of 15 pending iterations, exactly ONE
is startable — databasev2 4, io_uring group-commit, whose forks were confirmed
settled 2026-08-20. Everything else pending needs a brainstorm first. That was
invisible while one key carried both meanings, and it is now on the board.
Also caught by the sweep, unrelated to readiness but found by cross-checking
frontmatter against the board: SIX duplicate rows. Every iteration moved into
databasev2 was still listed in the LANGUAGE pending table under its retired id
(23, 32, 33, 20, 21, 27) as well as its new one. Stale copies removed. And two
databasev2 rows made claims the sweep contradicts — iteration 1 was billed
"startable today" while its forks are open, and 6 still called itself the
ceiling-raiser after 2 took that role.
Docs only. linkcheck 0 broken / 0 anchors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7.9 KiB
7.9 KiB
| track | iteration | status | readiness |
|---|---|---|---|
| porch | 6 | pending | refine |
porch 6 — streaming core: the seam three iterations wait on
Part of Story —
porch, the writeonce web framework. Source: the Fiber parity study §3.The largest and riskiest slice in this track, and the one with the most downstream value: iterations 7 and 8 are both blocked on it, and porch's README has carried "lazy body streaming · streaming responses · explicit commit point" as parked since framework v1.
Goals
- A response that can be written incrementally. Today
internal/serve.wobuilds the whole response as oneTextand hands it to a singlenet.write, andserialize()always emitsContent-Length. Nothing can produce output it cannot first hold entirely in memory — which rules out large downloads, server-sent events, and any response whose length is unknown when the first byte is ready. - Chunked transfer-encoding on the way out, correctly framed and correctly terminated, because a truncated chunked response is indistinguishable from a network failure to the client and corrupts keep-alive for the connection.
- Chunked request bodies on the way in — carefully.
internal/parse.wodeliberately refuses them today, with a correct note that silently treating a chunked request as body-less is request smuggling. That refusal is good engineering. It may only be lifted by an implementation that handles the smuggling cases explicitly, and the refusal must remain the behaviour for anything the parser is not certain about. - An explicit commit point. Once the first byte is written, the status and
headers are gone and no
aftermiddleware can change them. That is a real semantic change to the middleware contract and it has to be stated, not discovered — theafterchain currently runs on every response and security headers depend on it.
Phases
Phase A — the writer seam
- Decide the shape (fork 1) and introduce a way for a handler to emit body
bytes progressively instead of returning a complete
Resp. Handlers are classes, so this is a second interface besideHandler, not a callback. - Keep the existing whole-response path as the default and unchanged: the overwhelming majority of responses are small and should not pay for this.
- Verify: the two paths coexist; every existing response is byte-identical.
Phase B — chunked responses
- Chunk framing, the terminating zero-length chunk, and the interaction with keep-alive — a connection whose chunked response was truncated must be closed, not reused.
Content-Lengthand chunked are mutually exclusive;serialize()must pick one and never emit both.HEADon a streaming route: headers only, and decide whatContent-Lengthclaims when the length is unknown.- Verify: a chunked response reassembles byte-exactly; a mid-stream trap closes
the connection rather than leaving a half-frame;
HEADis coherent.
Phase C — the commit point and the middleware contract
- Define and enforce when headers are locked. An
aftermiddleware that tries to mutate a committed response must fail loudly in development rather than silently doing nothing. - Decide what happens to
SecurityHeadersandCors— both areaftermiddleware and both must still apply to streamed responses, which means they have to run before the commit for those routes. - Verify: security headers and CORS are present on a streamed response; a post-commit mutation attempt is reported.
Phase D — chunked request bodies
- Only if phase C is clean. Parse chunked request bodies with the smuggling
cases enumerated and tested: both
Content-LengthandTransfer-Encodingpresent, duplicatedTransfer-Encoding, unknown transfer codings, and oversized or malformed chunk sizes. - Every ambiguous case stays a 400-and-close, matching the existing duplicate
Content-Lengthdiscipline. - Verify: a well-formed chunked upload arrives intact; every enumerated smuggling shape is refused.
Phase E — the gate and the ledger
- A streaming route in a sample, gated on both consumers, plus a large-body leg proving memory does not scale with response size.
- Retire the README's parked streaming rows; record the commit-point semantics where a handler author will find them.
- Verify:
just web-app,just site,just linkcheckgreen; ASan clean.
Acceptance Criteria
- Given a streaming handler emitting N chunks, when a client reads the response, then the reassembled body is byte-exact and the framing is well-formed.
- Given a response far larger than the arena, when it is streamed, then it completes and peak memory does not grow with the body — the criterion that distinguishes streaming from buffering.
- Given a handler that traps mid-stream, when the failure occurs, then the connection is closed rather than reused, no half-frame is left behind, and the server survives.
- Given a streamed response, when it leaves, then the security and
CORS headers the
afterchain contributes are still present. - Given an
aftermiddleware attempting to change a committed response, when it runs, then the attempt is reported rather than silently dropped. - Given a
HEADrequest to a streaming route, when it is answered, then the headers are coherent and no body is sent. - Given a request with both
Content-LengthandTransfer-Encoding, when it is parsed, then it is refused with 400 and the connection is closed — smuggling is refused, never guessed at. - Given every pre-existing non-streaming response, when both serving gates run, then output is byte-identical to before this iteration.
Out Of Scope
- SSE — iteration 7, the first consumer.
- Compression — also 7; it composes with chunking and should not be entangled with building it.
- Byte ranges and
SendFile— iteration 8. - Request-body backpressure as a general mechanism. Reading a body slowly to push back on a producer wants cancellation, which porch does not have and which language iteration 31's actor lifecycle owns. This iteration streams out and parses chunked in; it does not add flow control.
- WebSockets. Already shipped (
ws_accept,wsframe) and deliberately a hijack that bypassesserialize()— that path must keep working untouched, which is worth an explicit regression check. - HTTP/2. Proxy-terminated by doctrine, and parked behind language iteration 23 regardless.
Info
Forks the spec must settle:
- What is the writer? Candidates: a second interface whose method is
called repeatedly until it signals done; a
Respvariant carrying a producer object instead of aTextbody; or a handler that receives the connection and writes directly (which is whatws_acceptalready does via the 101 hijack sentinel). The third is the least new machinery and the most footgun. The first fits the no-closures doctrine best, since a producer is just another class with fields. - Does the
afterchain still run for streamed responses? It must, or security headers regress. But it cannot run after the body. So eitherafterruns at commit time for streaming routes, or streaming routes declare they opt out and the framework refuses to combine them with header-mutating middleware. Silent partial application is the one unacceptable answer. - Is chunked request parsing in this iteration at all? It is separable and it is the riskiest security surface in the framework. Splitting phase D into its own iteration is a legitimate outcome of the brainstorm — the study is explicit that the current refusal is correct, so there is no pressure to rush it.