Commit graph

24 commits

Author SHA1 Message Date
aa13b2125f refactor(porch-store): re-scope porch 1 to the limiter, revert idempotency
- idempotency built, reviewed, then reverted WHOLE to the tag
  archive/porch-idempotency. Not a design failure: it passed its gates.
  It provokes a C-runtime SIGSEGV in wo_arena_alloc/wo_str_new under
  concurrent call()-parked callers
- the evidence for that attribution: over ten gate runs every failure
  was an idempotency leg and none was the limiter's, which drives the
  same pool through the same call/park machinery. The begin arm has 5x
  the allocation sites inside receive and moves a whole Req plus a
  Handler through the mailbox
- before the split the suite reported 0 to 6 failures run to run; after
  it, five consecutive runs at 56 checks, 0 failures
- PoolMsg loses digest/req/handler, and NullHandler/dummy_req/fresh_req
  go with them — every rate-limit count used to allocate a throwaway
  Req it never read
- IdempotencyKey is KEPT and commented: the schema is settled and the
  digest-as-column decision cost a review round to get right
- the limiter's saturation 503 has no leg of its own now (§19 drove
  Idempotent). Stated in the README rather than papered over — a
  deterministic leg needs a slow actor, and only the reverted arm was
- new: porch 9 (idempotency, on hold) and language 41 (the arena crash,
  with the reproduction harness and the evidence that localises it)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 79e6da4465133dc555e913c960d544ef1c7bedd8)
2026-09-15 01:25:00 +02:00
ad1ad8361c docs(audit): fix stale docs against the code (TLS, net.connect, RNG, lang-41); jarvis deps
Code is the source of truth; these claims no longer matched runtime/src:

- "net.connect does not exist" — landed 2026-09-07 (id 110); net.connect_tls /
  read_tls / write_tls (115-117) + net.accept_tls (118), WO_B_MAX 118. Fixed
  in jarvis 00-story (problem statement + architecture + out-of-scope), porch
  00-story (proxy middleware row), rv2 7 (push-collector fork), 00-code-review
- "TLS: none / proxy-mandated forever" — retired by rv2 9 (in-process TLS both
  directions). Fixed in porch + web-app + site example READMEs (proxy is now a
  deployment choice; HSTS row), 00-code-review
- "no RNG anywhere in the runtime" — imprecise: the runtime has a getrandom(2)
  source since rv2 9 (TLS ephemerals), but nothing exposes it to .wo yet.
  Fixed in CODE-LOGIC (digests), lang 34, porch 2, status lang-39 row
- "porch 9 blocked on language 41" — lang 41 fixed 63065ff. Fixed in porch 1,
  jarvis 00-story, status NEXT PLAN, dependency graph (L41 done, P9 ready)
- dependency graph §7 rewritten: the runtime side is done; jarvis 1 waits only
  on porch (developer's porch-first order). Adds jarvis 1's dependency table +
  the build order that satisfies it
- 00-code-review: a dated 2026-09-09 re-verification appended (record kept)
- site README lives in the writeonce-site submodule: committed there, pointer
  bumped here

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit f1049dd9b7c7770da28bcabfc1cb1324621e7ee6)
2026-09-15 01:16:13 +02:00
484402a156 docs(porch-store): correct the one-slot-pool advice, soften a gate-proof claim
- bullet 2 wrongly told app authors to hold a bare actor handle and
  re-wrap it as a forced ONE-slot Pool per connection -- that was my
  own advice, not the previous implementer's, and the reviewer showed
  WO-E222 fires on the class Pool, not on multi PoolSlot
- rewritten around the new pool_slots/pool_of pair: make_pool(n) once
  at process start, multi PoolSlot held directly in connection-actor
  state, a transient Pool rebuilt per use -- and states explicitly that
  calling make_pool per connection restores the lost-increment race
- disclose that the gate's own ConnWorker fixtures still build a
  deliberate one-slot Pool per leg, so no leg yet exercises real
  N-actor sharding through pool_slots/pool_of
- soften the rate-limiting row: saturation-503 is gate-proven only via
  Idempotent/pool_begin, not through Limiter's own try/catch arm

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 6d48dbca98d4ea4c6d93f470ae3aad0b00acd2a1)
2026-09-15 01:15:30 +02:00
03a7ad6658 fix(porch-store): log a genuine pool_count trap, not just 503 silently
- catch (e) nil could not distinguish a real store failure (e.g. a
  mod-by-zero from Pool { actors: [] }) from ordinary saturation
- print_err the trap message before answering 503, matching the same
  fix in idempotent.wo's pool_begin catch

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit c53ad583051abeeeb302301fc3d91073f0a15fcc)
2026-09-15 01:15:30 +02:00
359d21a57f fix(porch-store): widen idempotent replay headers, real per-conn sharding
- stored/replayed headers widen from content-type only to an allowlist
  (content-type, location, etag, cache-control), matched case-insensitively
  -- a redirect() lost its Location on its own first response, not just replay
- add pool_slots(Pool) -> multi PoolSlot and pool_of(multi PoolSlot) -> Pool
- Pool is demand-promoted to traced (WO-E222) and can't live in actor
  state; PoolSlot/multi PoolSlot never is, the same shape chat/main.wo's
  Room already holds directly -- this is what lets an app actually shard
  across N actors per connection instead of a forced one-slot pool
- log a genuine pool_select trap instead of silently folding it into 503
- fix stale comments: the prune below IS a delete-then-insert (of a
  fresh row, not the same one) contradicting the doc comment above it;
  the catch shape referenced in two comments had changed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b738269314f01a95dee1341437c7661ce9e28730)
2026-09-15 01:15:30 +02:00
899f2c604e fix(porch-store): idempotent key_header must be matched case-insensitively
- normalise self.key_header via to_lower before the req.headers lookup
- req.headers keys are already lowercased on read (internal/parse.wo);
  the documented key_header: "Idempotency-Key" never matched, silently
  disabling idempotency (falls through to inner.handle) on every request
- key/digest lookups use the normalised name consistently
- digest now always includes method+path, body appended only when
  include_body is set -- a bare "" digest under include_body:false
  previously matched any other request reusing the same key
- log a genuine pool_begin trap instead of silently folding it into 503
- update the two doc comments describing the old, unsafe shape

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 91099cfbb2fb9a2d351894e444fed306b25557d5)
2026-09-15 01:15:30 +02:00
84cfbb1885 docs(porch-store): saturation gate leg closes out porch 1
- Add saturation leg (scripts/web-app-accept.sh): one-actor pool,
  WO_MAILBOX=2, 15 concurrent requests, exactly 3 served + 12 answer
  503; execution count matches the 200 count, retry-after + real
  cause verified on the 503s
- Guard make_pool(n<1) by clamping in make_pool itself, not
  pool_select's division -- that trap runs inside the middleware's
  own try/catch and would be swallowed as ordinary saturation forever
- README: rate limiting + idempotency ledger rows moved to done,
  scoped to what the gate proves; documented Handler-decorator
  shape, Pool aliasing (WO-E222), call's scalar-only reply (WO-E226),
  pool size as a capacity decision
- Story: Progress table filled with real hashes, 7/9 acceptance
  criteria marked verified with citations, 2 marked verified by
  construction (never gated even in the original plan), status: done
- Status board: standup entry, porch 1 pending row updated
- Recorded a pre-existing runtime hang (main() returns cleanly, OS
  process sometimes hangs under concurrent call()-parked callers)
  that also reaches the new leg's teardown; contained with kill -9
  rather than asserted, so it can't flake the leg's actual subject

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 21934b18910070a3f24b8bd4367fcb9d397dc1fb)
2026-09-15 01:15:30 +02:00
659ea26582 fix(porch-store): delete the ephemeral nonce row after one read
- The nonce naming an ephemeral (4xx/5xx) row is handed to exactly one
  call() reply and nowhere else -- no other message can ever construct
  that key, so idempotent.wo deleting it right after building the Resp
  is safe by construction (unlike the earlier shared bare-key row,
  which a second message COULD reach and made deleting it racy)
- Closes the leak AND a real correctness edge: the nonce is
  time.ticks() % 1_000_000_000, wrapping every ~1000s -- with rows kept
  forever, a later failed attempt on the same key could land on the
  same nonce and either collide with the unguarded insert or resurface
  a stale replay, exactly what rounds 1/2 removed
- Gate leg 18f: N ephemeral attempts against the same key must return
  IdempotencyKey's row count to baseline, not grow it by N -- confirmed
  failing (baseline+N) against the pre-fix code, passing after
- N picked at 3: the pre-existing runtime hang/segfault (out of scope,
  being tracked separately) reproduces more often at higher sequential
  insert+delete volume against the same key; 3 stayed clean across
  many runs while still proving the property precisely

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 9ad594748e01a665ccaf29733a48c2b83a2749da)
2026-09-15 01:15:30 +02:00
e296d0541d fix(porch-store): close the ephemeral-row race, not just shrink it
- Root cause of the residual: a 4xx/5xx row lived under the bare key,
  so a second message could delete-and-replace it before the FIRST
  caller's own middleware-side read (necessarily outside receive,
  WO-E226) ever ran -- the owner itself could read back a LATER
  message's answer, not just a duplicate reading a stale one
- Fix: a 4xx/5xx miss is never stored under the bare key at all. Each
  such attempt gets its own row, keyed by a nonce carried back in the
  scalar reply's low digits, so no other message for the same bare key
  ever touches it -- the decision AND the row's identity are both
  fixed inside the one serialized receive call
- Disclosed trade-off: that row is never revisited by a bare-key
  lookup, so it is never TTL-pruned either -- permanent per failed
  attempt, the same no-sweeper trade-off this codebase already makes
  elsewhere, not a new one
- Gate leg 18e: reran 20x in isolation against the fix with zero
  500-500 or 200-200 outcomes (was reproducible before)
- §18's SIGTERM-stop check now force-kills on timeout before clearing
  $SRV, instead of matching §14/§17b's own gap where a still-running
  process escapes the exit trap too -- an orphan no longer survives
  past this leg regardless of the assertion's own outcome

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 464147a9ddf3a53cb1946637c3f517ba615ee358)
2026-09-15 01:15:30 +02:00
d98ff82027 fix(porch-store): never replay a cached transient 5xx
- Miss path only marks a response a durable replay target (outcome 1)
  when status is 2xx/3xx; a 4xx/5xx gets outcome 3 instead
- Outcome 3's row is a one-shot relay: the scalar reply still can't carry
  a Resp (WO-E226), so the row exists only to hand the exact response
  back once, then idempotent.wo deletes it -- a retry with the same key
  is a genuine miss and re-executes, instead of caching a 500 for the
  24h default TTL
- Reviewer finding: caching any status meant a transient failure was
  replayed verbatim until TTL expiry, worse than no idempotency at all
- Gate leg 18d: FlakyHandler fails once then succeeds; same key twice
  must answer 500 then 200 -- confirmed failing (500, 500) before the
  fix, passing after

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit e61015f2065a7c6aec6f5c2439e78b53f796bab3)
2026-09-15 01:15:30 +02:00
c97de237ef feat(porch-store): idempotency rebuilt so the actor runs the handler
- Delete before/after flow: it stored on a miss, so two duplicates both
  missed and both ran; its 10s "in flight" check fired on fast legit
  replays and never on a real collision
- Idempotent now wraps the route's Handler and hands request + handler to
  the pool; a duplicate waits in the actor's mailbox, not a held reply
- keypool.wo kind-2 arm: digest match replays, mismatch refuses (422), a
  miss runs the handler inside receive and stores status/body/
  content-type, all via the same pool_pack(count, remaining_ms) scalar
  kind-1 uses (WO-E226 forces one return type)
- Outcome codes start at 1, never 0: idempotent.wo's try/catch cannot
  tell a literal 0 reply apart from a trapped call
- fresh_req() copies a borrowed Req's map fields into a new Req before it
  crosses the actor boundary (WO-E222: aliased graphs can't cross heaps)
- Reading a stored row back forces fresh Text via `.. ""` on every field
  copied out of json.decode's result -- decoded Text does not survive
  being handed onward once the decoded record goes out of scope
- insert is unguarded (kind 1's own convention): a swallowed failure
  would answer "stored" for a response never written
- web-app-accept.sh: leg 18a/b/c -- byte-identical replay off an ExecMark
  row count, digest mismatch is 422, genuinely parallel duplicates run
  the handler exactly once

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit eae1b06cdc38e4766d4e27a66b02264f47164e99)
2026-09-15 01:15:30 +02:00
37a63192c6 fix(porch-store): trust_proxy falls back to net.peer on an absent XFF
- limiter_key: an empty client_ip(req) under trust_proxy no longer keys
  on the literal "ip:" -- falls through to net.peer(req.conn) instead,
  same as the untrusted-default path
- the bug: every client omitting X-Forwarded-For shared ONE bucket,
  so one could exhaust it and deny/hide the rest
- curl availability check added alongside the existing woc/wovm check
  (the limiter gate legs drive the server with it)
- new gate leg: LIMIT+1 sequential no-XFF requests must all be 200
  (own key per connection, via a fresh ephemeral port each time) --
  confirmed it fails against the pre-fix code (6th comes back 429)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 831e9d8e6b1ef39cd938783dbc47c1abe6d53211)
2026-09-15 01:15:30 +02:00
491c2f42b4 feat(porch-store): limiter delegates all counting to the key pool
- delete all RateLimitCounter access from limiter.wo: query, increment,
  delete-then-insert, and the swallowing catch (e) nil -- the pool is now
  the only writer, so its serialization guarantee actually holds
- Limiter gains pool/limit/trust_proxy fields; before() calls pool_count
  and acts on the Verdict; make_limiter takes a pool
- key selection: req.principal first, else trust_proxy ? client_ip(req)
  : net.peer(req.conn); delete the dead req.ctx["verified_proxy"] branch
- 429 on a spent window (Retry-After, X-RateLimit-*); 503 + Retry-After
  on a caught actor trap (saturated pool), request never let through
- add Limiter.after(), registered alongside before() as both Mw and Aw
  (Cors's own shape) so the allowed path's X-RateLimit-* headers reach
  the response, not just req.ctx
- scripts/web-app-accept.sh: three new gate legs -- threshold (N pass,
  N+1th 429), SIGTERM+restart (still limited from the WAL), and N
  genuinely-parallel curl clients on one key with an exact-count assertion

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a653dd0aa64711c43461126d32b4632cc8f64c7a)
2026-09-15 01:15:30 +02:00
9af42c8e69 fix(porch-store): correct reset_at unit on the two fresh-window paths
- keypool.wo:78,89 passed msg.window (µs) straight into pool_pack's
  remaining_ms (ms) parameter on the first-hit and post-prune-reset
  paths; the third call site already divided by 1000 and was correct
- fix: pool_pack(1, msg.window / 1000) at both sites — a 60s window
  no longer reports reset_at ~16.7h away
- count/allowed were unaffected (computed independently); this only
  hit the client-visible reset instant, on the two most common cases
  (new key, window rollover)
- extended gate leg 16 to assert reset_at falls within a 5s band of
  time.now() + window_ms, not just on count — verified the assertion
  itself by reverting the fix, confirming leg 16 failed with the
  exact defect shape, then restoring it and confirming green
- woc docs/examples/porch/ exits 0; web-app-accept.sh: 47 checks,
  0 failures

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 153fd295d37905dd083823536c6781843699e455)
2026-09-15 01:15:30 +02:00
4be826f9ab feat(porch-store): key pool actor for serialized per-key counting
- add docs/examples/porch/middleware/keypool.wo: one PoolMsg (kind 1 =
  count, kind 2 = begin placeholder for task 4), a Verdict class, a
  fixed-size actor pool with a byte-sum-mod-N selector
- KeyActor.receive implements kind 1: reads the row, writes the new
  count via field assignment (writes through, never delete+insert),
  prunes a fully-elapsed window's row instead of resetting it
- window arithmetic on time.ticks(); reset instant sent back is built
  from time.now() only
- call's reply must be a copyable scalar (WO-E226), so the count and
  remaining window time are packed into one Int by the actor and
  unpacked into Verdict by pool_count — the packing stays inside this
  file, callers only ever see Verdict
- gate leg in scripts/web-app-accept.sh: a flat copy of porch (manifest
  stripped) with a driver dropped beside keypool.wo asserts two
  sequential counts return 1 then 2; verified failing (E403, make_pool
  undeclared) before this file existed, passing after
- woc docs/examples/porch/ exits 0; full web-app-accept.sh: 47 checks,
  0 failures

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 676e651d808ef2c3619f88c3808adc372cd0be6c)
2026-09-15 01:15:30 +02:00
377808b936 feat(porch-store): add digest column to IdempotencyKey table
- Add digest field to store sha256(method|path|body) separately from key
- Enables detection of "same key, different body" in future tasks
- Update idempotent.wo insert to compute and store digest value
- Typechecker passes: exit 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3a9bddcd1e3c11f5b371ce54cafc373685ca08b6)
2026-09-15 01:15:30 +02:00
aee5adddc4 fix(porch-store): add the missing use json import
- docs/examples/porch/ did not typecheck: WO-E403 "cannot resolve the
  receiver's type for the call to `encode`" on json.encode
- every other example that calls json.encode/decode imports it; this
  file did not, so the whole porch library was uncompilable on dev
- woc docs/examples/porch/ now exits 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 5c3544d524fa98dbb7a363600cd2eeb6dd1badac)
2026-09-15 01:15:30 +02:00
bf343045ab feat(porch-store): Idempotent middleware (Phase C, in progress)
- replays a stored response for a repeated Idempotency-Key: before()
  checks the key, after() stores status/body/content-type on a 2xx/3xx
- key is "idem:<header>:<value>", optionally plus a sha256 digest of
  method|path|body when include_body is set
- 409 while a key is in flight (stored within the last 10s), lazy TTL
  expiry on access, default 24h
- replay allowlists content-type only — never Set-Cookie or Date
- backed by IdempotencyKey from Phase A (519d411)

Written by a parallel session and committed here as-is because its
branch was consolidated away. NOT verified: it calls json.decode and
json.encode without a `use json` import, which every other example that
uses json has. Left unedited rather than fixed blind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit aee79264095eea3b2c38c92789c44b31f1c9ef8a)
2026-09-15 01:15:30 +02:00
e159b5a754 feat(porch-store): Limiter middleware (Phase B)
(cherry picked from commit 5b1e82ab4bf3bad39bb6762a52b2dfaafd18a110)
2026-09-15 01:15:30 +02:00
192d451f5e feat(porch-store): store tables for rate limit + idempotency (Phase A)
(cherry picked from commit 519d4117fd0d6d0bd7d51f80bcb20de4d6294503)
2026-09-15 01:15:30 +02:00
8311330531 docs(db2-keys): reconcile databasev2 and porch markdown with the code
- loader's resident:keys refusal said "rows are still fully resident"
  and "until tasks 5c/5d land". Both false since f606fc9. Corrected to
  name the real blocker: UPDATE needs read-modify-append
- databasev2 00-story: the sequence graph drew 2->3->4, which reads as
  3 needing 2 and 4 needing 3. Both backwards, and it still drew the
  2->5->6 path the 2026-08-27 amendment retired. Redrawn stating only
  real dependencies, with 4 and 3 shown as composing rather than
  ordered, and the execution order that actually happened
- databasev2 03: the hazard and its Outstanding entry both claimed
  nothing fails "because iteration 2's storage half is unimplemented".
  Marked discharged, and recorded that the hazard named only half the
  danger — the bitmap walk would have dropped keys rows outright
- databasev2 06: pending -> hold (largely superseded, revisit only on
  a measurement); dated its 5c/5d references
- porch 01: rewritten to the settled shape. readiness ready, status
  in-progress, phases B and C marked superseded with why
- porch 01 claimed time.after "is still a reserved builtin id". False —
  builtin 90, implemented. That claim is what made the iteration look
  cheaper than it is
- porch README gains honest ledger rows for both features (partial,
  being rebuilt), not shipped
- skill-catalog README pointed at a story path that moved tracks;
  linkcheck now 0 broken

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit b3d8c403e1d19ac27ec966de85cb293e0765795c)
2026-08-30 20:36:55 +02:00
62d29d6a77 docs(24): T10 closeout — stories done, board, graph, ledger, CODE-LOGIC
Iteration 24 closes, absorbing 31 and 34. No code in this commit.

- stories 24, 31, 34 -> `status: done`, each with a landing banner. 24's
  records the gate numbers and BOTH disclosed deviations: monitor takes
  three arguments (the caller may be `main`, which has no mailbox) and a
  v1 `call` reply is a typed scalar (which is what let the agreement be
  checked at compile time, WO-E226). 31's notes it landed INSIDE 24 and
  that a fifth mechanism it never anticipated came out of proving the
  gate — the drain guarantee (40). 34's names the gap it did NOT close:
  still no RNG, so CSRF/sessions stay blocked
- board: in-progress row cleared, marker doc deleted (convention), the
  standup entry in the six-question shape, chain note — next link is
  databasev2 4 (io_uring group-commit, chain 5)
- graph: PUBSUB2 (pub/sub + WebSockets, "rejected until here") -> done
- porch ledger: a WebSocket/pub-sub row added; the cancellation row now
  says what it actually waits on rather than repeating "the arc"; the
  README's "no WebSockets/SSE" limitation was stale — WebSockets are
  supported, SSE and chunked encoding are not
- CODE-LOGIC: runtime/src gains the actor-lifecycle section (call, death,
  the cap counter's sender/home-thread split, the monitor walk, the timer
  list), the drain guarantee, and the digest section; docs/examples/chat
  gains its own — actor topology, WHY two actors per connection, fd
  ownership, and the shutdown choreography

Battery after the doc edits: wovm-test 36 suites 0 fail, woc-test exit 0,
oop-e2e 119/0, chat 11/0, web-app 46/0, linkcheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 00:06:31 +02:00
01df75245f docs(porch): give the framework its own story track, iterations 1-8
- docs/stories/porch/ — a TRACK folder, not a status folder: status still
  lives only in frontmatter. Adds `track: porch` so a query over
  docs/stories/ can tell a porch 3 from a language 3
- 00-story.md carries the sequence, the dependency graph, and a table of
  what the track explicitly does NOT own (binding -> 29, cache -> 18,
  proxy -> 38, metrics -> 30, TLS/templates -> doctrine)
- eight iterations, each with phases, per-phase tasks, Given/When/Then
  criteria, out-of-scope and the forks a spec must settle:
  1 store-backed middleware (limiter + idempotency — needs nothing new,
    first on purpose so the store pattern is proven cheaply)
  2 randomness + cookies (phase A is language-track: a CSPRNG builtin;
    `Resp.headers` being a map cannot emit two Set-Cookie lines)
  3 sessions   4 CSRF   5 routing/response ergonomics (independent)
  6 streaming core (the seam 7 and 8 wait on; chunked-request refusal
    must survive)   7 SSE + compression   8 static + lifecycle hooks
- language iteration 39 -> status: hold, retitled superseded, with a row
  mapping each of its goals to the porch iteration that took it. Kept, not
  deleted: the Fiber study cites it and its randomness argument is what
  this track is built on
- board gains a porch section; board-views gains porch and both-track
  Dataview queries; porch README and the Fiber study §7 point at the track
- no code blocks in any story (plans carry concept and actions in words);
  linkcheck 0 broken / 0 anchors

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 20:17:33 +02:00
ffd791d05b refactor(porch): name the web framework porch, fix the wo.toml identifier claim
- docs/examples/writeonce-serve -> docs/examples/porch (git mv, history kept);
  `[deps]` key and import are now `porch` / `porch/http` / `porch/router`
- name history preserved on the library README, not rewritten into dated
  records: writeonce-framework -> writeonce-serve (08-25) -> porch (08-26).
  Stories, specs, plans and the audit reports keep the older name by the
  repo's own convention; only live docs and every path link were rewritten
- left alone deliberately: `internal/serve.wo`, `pub fn serve`, `serve_conn`,
  `app.serve(...)` — those are functions, not the module name
- web-app/wo.toml comment corrected: it claimed hyphens are not identifier
  characters and named a key this file never used. lexer.ml's `is_ident_cont`
  DOES accept `-` (an internal dash is part of the identifier, which is why
  binary minus needs spaces), so a hyphenated key would be legal too
- site now teaches the name: package card, the two-deps chapter and the
  handlers-are-classes chapter say `porch`; site-accept asserted the old
  /packages/serve route and caught the rename, as a gate should
- gates: web-app 46/0, site 21/0, deps-accept 8/0, oop-e2e 116/0,
  linkcheck 0 broken / 0 anchors; porch typechecks entry-less as kind=library

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 19:36:34 +02:00