writeonce/docs/stories/porch/05-routing-response-ergonomics.md
shoney.arickathil 1fe808b7a4 docs(stories): add readiness, retire status: refine, sweep all 47 iterations
- `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>
2026-08-27 16:54:45 +02:00

143 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
track: porch
iteration: "5"
status: pending
readiness: refine
---
# porch 5 — routing and response ergonomics: the parity that is merely missing
> Part of [Story — `porch`, the writeonce web framework](00-story.md).
> Source: [the Fiber parity study](../../plan/exploration/fiber/00-fiber-parity.md) §5.
>
> Independent of iterations 2–4 and of the streaming seam — startable at any
> time, and a reasonable slice to interleave when the risky work needs a break.
> Nothing here is hard; all of it is felt.
## Goals
- **The rest of the method helpers.** `App` has `get`/`post`/`put`/`delete_`.
A `Route { method: "PATCH" }` literal already works, so this is registration
ergonomics rather than capability — but writing the literal by hand for
`PATCH` while `get` exists is the kind of asymmetry that makes a framework
feel unfinished. Add `patch`, `options`, `head`, and an `all`.
- **Named routes and reverse routing.** Fiber has `Name()` and `GetRouteURL()`.
porch has neither, so every link in the site is a hand-written string that no
compiler checks — and the site is exactly the app where a renamed path breaks
a page silently.
- **A per-route body limit.** `BODY_MAX = 1048576` is one compile-time constant
for the whole server. An upload route and a JSON route want different numbers,
and the JSON route wants a much smaller one than the upload route can live
with.
- **Request ids.** `req.ctx` already exists to carry one; there is no generator
and no middleware. With iteration 2's builtin available this is a few lines,
and it is the difference between logs you can correlate and logs you cannot.
- **The response helpers written by hand today.** `Location`, `Vary`,
`Attachment`/`Download`, and a content-negotiated `format` dispatch on top of
the existing `accepts()`. Also q-value *ranking*, which the ledger has carried
as a known 🔶 since the negotiation slice landed.
## Phases
### Phase A — method helpers and route introspection
- The missing registration helpers, including `all`, and decide whether `head`
auto-registers alongside `get` (Fiber has `DisableHeadAutoRegister`, which
tells you the default is auto and that people want it off).
- Route introspection — list the table — because it is nearly free once routes
are already a `multi Route`, and it is what makes a startup banner or a
route-dump flag possible.
- Verify: each method dispatches; `405` still carries a correct `Allow` built
from the real table; existing routes unchanged.
### Phase B — named routes and URL building
- A name on `Route`, a lookup, and a builder that fills `:param` captures.
- Decide the failure mode for a missing or extra parameter. A silently wrong URL
is worse than a trap, and this is a compile-time-checkable shape only once
language iteration 29's `@derive` exists — so for now it is a runtime check
and should say so.
- Migrate the site's internal links onto it, which is the proof it is usable.
- Verify: every site link resolves through the builder; a wrong parameter set is
refused loudly.
### Phase C — per-route body limits and request ids
- Move the limit from a module constant to route-level configuration with the
current value as the default, so no existing app changes behaviour.
- A request-id middleware writing into `req.ctx`, and settle whether an inbound
header is trusted (fork 2).
- Thread the id into the logging middleware's output, since a request id nothing
logs is decoration.
- Verify: an oversized body is refused per-route; the id appears in logs and is
stable across a request's lifetime.
### Phase D — response helpers and negotiation ranking
- `Location`, `Vary`, `Attachment`/`Download`, and a `format`-style dispatch
choosing a builder from `accepts()`.
- Rank q-values properly instead of stripping them, retiring the ledger's 🔶.
- Verify: `Vary` accumulates rather than overwrites (which the iteration-2
repeated-header work makes possible); negotiation picks the highest-q match,
not the first.
### Phase E — the gate and the ledger
- Both serving gates, the ledger rows, the board entry.
- Verify: `just web-app`, `just site`, `just linkcheck` green.
## Acceptance Criteria
- **Given** a route registered with each new helper, **when** the matching
method arrives, **then** it dispatches; **and** an unmatched method still
yields `405` with an `Allow` listing exactly the registered methods.
- **Given** `head` auto-registration, **when** a `HEAD` request hits a `GET`
route, **then** the response is headers-only with the `Content-Length` a `GET`
would have sent — the behaviour `serialize()` already implements, now
reachable by registration.
- **Given** a named route with `:param` captures, **when** a URL is built with
the right parameters, **then** it matches that route's pattern exactly;
**and** a wrong or missing parameter is refused rather than producing a
plausible-looking wrong URL.
- **Given** two routes with different body limits, **when** a body exceeding the
smaller arrives at each, **then** it is refused at the small route and
accepted at the large one.
- **Given** no per-route limit, **when** a request arrives, **then** the
previous global limit applies unchanged.
- **Given** a request-id middleware, **when** a request is handled, **then** the
same id appears in every log line for that request and in the response header.
- **Given** an `Accept` header with q-values out of order, **when** negotiation
runs, **then** the highest-q acceptable type wins — not the first listed.
- **Given** two `Vary` contributions from different middleware, **when** the
response leaves, **then** both appear.
## Out Of Scope
- **A radix-tree router.** Path matching is a linear scan and the ledger marks
it 🔶 pending a *measurement*. Language iteration 22 built the benchmark
harness but pointed it at the database. Until someone benches the router, this
is an optimisation without evidence.
- **Case-insensitive or strict-slash routing.** Fiber exposes both as config.
porch is case-sensitive and lenient; changing that is a behaviour change for
existing apps and wants its own decision.
- **Compile-time-checked URL building.** The typed version needs language
iteration [29](../language-runtime-database/29-compile-time-metaprogramming.md).
Runtime-checked now, upgraded later.
- **Streaming responses, `SendFile`, byte ranges** — iterations
[6](06-streaming-core.md) and [8](08-static-and-lifecycle.md).
- **Typed binding of params into a class** — language iteration 29 again.
## Info
Forks the spec must settle:
1. **Does `head` auto-register?** Fiber's default is yes with an opt-out. Auto is
friendlier; explicit is more predictable and never surprises someone
debugging why a route they did not register is answering.
2. **Is an inbound request-id header trusted?** Behind the mandated proxy,
trusting it is what makes tracing work across hops. On an open port it lets a
client forge correlation ids and poison logs. `client_ip` and `net.peer`
already exist for exactly this trust decision — reuse that conclusion.
3. **Where does a route's body limit live?** A field on `Route` is the obvious
home but widens a record that the conformance corpus pins the ownership shape
of. Check that fixture before choosing.