Commit graph

256 commits

Author SHA1 Message Date
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
7833dd5740 feat(gates): example apps log to /tmp/<example-app>.log so it can be tailed
Every gate wrote its server output into a per-run mktemp dir that its own
cleanup trap deletes on exit — nothing to follow during the run, nothing
to read after it.

- chat -> /tmp/chat.log, web-app -> /tmp/web-app.log,
  site -> /tmp/site.log, log-watcher -> /tmp/log-watcher.log
- truncated once at gate start, appended for the rest of the run, so one
  file holds the whole run in order
- each gate PRINTS the path as its first line, with the tail -F command
- legs are banner-separated and name their port and env
  (===== leg 2 - port 18902 - env WO_IO=epoll =====)

Appending breaks readiness detection unless it is leg-scoped:

- serve() used to grep the whole file for `listening`, which after the
  switch to append would match an EARLIER leg and return before the new
  server was up. It now records the line count first and searches only
  tail -n "+$LEGFROM"; the ASan scan is scoped the same way
- log-watcher's checks grep per-invocation files, so those are kept and
  the output is teed into both — process substitution adds no pipeline
  stage, so $! is still the command's pid the gate kills and waits on
- its one SYNCHRONOUS invocation appends after it finishes rather than
  teeing: the grep on the next line would race tee's flush

Verified, all green: chat 11/0, web-app 46/0, site 21/0, log-watcher 7/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 23:53:10 +02:00
d87846354e docs(board): iteration 24 is 9 of 10 and on master; only T10 closeout remains
- narrative said "five of ten tasks landed" and named the branch as the
  live location; T4/T5 (ids 89/90) had landed and the slice merged to
  master 2026-08-27 (60414a1, fast-forward)
- records what was verified ON master: chat 11/0 at the full 1000-client
  soak, runtime 36 suites 0 fail, compiler 556 checks, corpus 119 checks
- T10 closeout is what still holds stories 24/31/34 open

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 23:44:56 +02:00
60414a1754 feat(runtime): the shutdown drain guarantee — iteration 40
A message sent before the stop flag is observed must be delivered and run
before the engine stops. One rule; a spin count could never express it.

- root cause in `shard_main` (runtime/src/vm.c): NEXT_RUNNABLE() already
  stated the contract — "a WORKER on stop keeps DRAINING ... so queued
  shutdown messages (close frames!) still run" — but the IDLE branch
  contradicted it, calling fib_reap_all and breaking on WO_IO_STOP,
  abandoning its inbox for wo_engine_stop() to free wholesale
- an actor between messages is exactly that idle case, which is why a WARM
  soak server hid it: warm shards held live fibers and took the right path
- fix: while the primary's drain window is open, an idle worker adopts its
  inbox and runs what arrives; sched_yield on an empty poll so a drain
  cannot burn a core per shard and starve the actors it exists to let run
- unreachable at WO_SHARDS=1: wo_engine_stop returns early at nshards <= 1

Measured:

- fresh-server SIGTERM drain: 5 of 16 failing before, 20 of 20 clean after
- `just chat` at the FULL 1000-client soak: 11 checks, 0 failures, both
  WO_IO backends, ASan clean with zero leaks
- the fd leg settled at scale too: 1000 connections left the count at 44,
  unchanged after 20 more — lazy per-shard init, not a leak
- runtime battery 36 suites (18 x both dispatch flavors) 0 fail;
  compiler 556 checks 0 fail

- story: docs/stories/language-runtime-database/40-shutdown-drain-guarantee.md
  (chain 3 with 31, status done), board row, slice marker updated
- outstanding and named: a pin below the gate needs new multithreaded test
  infrastructure — nothing in runtime/test/ drives wo_engine_start/stop and
  no corpus fixture can trigger a stop

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 23:43:45 +02:00
3bc85d85f3 docs(slice): marker reflects T4/T5 landed, the drain blocker, and the stale-artifact trap
- T4 monitor + T5 time.after landed in 4092074 (ids 89/90); marker still
  listed them pending because it came from master, which lacks that commit
- records the branch baseline (18 suites x 2 flavors, 0 fail) and the gate
  at 11 of 12 legs green
- names the stale-artifact trap: after a branch switch, compiler/_build and
  runtime/build hold the OTHER branch's binaries, and a v7-vs-v6 mismatch
  surfaces only as "no listener"
- the drain guarantee is now the single named blocker; nothing else in the
  slice should land before it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 23:28:25 +02:00
4af1e8bcdd fix(chat gate): every leg starts its own server — and it found a real bug
Gate defects, all measured:

- fd check was core-count dependent: `fds_before + 8` read LAZY per-shard
  init as a leak. Shards init on first fiber, each taking one io_uring +
  one eventfd, capped at nproc; on 20 cores the first wave legitimately
  adds 18. Measured 26 -> 44 after 20 clients, still 44 after 40 more.
  Replaced with the invariant the check is for: a second wave must not
  raise the count. Core-count independent, and catches a slow leak that
  any fixed slack would hide
- a failed leg ORPHANED its server: drain inherited $SRV from the soak
  leg, so its python died on int("") and the soak server was never
  killed — its listener then broke the next run's soak on the same port.
  drain now starts its own server; cleanup kills every server a run
  started, matched on the run's unique temp dir
- two legs the plan requires were missing: WO_SHARDS=1 (the single-shard
  control) and WO_MAILBOX=8 (drop-slow-member backpressure). Both added,
  both green. The mailbox leg shrinks the slow client's SO_RCVBUF so it
  needs no sleeps
- chat adopted the porch naming (use porch/..., [deps] key) after the
  rename landed on master

Decoupling the legs exposed a REAL drain bug, traced and documented in
docs/2026-08-27-chat-drain-finding.md, NOT fixed here:

- on a FRESH server the SIGTERM drain is flaky: 5 of 16 runs left a
  client at EOF with no close frame and no diagnostic
- traced: main -> Registry -> Room -> Writer. Registry runs (diag
  confirms), the Room NEVER processes its shutdown message, so the
  Writer's close branch never runs. Clients that do get a frame are
  saved by their own Reader seeing env.stopping()
- ruled out: the spin budget (a 1s wall-clock deadline still failed 2 of
  12 — reverted, it fixed nothing and cost 1s per shutdown),
  dummy_writer() spawning during shutdown, and write failure
- the fix is an engine guarantee — a send issued before the stop flag is
  delivered — which belongs to the actor lifecycle, not a spin count

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 23:27:46 +02:00
ebc3522c40 Merge branch 'master' into chat-ws-lifecycle 2026-08-27 23:11:49 +02:00
746dc2b42b docs(databasev2): third track — the database beyond RAM, with per-table storage modes
- docs/stories/databasev2/, numbered from 1. Six PENDING database iterations
  moved from the language track and renumbered, keeping the old id in
  `was_language_iteration:` so a search for "iteration 32" still finds it:
  32 -> 3 WAL checkpoint, 23 -> 4 io_uring commit, 33 -> 7 single-file store,
  27 -> 8 query grammar, 20 -> 9 cross-program, 21 -> 10 keypair auth.
  Done work (9, 9b, 22) stays as v1 history; language 18 left whole
- the problem, read off the engine not guessed: rows are malloc'd slabs with
  addresses stable forever, NO eviction/spill/paging anywhere in database/src,
  the WAL never checkpoints so boot replays all history, and durability is one
  process-global WO_DATA so no table can say it matters more than another.
  An allocation failure IS a clean catchable WO_T_OOM — but swap thrash
  arrives first and carries no error signal at all, which is the real hazard
- four new iterations:
  1 measure the ceiling FIRST (curve not cliff; the three exits; kill -9 at
    exhaustion) — every later default should follow from a number
  2 `@table(mode: ram | durable | cold)` — the grammar ask. Small surface
    (Ast.table_cfg gains a key, the parser already rejects unknown args), big
    semantics: `durable` defaults so nothing changes silently, and the
    compiler refuses a durable row holding a `ref` into a ram table
  5 bounded tables + refuse/evict/back-pressure, shedding BEFORE the OS acts
  6 cold tiering — mostly forks, incl. whether the language surfaces the
    fault cost and whether @unique on cold is refused outright. A paged
    B-tree stays rejected: if tiering needs one, reject tiering
- 39 links repointed, link TEXT renumbered to track-local ids; arc gains one
  pointer row replacing the six moved; board + board-views cover three tracks
- linkcheck 0 broken / 0 anchors; no code blocks in any story

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 20:52:48 +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
c0b0dbb846 docs: audit all markdown against the code, fix findings, flatten status folders
- README: shipped concurrency/HTTP/WebSockets sat in the roadmap as "not yet
  available"; "no package manager" contradicted [deps]; the deps example
  would not have compiled (the key IS the module name)
- runtime/README: leads with wovm, wo-rt.c demoted to a historical section;
  dropped 2 nonexistent recipes, crates/rt, @gc refcounting, 13 suites -> 18
- employee + log-watcher READMEs claimed "does not compile"; both are gates
- error catalog: +10 emitted codes incl WO-E250, the only diagnostic the
  shipped query surface raises; recorded why the sweep rotted
- language-surface: group-by parses, then the typechecker refuses it
- 00-code-review + 00-link-audit re-run; history kept, not rewritten
- 48 dead Rust-era exploration links de-linked rather than re-pointed (their
  prose names the retired plan by number); successor map -> discarded.md
- 08-project-structure: compiler/plan/ never existed; corpus has 9 dirs, 5 empty
- releasing.md: dropped a --draft step the workflow never had
- new docs/00-doc-audit.md: findings + disposition, incl one row where the
  audit was wrong and the doc it accused was right
- status folders removed: 34 stories flat, status only in frontmatter; 252
  links recomputed from resolved paths; board/board-views/structure retaught
- story 24 -> in-progress, since frontmatter is now the only truth
- new iteration 38: fs mutation verbs + net.connect, the two capability
  families no iteration owned
- new iteration 39: gofiber/fiber v3.5.0 parity study. The ledger called
  CSRF/sessions unblocked by iteration 34's HMAC, but the runtime has no
  source of randomness at all
- linkcheck skips .dev/.superpowers: 0 broken paths, 0 bad anchors

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 19:20:22 +02:00
b3f8ed9985 feat(site): source links to the repo, deployment layout documented
- "View source" and the nav GitHub link pointed at the user profile
  (github.com/shoneyj); both now point at github.com/shoneyJ/writeonce
- README: what actually has to reach the host — the self-contained
  binary plus dist/ (served by /dl) and data/ (WO_DATA) — and the four
  environment variables, with SITE_HOST left UNSET behind a proxy so
  the process binds loopback
- says plainly that dist/ must hold the PUBLISHED release assets: the
  build is not byte-reproducible, so a local tarball would not match
  the published .sha256 and the mirror would disagree with GitHub

Prepared and verified locally: docs/examples/site/dist/ holds the real
v0.1.0 assets (digest matches the release) and target/site serves them
byte-identically. Both directories are gitignored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 08:45:15 +02:00
8f3824409b fix(site): glibc floor is 2.35, not 2.38 — read off the release
The published v0.1.0 binaries need less than the page claimed:
woc imports up to GLIBC_2.35, wovm up to GLIBC_2.34. The page said
2.38, which was measured on a dev workstation (glibc 2.39) before CI
existed — and understating support turns working platforms away.

- supported systems: glibc 2.35+, covering Ubuntu 22.04+, Debian 12+,
  Fedora 36+
- RHEL 9 (2.34) runs wovm but not woc: build elsewhere, copy the
  self-contained binary
- say plainly that the floor is set by the machine that BUILT the
  release, which is why CI pins ubuntu-22.04
- site-accept follows the new string

This is the pinned-runner decision paying off: 2.38 -> 2.35.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 07:42:59 +02:00
3f9ce2bf32 fix(ci): do not pin dune — use whatever setup-ocaml provides
Some checks are pending
release / release (push) Waiting to run
The pinned `opam install dune.3.14.0` failed. setup-ocaml installs a
dune of its own for caching, so requesting an exact older version is a
downgrade the solver refuses — which also means run 1's
`dune: command not found` was only ever a PATH problem, fixed by
`opam exec --`.

- probe with `opam exec -- dune --version`, install only if absent
- echo the resolved version so the log says what built the release
- any dune >= 3.14 satisfies `(lang dune 3.14)`

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 07:39:51 +02:00
4f89d42948 fix(ci): install dune and build inside the opam env
First run failed with `dune: command not found`.

- ocaml/setup-ocaml provides a compiler and opam, not dune. dune is an
  ordinary opam package and this project has no .opam file for the
  action to infer one from, so nothing pulled it in
- add `opam install -y dune.3.14.0`, pinned to the version
  compiler/dune-project targets (`(lang dune 3.14)`)
- run the build as `opam exec -- ./scripts/mkdist.sh`: the script calls
  dune internally, so it needs the opam environment on PATH

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 07:17:59 +02:00
72cd510676 ci: workflow_dispatch is a real dry run
- manual runs skip the tag guard (there is no tag on a dispatch, so
  GITHUB_REF_NAME is the branch and the guard always failed) and skip
  publishing
- a dispatch now builds, verifies the digest, smoke-tests the
  extracted toolchain and reports the glibc floor, then stops
- replaces the throwaway-tag rehearsal in the checklist: no tag to
  delete, no draft release to clean up

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 06:01:26 +02:00
c47d91c153 docs(releasing): what the hosted runner costs
- public repos: standard runners free, unlimited minutes; only larger
  (4-core+) runners bill there and this workflow does not use one
- private: included minutes per plan, then per-minute; Linux x1 vs
  Windows x2 / macOS x10; each job rounds up to the next minute
- sized from a measurement: cold mkdist.sh is 3.4s on 20 cores, so
  under a minute on a 2-core runner — setup-ocaml dominates, ~3-10
  min per release, and it only runs on a tag
- release assets do not count against Actions artifact storage
- flags that rates drift; check the billing page

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 05:23:33 +02:00
3dcadefbfd docs(releasing): why the release job stays on a hosted runner
- self-hosted works technically: outbound HTTPS only, no inbound
  ports, honours HTTPS_PROXY/NO_PROXY — a box behind a proxy is fine
- but it defeats the pinned-runner decision: the build host sets the
  glibc floor, so a workstation runner (2.39 here) puts it back to
  2.38+ and drops Ubuntu 22.04 / Debian 12 / RHEL 9
- and a workstation-built release is unattested
- records what self-hosting accepts: jobs run as the starting user,
  with that user's ~/.ssh, credentials and network reach — including
  hosts named in ~/.ssh/config; worst on public repos, where a
  stranger's PR runs code on the runner
- if unavoidable: dedicated VM, unprivileged user, --ephemeral,
  segmented network, treat .credentials as a secret

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 05:16:43 +02:00
a705622f4a docs(releasing): first-time pipeline readiness checklist
- states plainly what does NOT trigger it: builds run on a
  GitHub-hosted runner, not locally, and only on a `v*` tag push —
  pushing master releases nothing
- 14 numbered steps: get the workflow onto GitHub, enable Actions,
  allow ocaml/setup-ocaml, the 403/workflow-permissions fallback,
  a --draft rehearsal on a throwaway tag, cleanup, then the real tag
- calls out that the rehearsal tag is EXPECTED to fail the tag/VERSION
  guard, and how to rehearse the full job instead
- step 8/9: read the runner's glibc floor and reconcile
  install/view.wo with it — the runner, not the dev machine, decides
  who can run the release
- lists the three likely first-run failures

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 05:04:39 +02:00
463f897e5f ci: release workflow — no gh auth login, tag-triggered
- .github/workflows/release.yml: builds, verifies and publishes on a
  `v*` tag. `permissions: contents: write` on the injected
  GITHUB_TOKEN replaces `gh auth login`; no PAT, nothing to rotate
- runs-on ubuntu-22.04 DELIBERATELY: the build host's glibc caps which
  symbol versions the binaries import, and that cap is the floor every
  user needs. 22.04 (2.35) includes Ubuntu 22.04 / Debian 12 / RHEL 9;
  24.04 (2.39) would exclude them
- guards that fail instead of publishing: tag vs VERSION, produced
  asset name vs the filename /install links, sha256, and a smoke test
  that builds a hello project with the binaries INSIDE the tarball
- reports the shipped glibc floor so the claim on /install is checkable
  from a build log
- releasing.md: pipeline route up front, manual route kept; GH_TOKEN
  recipe for non-GitHub CI

Not run — this repo has no CI history and Actions cannot execute
locally. Every guard's shell was dry-run here against the real dist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 04:58:40 +02:00
844bcc1067 docs(releasing): full gh auth login section
- gh's credential is separate from git's: SSH keys let you push but
  not call the API, so a machine that pushes can still fail to release
- the five interactive prompts and what to answer, with SSH as the
  protocol to match this repo's existing remote
- headless path: PAT scopes (classic repo/read:org/gist, fine-grained
  Contents: read and write), --with-token from a 600 file, GH_TOKEN
  for automation
- verify with `gh repo view shoneyJ/writeonce` — proves the token
  reaches THIS repo, not just that it is valid

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 04:54:57 +02:00
b11f964eec docs: release runbook — build, verify, publish on GitHub
- docs/guides/releasing.md: the steps from `just dist` to a working
  download button
- pins the constraint that matters: the asset filename and tag must
  match the URL /install links, or the button 404s
- includes verifying the tarball with the binaries INSIDE it, tagging
  the built commit, `gh release create` with both files, the web-UI
  path, and a curl check of the exact link the site uses
- notes dist/ is gitignored, the shoneyJ/shoneyj path-case difference,
  and what a version bump must touch in install/view.wo

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 04:51:14 +02:00
ef37b8ffa2 feat(serve+view): file serving, downloads, supported systems; rename
- rename the two libraries: writeonce-framework -> writeonce-serve
  (`use serve`), wo-html -> writeonce-view (`use view`). Names say the
  ROLE now; every sample, script, gate and live doc follows
- stories/specs/plans keep the old names: they are dated records, and
  both library READMEs carry a "renamed 2026-08-25" note
- serve/http/files.wo: StaticFiles { dir, max_bytes } — traversal
  refused not normalised, extension content types, attachment
  disposition for archives. Lifted out of the shop, which had said in
  a comment that it belonged in the framework
- shop drops its private copy and mounts the framework's
- site: /dl/*path over $WO_DIST (default ./dist), 16 MiB ceiling
- /install gains supported systems — Linux x86-64, glibc >= 2.38,
  not musl — read off `file` and the binaries' GLIBC_ symbol
  versions, not off a wish list; plus GitHub release as primary,
  /dl as mirror, and the sha256 verify step
- site-accept: 17 -> 21 checks (supported systems, gzip download with
  a binary-safe probe, checksum, /dl traversal 404)

Verified on 192.168.0.165: the real 960,820-byte tarball downloads
as application/gzip and its sha256 matches the published digest.

Gates: oop-accept MET, site 21/0, web-app 46/0, fibers 10/0,
db-actor 8/0; shop rebuilt and its /assets served by the framework.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 04:41:00 +02:00
3afdafa5c4 feat(site): logo, favicon, install guide, package catalogue
- layout/logo.wo: the mark as inline SVG — dark tile, two-stroke "W"
  (white then accent blue). One source: nav brand + /favicon.svg
- favicon/controller.wo: GET /favicon.svg, image/svg+xml, day cache
- install/: GET /install — toolchain tarball, PATH, verify, first
  project, build/run, adding a dep. Copy from the real install README
- packages/: GET /packages + /packages/:name — catalogue with the
  [deps] line, what each library gives you, and a usage snippet.
  Index cards are child components (multi Component)
- wo-html: page_head(title, head, body) and a `head` slot on Layout —
  a favicon link or meta tag had nowhere else to go; page() passes ""
- header: Install/Tutorial/Packages/GitHub, brand shows the mark
- main.wo: SITE_HOST picks the interface (loopback default), bound
  address printed at startup
- site-accept: 11 -> 17 checks (install, packages x2, 404, favicon,
  inline logo)

Verified on 192.168.0.165:8080 — every route, favicon bytes, and the
mark rasterised at 256px and 32px.

Gates: oop-accept MET, site 17/0, web-app 46/0, fibers 10/0,
db-actor 8/0; shop rebuilt clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 04:05:00 +02:00
23550e7021 feat(lang+wo-html): raw text literals, component layer, MVC samples
- lexer: backtick raw text literal — content verbatim, no escape
  processing, common source margin removed at lex time; `${ }` raw and
  `{{ }}` auto-escaping holes
- `{{ e }}` desugars to `esc(${e})` in parser.ml — a Call on the `esc`
  in scope, so types/owner/emit/.wob/VM are untouched
- WO-E004 unterminated raw literal; WO-E005 newline inside "..." —
  closes a hole where a missing quote silently ate the rest of the file
- wo-html: `Component` interface, `render_all`, `Layout`, README
- framework: `ok_html` joins ok_text/ok_json in http/types.wo
- site + shop restructured to one-feature-one-module MVC (view +
  controller per directory, model at the root, bootstrap-only main)
- removed the filler `pad: Int` convention — verified unnecessary for
  plain classes, interface dispatch, containers and actors
- corrected recorded claims: gap #1 blocks neither the build nor the
  layout; a class crosses module lines, only a free fn is scoped
- docs/guides/language-surface.md — the full grammar inventory
- story 37 landed and moved to done/

Gates: oop-accept MET, oop-e2e 116/0, woc-test 556/0, site 11/0,
web-app 46/0, fibers 10/0, db-actor 8/0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 03:58:41 +02:00
a4743edcc6 fix(shop): views markup-first within today's grammar
- render() bodies: one HTML line per 'h = h ..' statement, single-
  quoted attributes, ${} holes, esc() on data — el() chains gone
- discovered + recorded gap #3: no multi-line expressions or literals
  (leading/trailing .. and paren grouping all reject at NEWLINE) —
  exactly the tax story 37's raw literal deletes
- 37-target comment blocks dropped (bodies now self-explanatory; the
  README states the delta); rebuilt + full buy-flow re-smoked (178.0
  total, stock 12->10, 409, traversal 404)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 00:23:39 +02:00
bbf5fb66d3 feat(shop): 37 target = in-class raw template literals (developer form)
- separate view.html files dropped; every render() now carries its
  '-- 37 target:' literal above the hand-lowered body — the pair is
  the DX referendum in one file
- story 37 re-pointed: raw multi-line literal + {{ }} auto-escaped
  typed holes + {!! !!} raw slots; structural control stays if/for;
  w:if/w:for and .html files demoted to later; forks revised
- rebuild verified on untouched toolchain

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 00:15:38 +02:00
5a2e6402c8 feat(shop): story-37 target templates beside the hand-lowering
- view.html per feature + layout app/header/footer.html: the markup-
  first form woc will compile ({{}} auto-escaped, w:if/w:for,
  {!! !!} slots, w:component sections); inert today, verified not to
  disturb the build
- README: pair is the DX referendum — .html is the target feel,
  view.wo is today's cost; doctrine line reworded (templates compile
  or don't exist)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 00:01:11 +02:00
89df517613 feat(shop): the program template — MVC on disk; story 37 redirected
- docs/examples/shop: types.wo model, per-feature view modules
  (render() classes), root controller files, layout shell/header/
  footer, static assets controller + real style.css, README with
  Angular file map + run + DX referendum notes
- buy flow proven by hand: stock check/decrement, 409s, traversal
  404, css typed, WAL-durable; builds on the UNPATCHED toolchain
- two language gaps recorded, not fixed (per directive): pub+@table
  cannot combine (controllers forced into root module); @view
  projection classes absent
- story 37 REDIRECTED per Vue-SFC review: view.html compiled by woc
  ({{}} auto-escaped, w:if/w:for, typed against view class, no
  runtime engine); render()-as-concatenation failed the referendum;
  forks revised; shop named the acceptance consumer

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 23:57:44 +02:00
67cd039533 docs: fix stale story-35 links (file moved to done/ at landing)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 19:36:18 +02:00
8f96584682 docs: story 37 — wo-html components (MVC view layer, Angular format studied)
- Component interface (render->Text), layouts/slots, site migrates
  as acceptance; framework stays micro (view layer = library)
- Angular map recorded: inputs/templates/ngFor/projection translate,
  DI/bindings/client-side rejected by doctrine (no closures, no JS)
- four forks for the spec; unscheduled, off-chain; table + board rows

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 19:35:41 +02:00
b19042bca0 feat(site): go.dev-style homepage + shared nav/footer chrome
- wo-html grows generic nav_bar/card/btn_link + the utility classes
  they need (sticky nav, footer, 2-col grid, hero sizes); library
  stays content-free
- home: hero (tagline + Get started/View source CTAs), real actor+
  table code showcase, four why-cards, chapters strip (gate's
  'Learn writeonce' anchor kept); every page shares nav + footer
- site gate 11/0 unchanged; loopback bind untouched

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 19:11:21 +02:00
735fd270db feat: chat sample + gate (T8/T9, IN PROGRESS) + stop-drain semantics
- docs/examples/chat: registry (call consumer) / room / reader+writer
  actor pair per connection over ws_accept + wsframe; presence,
  broadcast, cross-room isolation, mailbox-full = drop-from-room;
  reader tail sends hardened (a full writer no longer orphans the fd)
- RUNTIME SEMANTICS CHANGE (the drain): SIGTERM no longer kills parked
  fibers from outside — the plane WAKES them and each wait RESOLVES
  (deadline'd waits answer their timeout result, sleeps return early,
  plain waits answer WO_SYS_STOPPED and unwind THAT fiber alone; main's
  STOPPED still ends the program). Workers keep adopting their inboxes
  after stop until eng_shutdown. This is what lets a program drain:
  chat's close frames now reach clients (byte-verified 0x88), then
  main returns and the reap runs
- also: SIGPIPE ignored process-wide (EPIPE trap instead of death);
  two-phase engine teardown (real drops while arenas+routing live,
  settle passes for routed frees) — fixes the registry-map leak and
  the drain UAF ASan found
- gate scripts/chat-accept.sh + just chat: handshake independently
  verified, functional matrix on BOTH backends, 1k-hot-room soak
  (1000/1000 in ~35ms), drain close-frames, SIGTERM exit 0, ASan leg
  clean. OPEN: soak-fds check (18 fds settle slower than the window)
  + full battery after the semantics change — NOT yet run
- committed for manual testing at the user's request

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 09:53:21 +02:00
4092074201 feat: monitor + time.after (ids 89/90) — the lifecycle slice completes
- monitor(watched, observer, msg): registration lives on the watched
  actor's home thread (kind-7 envelope cross-shard); actor_die walks
  the list; already-dead fires NOW; the notice msg moves; a full
  observer's notice drops with a stderr line (no fiber to trap)
- time.after(ms, addr, msg): per-shard timer list riding the deadline
  machinery (uring tick min + epoll timeout both include timers;
  fired from the same sweep); ms <= 0 delivers now; NO cancel — the
  generation-counter idiom is pinned by run/timer-generation
- runtime_notify: one runtime-sourced delivery path (notices, timers) —
  reserve-or-drop, cross-shard via kind-0 envelopes
- compiler: monitor typed as a bespoke free fn (notice typed against
  the OBSERVER's mailbox — the three-argument deviation, disclosed);
  time.after as a stdlib row whose msg arg is EXEMPT from the module-
  call fresh-arg drop (it moves — the double-own bug the timer fixture
  caught); owner move slots for both
- corpus: run/monitor-death (trap-death + already-dead notices),
  run/timer-delivery (armed + immediate), run/timer-generation
- teardown drops undelivered notices and unfired timers; battery 13/13

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 08:56:48 +02:00
9661c08696 feat: TE rejection + listen backlog 1024 + the 1k soak gate
- parse.wo: ANY Transfer-Encoding header is 400-and-close (RFC 9112
  §6.1) — silently treating chunked as body-less was the smuggling
  door the dup-CL fix left open
- net.listen/listen_unix backlog 64 -> 1024: the soak's connect bursts
  overflowed the kernel accept queue and BLACK-HOLED clients (three-way
  handshake done, server never sees the conn — 35-70 stuck per run,
  fully reproduced then gone at 1024; kernel clamps via somaxconn)
- web-app gate grows to 46 checks: TE-reject; the 1k soak — 500 real
  conns all served + 500 idle conns all evicted, server fds home
  (45 -> 45), RSS 24MB, healthy after
- battery green (site restart + fibers-TSan legs flaked under parallel
  battery load, both clean serially — the standing flake pair)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 08:23:55 +02:00
a32550d968 feat: iteration 35 — net seams + the serving slice (fiber-per-connection)
- runtime ids 91-95: net.read_dl/accept_dl/write_dl (per-call deadline,
  nil/false = the EXPECTED timeout; ms<=0 = old behavior bit for bit),
  net.listen_unix (unlink-before-bind, O_NONBLOCK on the listener —
  probe-found: accept4's flag covers accepted sockets only), net.peer
- plane: one-op-per-park stays law — deadlines ride one per-shard
  TIMEOUT tick (sentinel user_data) + post-CQE expiry sweep +
  POLL_REMOVE tombstone; epoll's deadline scan grew the fd-park case;
  fibers POOL instead of freeing mid-run (stale-CQE UAF); plain parks
  zero park_deadline (no stale sleep deadlines)
- probe: all five seams verified on BOTH WO_IO backends (timeout
  timing exact, peer round-trip, unix rebind)
- framework: parse_request grows first_ms/read_ms; serve_conn — the
  keep-alive loop with deadlines where parked idle conns are LEGAL
  (close-when-idle RETIRED); App.handle_conn exposes it; plain serve()
  unchanged for simple apps
- web-app: app-owned accept_dl loop + ConnWorker actor per connection
  (each builds its own App; cross-shard placement rides the DB actor);
  WA_IDLE_MS knob; gate grows to 41 checks — two slow requests served
  in PARALLEL, stalled client evicted at the idle deadline, slow-loris
  torn at the read deadline (400)
- docs: story 35 -> done with banner; SQE/CQE design spec LANDED (was
  the review doc); ledger rows (timeouts/unix/keep-alive/peer), graph
  (NETSEAM cleared, KEEPAL done), builtin-surface rows, runtime
  CODE-LOGIC section, board entry
- battery 13/13 fresh-built

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 08:04:32 +02:00
82713e4010 feat: framework v1 slice 2 — the remaining ledger, ten items
- After seam: interface After + Aw + use_after; dispatch funnels every
  response (handler/short-circuit/404/405) through the after chain;
  the WS 101 sentinel skips it (never serialized)
- http/secure.wo: SecurityHeaders (nosniff/DENY/referrer; HSTS stays
  at the TLS proxy), Cors (preflight 204 before + origin stamp after,
  one class both halves), HostAllow (421), client_ip (XFF parsing —
  peer VERIFY stays story 35)
- http/nego.wo: accepts() (exact, type/*, */*; q stripped not ranked),
  etag_for (quoted base64 sha256), with_etag (If-None-Match -> 304)
- router: *rest wildcard (last segment, empty rest matches), Group
  (prefix + routes + group middleware) + Gmw prefix-scoped entries,
  App.mount; new App fields carry defaults so standing ctor literals
  keep compiling
- Req grows ctx bag; parse rejects duplicate Content-Length (400,
  RFC 9112 §6.3)
- web-app exercises all of it; gate grows 26 -> 38 checks (wildcards,
  group+ctx, etag+304, 406/200 negotiation, sec headers, 421,
  preflight+origin stamp, dup-CL 400)
- ledger rows flipped; dep graph section 3 grown (slice-2 done nodes,
  crypto gate cleared, cookie/CSRF/session/webhook/JWT now ready)
- merges: chat-ws-lifecycle (digests for ETag; WS + lifecycle ride
  along) + site-sample (second consumer gate); battery 13/13

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 06:45:51 +02:00
0fe0efc6ce Merge branch 'site-sample' into framework-v1b 2026-08-23 06:33:15 +02:00
f7cf08b82c docs: slice marker — T1/T2/T3/T6/T7 landed, T4/T5/T8/T9/T10 pending
- progress ledger with commit ids + the two en-route compiler/runtime
  fixes; pending list carries each task's remaining shape and the
  disclosed deviations (scalar replies v1, three-argument monitor)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 06:27:31 +02:00
9a9e4927f6 feat: call/reply — send that waits (id 88, envelope kinds 5/6, WO-E226)
- runtime: mailbox slots grow caller metadata (wo_msg), call parks on
  WO_PARK_INBOX (the DB-RPC protocol) and the resume consumes a SCALAR
  reply; FIBER_DONE ships the receive's return value home (same-shard
  unpark or kind-6 envelope); kind-5 carries cross-shard calls
- actor death is real now: a receive trapping uncaught marks the actor
  dead, error-unparks the in-flight caller AND every queued caller,
  drops queued payloads + state, releases cap slots; send-to-dead
  drops silently, call-to-dead traps — a call never hangs. Fixes the
  pre-existing leak/dangle in TRAPF's fiber-death path (cur_msg leaked,
  a->active dangled, the mailbox rotted)
- compiler: reply typing through actor-M erasure — every receive(M)
  program-wide must agree on one return type and it must be a copyable
  scalar (v1); WO-E226 names disagreeing classes / void receives /
  non-scalar replies; call's message moves exactly like send's (owner)
- corpus: run/call-echo (park + ordered replies), run/call-dead-trap
  (mid-call + to-dead, both catchable), compile-fail/call-void-receive,
  compile-fail/call-reply-disagree; cross-shard call proof rides the
  chat gate next
- battery 12/12 fresh-built (ASan+TSan lanes in fibers/db-actor green)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 06:22:24 +02:00
ad4b380cf1 feat: writeonce.de tutorial site + wo-html library (two-dep full stack)
- docs/examples/wo-html: library dep — esc(), element builders,
  Tailwind-style utility sheet as one static string, page() shell;
  self-contained responses, no CDN/JS/build step
- docs/examples/site: the language tutorial served by the language —
  9 seeded chapters in a @table (hello/values+bitwise/containers/
  classes/optionals+traps/tables/actors/deps/serving), server-rendered
  via wo-html, seed-if-empty so WAL restarts keep admin edits
- routes: / index, /ch/:slug (styled 404), /health, POST /admin/ch/
  :slug (bearer handler-side — mechanism framework's, policy app's;
  form-encoded title/body update by assignment, 302 back)
- chapter code samples use the lexer's \$ escape to show ${...}
  literally; .. never straddles newlines (accumulator style)
- gate: scripts/site-accept.sh + just site — TWO file:// dep remotes,
  11 checks incl. authed-edit-survives-restart; /health polling, no
  boot-race sleep
- README: run + nginx sketch for writeonce.de; CODE-LOGIC beside code
- full battery 13/13 (site gate included)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 01:51:14 +02:00
a7f87c72b9 feat: framework WS frame codec — http/wsframe.wo (pure .wo)
- ws_parse: one client frame off a carry buffer (parse.wo's carry
  convention); mask REQUIRED, RSV/fragmentation/64-bit lengths refused
  (kind -1), 7- and 16-bit lengths, 1 MiB payload cap, control frames
  <= 125; unmask via iteration 36 bitwise, parts+join stays linear
- serializers: ws_text/ws_close/ws_ping/ws_pong, server frames
  unmasked, 16-bit ceiling
- probe-verified against RFC 6455: masked "Hello" example bytes parse,
  torn 3-byte feed = incomplete, two pipelined frames sequence, ser
  bytes exact, worked-example accept key round-trips; committed gate
  coverage rides the chat sample's acceptance script
- deps-accept + web-app + oop-e2e green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 01:25:45 +02:00
cf95e5ce9c feat: framework WS upgrade — ws_accept + hijack sentinel (http/ws.wo)
- Req grows internal conn field (net.Conn, filled by parse) — handlers
  touch it only through ws_accept
- ws_upgrade_valid: RFC 6455 §4.2.1 (GET, Upgrade token, Connection
  token list, 24-char key, version 13); ws_accept_key pure
  (base64(sha1(key+GUID)) — the runtime vector already pins the RFC
  worked example); ws_accept writes the 101 and returns the fd;
  hijacked() = the status-101 sentinel
- serve.wo: 101 skips serialize AND close — the loop forgets the fd
  and returns to accept; plain HTTP byte-identical (web-app 26/26)
- codec + end-to-end proof land with the chat sample's gate; battery
  12/12 (fibers TSan leg flaked empty under load, 10/10 on rerun;
  web-app restart leg has a pre-existing 0.5s boot race, noted)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 01:22:59 +02:00
dd7dd42bd1 feat: bounded mailboxes + WO_T_ACTOR (trap 13); try-arm place-copy fix
- cap 1024 (WO_MAILBOX override at boot): sender-side atomic
  reserve/release on every path — same-shard, cross-shard envelope,
  OOM rollbacks; full mailbox traps the SENDER catchably; delivery
  pop releases; overshoot bounded by in-flight sends (disclosed)
- test_mailbox 12/0: exact cap single-threaded, two racing senders win
  exactly cap slots, drain/refill clean
- corpus run/mailbox-full-trap: parked sleeper, send loop catches
  "actor mailbox full" after >= 1024 sends
- pre-existing compiler bug found + fixed: a try ARM yielding a Text
  PLACE (bare e.msg, try box.field) aliased a register the arm's scope
  end freed — ASan use-after-free, SEGV on the next unwind's
  double-walk; emit_try now applies copy_place_text to both arm
  results; pinned by corpus run/catch-msg-place
- db-bench driver: msgrate keeps iteration 22's unbounded-flood
  contract via WO_MAILBOX=MSG_N (the cap is 24's policy, not 22's)
- battery 12/12 fresh-built

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 01:05:13 +02:00
5fc32b4926 feat: iteration 34 — digest builtins sha1/sha256/hmac_sha256 (ids 85-87)
- runtime/src/crypto.c: hand-rolled cores, whole-value over Bytes,
  allocation-free tails; VM half returns fresh Bytes, WO_T_BOUNDS on
  wrong class id (Bytes builtins' message shape)
- test_crypto: RFC 3174 + FIPS 180-4 + RFC 4231 (incl. long-key case 6)
  + 63/64/65 block-boundary sweep + the RFC 6455 handshake input, 18/0
- compiler surface flat per house convention (sha1, not crypto.sha1 —
  matches base64_encode): types.ml signatures + result types, emit.ml
  ids/dispatch/arity/known-list/drop-table (fresh-Bytes entries so
  results get their drops)
- corpus run/crypto-digests pins the .wo path through base64_encode
- no .wob bump (ticks-84 precedent); WO_B_MAX 87; surface doc rows
- slice marker + board row: iteration 24 (absorbing 31+34) executing
- battery 12/12 fresh-built

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 00:42:43 +02:00
7ca8b178ea docs: plan — chat + actor lifecycle (10 tasks, 5 stages); spec approved
- stage 1 crypto (ids 85-87, RFC vectors), stage 2 lifecycle (cap +
  WO_T_ACTOR, call kinds 5/6, monitor, time.after — ids 88-90), stage
  3 framework WS (ws_accept + hijack, wsframe codec), stage 4 chat
  sample + 5-check gate, stage 5 closeout
- two spec deviations pre-disclosed: reply-type agreement rule
  (WO-E226 through actor-M erasure), monitor three-argument form
- battery-with-builds-first constraint baked in (stale-binary lesson)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 00:28:26 +02:00
9a9da1b41a docs: spec — chat + actor lifecycle (iteration 24 absorbs 31, 34 resolved)
- one iteration by directive 2026-08-23: call() parks with typed reply
  (envelope kinds 5/6 over the DB-RPC park), mailbox cap 1024 +
  WO_T_ACTOR fail-fast, monitor(addr, msg) one-way, time.after
  one-shot no-cancel
- story 34 resolved: C builtins sha1/sha256/hmac_sha256 over Bytes,
  RFC vectors gated
- WS pure .wo: handler-owned upgrade (ws_accept + hijack sentinel),
  frame codec over Bytes via 36's bitwise, two actors per connection
  (sole-reader + sole-writer)
- chat sample: registry + room actors, python raw-RFC6455 gate —
  cross-shard functional, 1k soak, SIGTERM drain, battery unchanged
- status PROPOSED — awaiting review before the plan

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 00:23:58 +02:00
64a4700190 docs: reconcile story 34 + gates with merged master
- story 34: premise fixed — iteration 36 landed bitwise/hex, digests
  and HMAC now expressible in pure .wo; C-builtin vs pure-.wo is the
  story's brainstorm call, not an impossibility
- dependency graph: crypto gate names story 34; net-seam gate names
  story 35; radix gate corrected — 22 benched the DB, router scan
  still unmeasured, perf-targets entry first
- framework README ledger: timeouts/unix/peer rows point at story 35;
  ETag row at story 34; path-matching row repointed off iteration 22

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 23:30:55 +02:00
ee018f06e2 Merge branch 'read-path-index' 2026-08-22 23:26:01 +02:00
dc3e5b7e5b Merge branch 'db-bench' (iteration 22 — durability/throughput baseline)
- brings time.ticks builtin (id 84), bench sample + campaign driver
  (scripts/db-bench.py), just db-bench/db-bench-quick recipes, first
  baseline recorded, story 22 to done/, postgres study cards
- conflicts resolved: board in-progress table (iteration 36 +
  framework rows kept, db-bench row now "22 landed"; dangling order
  anchor repointed); story 36 moved back to in-progress/ (dir-rename
  inference dragged it to done/ — 36 still awaits the manual pass)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 22:57:51 +02:00