- `resident: all | keys` replaces `resident: all | index`. Two reasons beyond
taste: it kills the collision with the `index:` argument
(`@table(index: [customer], resident: index)` read badly), and it puts both
values on ONE axis — each now answers "what row data stays resident",
where `all`/`index` mixed a quantity with a structure name
- accurate as well as clearer: what stays resident is the id->offset map, the
secondary indexes and the unique shadows — all key structures; row payloads
are exactly what leaves. `resident: none` was rejected as overclaiming,
since the indexes very much are resident
- checked for collisions: neither `all` nor `keys` is a keyword or a builtin
(`key_at`/`val_at` exist, bare `keys` does not)
- the spec's wart note became a recorded decision; the rejected spelling is
kept quoted so the rationale still reads
- fixes a bug I introduced in the 2026-08-26 track move: all six moved
iterations carried a banner reading "Part of [Story — the database beyond
RAM]" whose link pointed at the LANGUAGE arc — correct target, lying text,
the exact failure mode the link audit warned about. Banners now point at
the databasev2 story, and the original "Part of" line says plainly which
track the iteration was authored in before the move
- linkcheck 0 broken / 0 anchors
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- driving case: a 120 GB order table on a 32 GB host. Not a tuning problem;
no eviction policy fixes it. Developer accepted reconsidering the principle
- principle 7 rewritten: durability half UNCHANGED and unconditional
(WAL-logged, fsync before ack, CRC-dropped torn tail); residency half
demoted from law to per-table declaration. Old wording quoted in place so
the amendment is legible, with the reason: a doctrine a real workload
cannot satisfy gets ignored, and the failure it produced was an OOM kill
- spec: docs/superpowers/specs/2026-08-26-table-residency-design.md
One log-structured engine — the WAL already holds every row, so keep an
in-RAM id->offset map and pread rows back. No second engine, no user-space
row cache (the kernel page cache is the hot copy, which is already this
repo's stated position and why it avoids O_DIRECT)
- arithmetic that makes it work: 240M rows x 16 B of index = ~3.8 GB
resident in 32 GB. Indexes stay resident, rows do not. Buys ~2 orders of
magnitude, not infinity — stated plainly in the spec
- grammar: two optional keys, `durable: true|false` and `resident: all|index`,
both defaulting to today's behaviour, so all 28 existing @table
declarations compile untouched and no golden is reblessed
- rejected, with reasons recorded: mmap (rows are pointer-bearing —
table.c returns (uintptr_t)t as the slot word), buffer pool (the Rust-era
phase-12 design that died with that track), paged B-tree (stays rejected),
a three-valued enum, automatic spill, disk-backed-by-default
- self-review caught the budget defaulting to "none" while promising the ERP
developer a diagnostic instead of the OOM killer — contradiction fixed:
the budget defaults to a fraction of host memory, and its value comes from
databasev2 1's swap-onset measurement
- live docs that contradicted the amendment updated (subagent doctrine,
its guide, discarded.md's two rows, iteration 04's read claim, 07, 38);
dated specs/plans left as records. linkcheck 0 broken / 0 anchors
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- 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>
- 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>
- 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>
- 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>
- "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>
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>
- 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>
- 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>
- 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>
- 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>
- .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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- framework README: three rows still said "until fibers/shards" /
"fibers (11)" — now "arc landed 2026-08-21, parked until own slice";
ledger date 2026-08-22
- dependency graph: crypto-fork gate note updated — bitwise + hex
landed with iteration 36, digests expressible in pure .wo; story
34's brainstorm still owns the pure-.wo vs C-builtin call
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- owns the framework ledger's three seam rows; deadlines compose with
the arc's park plane (POLL_ADD+TIMEOUT fork recorded); framework
knobs explicitly out of scope; stalled-client soak in acceptance
- board + table rows (held seqs bumped); pairs naturally with 24
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- SHA-1 (WS-handshake hard req, RFC 6455 worked example as acceptance),
SHA-256, HMAC-SHA256 over Bytes; hand-rolled C per doctrine, FIPS/RFC
vector fixtures; four forks recorded (namespace, shape, source, file)
- gates chain item 24; digest floor for held 21 + ETag row; 24's
dependency note repointed; board + table rows (held seqs bumped)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- file path IS the wal; dir form byte-identical; one fork (nonexistent
path semantics) leaning recorded; off-chain, driver-only
- board + story table rows (held tail seqs bumped)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- engine: wo_idx_probe answers single-column equality from the index
hash buckets (idx_hash_key1 reproduces idx_hash bit for bit; verify
compares exactly as the slab walk did, so results identical);
composite indexes keep the walk; both executors wired (local + DB
actor RPC)
- compiler: probe_key_of_where lowers "var.col == key" on an indexed
column to DB_PROBE; all where guards still run (guard stays the
final arbiter); keys = ident/int-literal only; Float/Bytes excluded
(engine raw-eq narrower than VM float-eq)
- measured: reads 1.3k -> 1.3M ops/s, p50 600us -> 1us (~x850);
query x830; mixread 1.3k -> 89k s1, 21 -> ~1.9k sN
- gate policy moved into the driver (tolerance_for: refresh-proof);
latency floors max(4x,100us); quick mode skips poll-bound mix
floors; both tolerance classes proven to bite
- proof: test_table wo_idx_probe suite (RED first), corpus
query-index-probe 105/0, full battery green, TSan clean, two
campaigns pass the refreshed baseline
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- constraints-and-grammar: gram.y PK/FK productions, pg_constraint,
RI trigger semantics; writeonce direction — @key as unique alias
(id stays THE key), ref actions (@on_delete), backlink-implies-index
(improves on postgres' not-auto-created FK index)
- indexing-and-point-lookup: AM roster + algorithms (Lehman-Yao,
linear hashing), TID = row address; writeonce gap — probe walks
slabs while idx_bucket exists; O(1) slice direction, non-goals
- card index updated; Rust-era plan-10/11/12 links unlinked (rot)
- docs/guides/database-developer-subagent.md: format, paste-ready
agent definition (doctrine/file map/gates), verification, division
of labor
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- bench/baseline.json: 74 metrics from the first full campaign;
tolerances tuned by a two-run repeatability check (mix* 50%,
read/query 35%, rest 15% — rationale in _config)
- gate bites: --check mode; doctored copy fails, both real runs 74/0
- headline: durable seed 4.5k/s vs ram 297k/s (23's case); reads
O(table) at ~1.5k/s; mixread 1280 vs 21 ops/s single-vs-multi
(the arc's price); msgrate 13.4M vs 2.45M (mutex-inbox number)
- arc delta recorded in story 8; findings in sample README
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Mixer actors: 90/10 read/write, per-actor histograms merged through
the store itself (Hist rows) — exact aggregate percentiles
- msgrate: one-way flood at a worker-placed sink; measured 15.3M
msgs/s same-heap vs 2.06M cross-shard — the mutex-inbox number
- finding: point lookups are O(table) (probe walks all slabs), so
read-heavy mix is quadratic in store size — all-mode calibrated to
N/10 mix ops; the number 22 exists to publish
- TSan clean both shard counts (setarch -R, fibers-gate pattern)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- seed/read/query/write/wal/verify/verify-acked + all (one-process
campaign: RAM store dies with the process)
- per-op time.ticks, 1us-bucket histogram percentiles (reservoir
deviation: no element-write/sort in language; better tail anyway)
- Meta expectation rows ride the same WAL verify checks
- finding: hand-built multi<TableClass> SEGVs on drop (elems classed
OWNED, refs are scalar ids) — worked around, recorded
- finding: reads ~1.6k/s p50 595us vs 287k/s inserts — probe walks
all slabs; the number 22 exists to surface
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- worker DB builtins marshal to shard 0: requester-side slot encode
(VM heaps never read cross-shard), owner executes serialized in
adopt, reply unparks via new WO_PARK_INBOX park + envelope 3/4
- engine gains thread-agnostic slot entry points (insert_slots,
update_field_slot, val_encode/clone, wo_db_exec_req); traps and
messages byte-identical to the local path
- main.c: engine + replay boot BEFORE shards spawn; workers assert
rt.db/rt.wal NULL; busy shard adopts inbox once per slice
- latent stage-1 bug fixed: shared io_uring params static raced by
lazy worker init lost park wakes (~1/20 hangs); params per-vm,
short submit now fails loud
- new sample docs/examples/db-actor + just db-actor gate 8/0 (multi
x3, uring/epoll forced, single byte-exact, WAL replay pair);
ASan+TSan 6/6; full battery green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- five-property map in marker doc: atomicity/durability/recovery
proven or held (18); concurrency control = stage 3's property;
space reclamation RAM done (slot reuse), disk = new story 32
- story 08: three stage-3 criteria (one commit per write RPC +
ack-after-owner-fsync, workers WAL-free + replay-before-serve,
no torn reads under TSan corpus); arc plan stage 3 carries them
- refine/32-wal-checkpoint.md: snapshot + truncate, bounded replay,
crash-during-checkpoint safe; four forks; after 23
- chain now stage 3 -> 22 -> 31 -> 24 -> 23 -> 32 in all 10 docs;
boards + seq bumps synced
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 08 landed in discarded/ by mis-drop, swept into prior commit with
two dead links; corrected to stories/.../in-progress/ per intent
- 11 joins it (one arc, active slice)
- board doctrine: active stories bucket named; all links re-verified
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- one marker doc, deleted on landing; board doctrine names the
second folder exception
- board In-progress row was stale (nothing active + dead anchor);
now points at marker + arc plan tasks 7-8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- io_uring is a must; epoll approach discarded (developer decision)
- plan superseded by shard-fiber-arc plan of record; banner + row in
plan/discarded.md; file kept as idea reference
- three live pointers repointed: status language-track row 8,
principles enforced-by, story 08 note
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- code-verified ready: arc stages 1+2 merged to master; stage 3
concrete in plan (tasks 7-8); rt.db set on primary only
(main.c), worker_late_init memsets rt — WO_T_DB hole real
- 22/23/24/31 stay in refine/: open forks, no bench harness,
no crypto builtins, chain-blocked
- links fixed both directions; board doctrine names hold/ bucket
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- iterations 18/25/26 marked hold in their spec + plan headers
- 25's story file removal committed; plan doc stays for resumption
- web-framework spec's relates-to flags 25 held
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Float full stack: literals (fraction/exponent; `0..10` still a range), f64
opcodes 34-41, @table column, WAL bit-exact replay, json fractions in and
shortest-round-trip out. IEEE-quiet — FDIV never traps where DIV does.
- Bytes: a wo_str with its own class id, so alloc/free/copy are shared but no
Text builtin accepts one; len/at/slice/eq/concat, base64 both ways, json
boundary as base64; TEXT_COPY preserves the kind.
- No implicit Int/Float mixing (WO-E201 in the typechecker, not the emitter,
which picks the opcode from one side and would misread the other).
- One IEEE deviation: float_cmp total order (NaN last, -0.0 == +0.0) for
indexes and order-by, keys canonicalized to match. `?Float` nil is a
reserved quiet NaN — the zero word is +0.0, WO_NIL_SCALAR's bits are -2.0.
- Renderer prefers fixed over exponential in 1e-6..1e21: pure shortest makes
a price of 900.0 read `9e+02`. One renderer for interp/json/float_to_text.
- Fixed en route: lexer double-counted the leading digit; is_scalar_shaped
took Float/Bytes as Int-shaped; Bytes ownership needed a shared heap-scalar
predicate or temps never dropped; order-by bit-compared negatives backwards.
- Iteration 17: `kind = "library"` (absent = program; bad value = WO-E109),
entry-less check mode retiring the `--emit` workaround, Go's `internal/` as
WO-E108 at the consumer's `use`. Driver-only; VM/.wob/GC untouched.
- Framework reorg: internal/{parse,serve}.wo; http/form.wo split out to keep
media_type/form_values public (parse.wo had grown public surface).
- Docs: link audit (97 -> 88 broken, conflict markers resolved, 2 duplicate
stories removed), 00-code-review verified 26/27, iterations re-sequenced.
- Also carries the pre-staged pub(read)/using/#if work from the index.
- Gates: corpus 103/0, test_wal 156/0, web-app 26/0, oop-accept ALL MET.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- developer directive: pending iteration IDs now ARE the priority order;
LANDED iterations keep historical numbers (code comments and commit
history cite them — records, not a queue); 8/11 (the half-landed
arc), 17 (parked, artifacts on a branch), 18 (next, artifacts named)
also frozen
- mapping (recorded in 00-story): 19<-20 Float+Bytes, 20<-9c attach,
21<-9d keypair, 22<-9e benchmarks, 23<-9f io_uring WAL, 24<-19 chat,
25<-10 services, 26<-12 blue-green, 27<-9g query corpus,
28<-14 skillhost, 29<-13 metaprogramming
- 11 story files renamed; every doc reference re-numbered (word-boundary
sweep for the lettered 9x ids, phrase-level for numeric ones); the
iterations table rewritten with Seq == priority and "(was N)" notes;
story-scoped link check: zero broken
- merge-recovery folded in: the partial master merge had dropped the
chat story, the fibers exploration note, the arc spec+plan, the
framework-v2 plan, and the iteration-17 spec+plan — all restored from
their branches and renumbered consistently
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- brainstorm settled four forks: Float FULL STACK in one iteration
(literal, IEEE f64 VM ops via u64 bitcast, @table column + WAL slot,
json fidelity — json.c's own comment names the hole: fractions are
malformed because "the language has no [float]"); IEEE-754 QUIET
semantics (division never traps, NaN flows; Int keeps DIV0; no
implicit mixing — float(i)/trunc(f) bridges); BYTES ships alongside
(binary carrier: multipart files, WS frames for 19, crypto digests;
Text goes back to meaning text); iteration 20 + spec before code
(.wob/WAL version bump earns a written spec)
- the rest of the missing-type survey recorded with reasons: Result/
Option expressible today (?T + payload unions), tuples covered by
records + doctrine, Decimal stays cents-until-a-workload, Char/
Unicode its own future story, Set/ADTs parked post-12, scalar
newtypes need the rejected `abstract`
- one indexed-storage deviation from raw IEEE spec'd loudly: a Float
index needs a total order — NaN sorts last
- board row, story-table row (seq 26), graph node (9+16 -> 20 -> 19;
crypto gate wants Bytes)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- docs/examples/fibers: part 1 is TIMING-FREE and byte-exact — main
sends three messages then burns reductions; each budget expiry hands
the Counter actor exactly one delivery (cooperative mechanics,
preemptive fairness, BEAM's shape); part 2 parks a Sleeper actor
mid-receive on the I/O plane while main keeps ticking — the wake
lands between ticks, proving a sleeping fiber blocks nobody
- the missing "sleeper: up" on the first run was main-return-reap
working as specced (main ended before the deadline); the demo's
window widened so the wake is observable
- scripts/fibers-accept.sh + `just fibers` (8 checks): build, part-1
exact + part-2 ordering invariants on auto/uring/epoll backends,
and an ASan-runtime rebuild+run
- README points at the doctrine writeup (exploration/fibers)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- done/ (11): 1, 2, 3, 4, 6, 7, 7b, 9, 9b, 15, 16 — landed iterations
(9/9b remainders live in the post-12 drain list, not in the files)
- refine/ (8): 9c, 9d, 9e, 9f, 9g, 11, 13, 14 — everything marked
"no spec yet / brainstorm before planning"
- root keeps: 00-story (index), 05 (partial, plan 8 open), 8/10/12
(specs or plans exist), 17 (parked, spec+plan approved), 18 (next)
- every cross-reference re-pathed and VERIFIED resolving: board, specs,
plans, employee-list README, story table, intra-story links (moved
files' relative links deepened one level; done/7b's 9e pointer now
crosses to refine/)
- pre-existing dead link noted, not touched: refine/11-fibers.md points
at docs/plan/exploration/fibers/00-fibers.md which does not exist
(predates the move)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- 00-story.md iterations table rows reordered into implementation order
with a Seq column; # stays an immutable ID (files never renumber —
every board/spec/plan references by number)
- order: 1-7b, 9, 9b, 15, 16 (landed, landing order) -> 18 NEXT (spec
approved) -> 9c -> 9d -> 9e -> 8 -> 9f -> 11 -> 10 -> 12 -> 9g -> 14
-> 13; 17 parked row at the end, slots anywhere after 16 on directive
- story note + board implementation-order list synced (18 inserted as
item 2 after the parked-17 note; 9g now explicitly before 14; list
renumbered)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- re-analyzed every story/spec/plan markdown for dependency statements
- added: iteration 17's OUTGOING edge (framework internal/ reorg + check
mode, WO-E108/E109 reserved); 1-6 foundation anchor; 9b -> 10 ("service
blocks want query results"); 14's gap fan-out node; post-12 parked
drain cluster with dashed scope-directive edges (13 + drain are
directive-held, not technically blocked)
- new graph 2: the concurrency chain — 8/9f/11 and EVERYTHING they gate:
keep-alive parking retirement, h2c, body/response streaming + commit
point, cancellation, pub/sub+WS, 9c's rejected async-statement
alternative, schedulable idle timeouts, fiber jobs (+18),
cancellation->rollback (+18+cancel)
- graph 3 notes 9d's keypair crypto is its own C impl, neither waits for
nor feeds the crypto-fork gate; storage-integration rows point at
their real owners (future migrations story, 9-series query surface)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- docs/00-dependency-graph.md: three mermaid graphs — story iterations
(hard edges only; 18 is the only spec-approved node with all
prerequisites green), framework v1 ledger items (three recurring
gates: net seams, crypto fork, iterations 8/11; nine slices startable
today in any order), framework v2 internals (cache/flags independent,
transaction{} is the critical path, jobs compose on it)
- maintenance rule: node classes update in the same change as board rows
- board links the graph up top; spec 18 banner -> APPROVED, plan next
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- framework README core checklist expanded into the v1 STATUS LEDGER:
seven categories (transport, routing, request/response, context &
middleware, storage integration, security, crypto), every item
marked done / partial-with-named-gap / candidate / parked-behind-8-11
/ needs-runtime-seam
- verified before labeling: BODY_MAX caps headers AND body (size limits
done); net has no timeout or unix-socket or peer-address surface
(runtime seams); language has NO bitwise operators, so SHA/HMAC/CRC32
must be C runtime builtins or bit ops land first (fork to brainstorm);
radix routing waits for 9e to measure the linear scan first
- crypto hard stop recorded: HS256 unlocks and nothing past it
- memory-rich features relabeled FRAMEWORK V2 = iteration 18 (story +
spec banners + board rows); v1 gaps land as slices per the ledger
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Part A transaction: one wal_commit at block end over the existing
staged batch; reads see own writes (RAM stays authoritative); trap
unwinding out = abort (undo list: insert->remove, update/delete->
pre-image, captured before RAM apply, txn-only cost); try inside
keeps the block alive; WO-E110 lexical nesting, WO_T_DB dynamic;
no new opcodes, no .wob bump (internal builtins + catch-frame-shaped
abort marker); E108/E109 stay reserved for parked 17
- Part B: cache.wo (ttl_ms/cap, lazy time.now-ms expiry, FIFO over LRU
with the tradeoff stated, Text values via json); flags.wo (@table
wf_flags, on as Int 0/1 - Bool columns unproven, read-through map,
set updates table+map); jobs.wo (@table wf_jobs, enqueue composes
with transaction, JobRunner interface, App.jobs(take r, budget),
Dispatcher.idle() called post-accept PRE-PARSE - deterministic for
the SIGKILL durability proof, unlike after-response)
- web-app demo: transactional order+confirm enqueue, GET /jobs count,
POST /flags/:name with a flag-gated header on the product list
- gate: SIGKILL-after-201/restart/drain proof + flags persistence;
corpus carries transaction-commit/abort + WO-E110 + cache-ttl
(stamps injected, no sleeps)
- board row 18 -> spec written, awaiting review
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- http/multipart.wo: RFC 7578 whole-body parsing within BODY_MAX —
boundary from the raw content-type (quoted or bare, key
case-insensitive), parts split on --boundary, each part = headers,
blank line, content; filename + per-part content-type kept (lowercased)
- strict malformed-is-nil: no closing --boundary-- marker, a part
without content-disposition, missing blank line, no boundary param,
wrong media type — all nil, the caller's 400
- part_named(parts, name): first matching field's content, caller-owned
- web-app CreateProduct now accepts multipart/form/JSON (curl -F shape)
into the shared insert path
- probe 13/13 + 3x reuse loop (fields, crlf-in-content, quoted boundary,
file part, zero-part close, five malformed shapes) release + ASan
- gate grows 19 -> 21: multipart create 201, missing closing marker 400
- README: multipart row ✅ (all three body hooks done), limits updated;
story 16 + board record the landing
- gates: web-app 21/0, oop-e2e 89/0, deps-accept 8/0, log-watcher 7/0,
employee 8/0, woc-test green
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- media_type(req): content-type lowercased, "; charset=..." stripped,
"" when absent — the content-negotiation hook
- form_values(req): application/x-www-form-urlencoded body -> decoded
pairs through the existing query decoder ('+' as space, %XX); nil on
any other content-type so a JSON body is never misread as a form key
- web-app CreateProduct accepts form OR JSON; shared create_product
insert path; field/number validation answers 400
- probe 7/7 (plus/pct decode, empty value, case + charset param, json
and missing content-type nil, empty body, media_type strip) + ASan
- gate grows 17 -> 19: form create 201 with decoded name, non-numeric
price 400; hit() gains a content-type argument
- README: checklist row form ✅ (multipart stays candidate), limits
paragraph updated; story 16 + board record the landing
- gates: web-app 19/0, oop-e2e 89/0, deps-accept 8/0, log-watcher 7/0,
employee 8/0, woc-test green
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- App.get/post/put/delete_(pattern, take h: Handler) — the take-interface
shape probe-proven release + ASan before landing; retires plan-16
deviation 1; delete_ because delete is the query keyword
- dispatch matches path-first: wrong method on a known path answers 405
with Allow in registration order; unknown path stays 404
- HEAD routed as GET, body suppressed, Content-Length names the body a
GET would carry (serialize gains head_only)
- Logging middleware (request line to stderr) ships in router/
- set_header(mut r, name, value) — the builder escape hatch
- web-app registers through the helpers (dogfood); README documents all
- gate grows 14 -> 16: 405+Allow, HEAD-vs-GET content-length equality
- all gates green: web-app 16/0, woc-test 540/0, oop-e2e 89/0,
deps-accept 8/0, log-watcher 7/0, employee 8/0
- board/story: iteration 17 parked (spec+plan ready on library-internal),
16 carries the v1-polish landing, order list updated
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- NEXT PLAN step 1 now carries the reason: 17's edit targets (framework
sources, [deps] resolution in main.ml, just web-app gate) exist only on
the web-framework branch; unmerged start = branch stacked on unreviewed
branch
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- NEXT PLAN rewritten: iteration 17 is the goal slice (merge branch, spec/
plan, kind = "library", internal/ WO-E1xx, framework reorg, gate list)
- previous NEXT PLAN retitled "Landed 2026-08-15 — the executable milestone";
all six measured items were already done, board rows were stale
- board row 7: in-progress -> landed 2026-08-15 (ASan-clean, SIGTERM, fd-flat,
soak, just log-watcher 7/0); iteration 07 story banner updated to match
its plan doc's done banner
- in-progress table now carries iteration 17 spec/plan
- implementation order re-sequenced for framework goal: 17, 9c/9d, 9e, 8,
9f, 11 (+ h2c unparks), 10, 12, then 14/9g demoted (skillhost no longer
the driving workload), 13 + parked drain last
- story notes record the shift and the new order
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- new "Implementation order (sequenced 2026-08-20)" section in 00-status.md
pending bucket: 7-finish, 17, 9g, 14, 9c/9d, 9e, 8, 9f, 11, 10, 12,
13 + parked drain
- forcing rules recorded: 9f after 8+9e; 9c precedes 10; 9d folds into 9c;
12 after 9+10; 11 rides 8's scheduler; h2c behind 8/9f/11; post-12 park
directive unchanged
- 9e placed before 8 so restructure/perf work has a signed baseline
- 14 early as next driving workload (stdlib-shaped, shard-independent)
- story 00-story.md notes point at the sequenced list
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- fork 1: library-ness manifest-declared, kind = "library", default program
- fork 2: privacy = Go internal/ directory rule, named diagnostic at use
- fork 3: dep-boundary-only scope; Go subtree rule recorded as later tightening
- fork 4: lib+bin dual — library default action is check, explicit build works
- Go-inherited rule pinned: internal type in public signature allowed, no check
- impact analysis added: framework loses --emit workaround, plumbing under
internal/; compiler = two seams (driver kind + dep-use refusal WO-E1xx)
- VM zero impact by construction: no .wob change, libs compile whole-program
into consumer image, internal modules still emitted (privacy strips nothing)
- GC zero mechanism impact; pinned: inference stays whole-program, app usage
can promote dep classes, internal/ invisible to gcinfer — intended, not bug
- board + roadmap rows: needs-refinement -> forks settled, spec/plan next
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brainstorm outcome, deliberately NOT implemented (developer decision: keep
as an iteration needing further refinement). Records:
- the two gaps iterations 15/16 exposed: library-ness is implicit (a
no-main project fails woc <dir> build mode — the framework is verified
via an --emit workaround) and the dep boundary leaks internals (pub has
no dep-private tier: parse_request is as importable as Handler).
- the conventions corpus: Go (package decides program-ness; cmd/;
internal/ = directory-shaped privacy, zero keywords) vs Rust ([lib]/
[[bin]] manifest targets; pub(crate)-family keyword visibility). Doctrine
fit points at Go's shape with an explicit manifest key (writeonce HAS a
manifest; explicit beats inference in errors).
- four open forks for the spec: kind declaration form; internal/ vs
pub(lib) vs export-allowlist; dep-boundary-only vs Go's subtree rule;
lib+bin duality. Draft acceptance criteria; web-app 14/0 as the
regression gate. Roadmap + board rows added.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Board row 16 -> landed (web-app 14/0), pending row removed; story header
records the landing + the two as-built discoveries (idle-keep-alive
starvation policy; the two compiler gaps the chain exposed and fixed);
README gains the framework+web-app sample entry; plan checkboxes ticked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- docs/examples/web-app: Product (@unique name, backlink orders) / Order
(ref Product) as @table classes; handlers as Handler classes —
ListProducts (ordered query -> JSON array), ShowProduct (unique-index
probe, 404), CreateProduct (checked json.decode -> 400; @unique trap ->
409), CreateOrder (FK insert), DeleteProduct (FK restrict trap -> 409);
Auth middleware reads WA_TOKEN. Entry validates port + token honestly.
- The [deps] KEY is the module name `use` imports: hyphens are not
identifier characters, so the app keys the dep `framework` while the
repository keeps its long name (recorded in the manifest comment).
- driver fix (real gap the chain exposed): a dependency's INTERNAL `use`
paths are written against its own root (`use http` inside the framework)
but compile under `<depname>/...` — compile_image now prefixes dep files'
use paths with the dep name (stdlib namespaces stay bare; a path already
starting with the dep name is untouched).
Verified end to end through the full chain (temp git remote of the
framework, file:// substituted, fetch -> lock -> build -> serve): 401
without the token; [] empty list; 201 create; 409 duplicate (@unique);
400 malformed json; list/show payloads exact; 404 unknown product; 201
order; 409 delete-while-referenced (FK restrict) with the server still
serving; SIGTERM clean; the product survives a process restart (WAL
replay). Gates: woc-test 540/0, oop-e2e 88/0, deps-accept 8/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- router/router.wo: Handler (handle(req) -> Resp) and Middleware
(before(req) -> ?Resp; nil = continue) structural interfaces; Route/Mw
record classes built as ctor literals at the registration site — the
ownership shape the corpus pins (run/container-owned-move);
route_match with :param captures over '/'-split segments (empties
dropped, first mismatch wins).
- app.wo: App holds the middleware chain + route table (take-push),
satisfies http's Dispatcher, dispatches middleware-then-first-match,
fills the captures onto the borrowed request in place (Dispatcher takes
`mut req` — the borrow checker rightly refused rebuilding a Req from
borrowed maps; captures collect locally so a failed match never touches
the request), 404 fallback, serve(host, port) delegation.
Verified against a throwaway app (not committed): middleware 401
short-circuit without the token; /things/:id captures 42 into the body;
unknown path 404; a DIV0 handler answers 500 and the next request is
served; SIGTERM clean. Framework image emits at 12161 bytes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- http/parse.wo: bounded-read buffering to the header terminator, then
exactly Content-Length body bytes; %XX decoding ('+' = space in query
strings only, malformed escapes pass through — parsing stays total);
path/query split with decoded pairs; header names lowercased; the
three-state Parsed record (closed / malformed / request) with keep-alive
carry-over — bytes past this request belong to the next one on the
connection.
- http/serve.wo: Dispatcher interface (the router's seam), status/reason
serialization with computed Content-Length, and the blocking loop:
malformed -> 400 + close; a trapping handler -> 500 AND the loop lives;
fds closed on every path; env.stopping() honored.
- Connection policy discovered by probing, not assumed: a parked keep-alive
connection BLOCKS accept on a single-threaded server (probe: client 1
idles open, client 2 starves). Policy: serve PIPELINED requests on one
connection (carry non-empty), close when the client would idle; a proxy
reconnects. README states it.
Verified against a throwaway echo app (not committed): %20 query decode;
two pipelined requests -> two responses on one connection; DIV0 handler ->
500 and the NEXT connection served; GARBAGE -> 400; SIGTERM stops clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- docs/examples/writeonce-framework: library project (no fn main) — wo.toml,
README (what it is + the honest v1 limits + the proxy TLS/h2 story), and
http/types.wo: pub Req/Resp records + the response builders (ok_text/
ok_json/created/not_found/bad_request/unauthorized/conflict/server_error/
redirect). Typechecks + emits entry-less via woc --emit (1767-byte image).
- docs/examples/web-app: manifest with the real [deps] entry (future GitHub
URL as documentation; the gate substitutes a file:// remote) + README
(routes table, run instructions, nginx h2-in-front sketch). Code lands
with Task 4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- error catalog: WO-E106 (dependency fetch/shape failures, one code, message
names dep + step) and WO-E107 (dep/local module-name collision) rows.
- README: a Dependencies subsection under the manifest docs — [deps] syntax,
.wo-deps/wo.lock behavior, offline-when-locked, --update-deps, flat-only.
- board: iteration 15 row -> landed (deps-accept 8/0), pending row removed;
story 15 header records the landing; 08-project-structure notes
.wo-deps (gitignored) + wo.lock (committed); plan checkboxes all ticked.
(One self-inflicted casualty during this task, restored from git before
commit: a buggy doc-edit script truncated 08-project-structure.md; the file
was recovered intact and the intended one-liner applied by hand.)
Gates at closeout: deps-accept 8/0, woc-test 540/0, oop-e2e 87/0,
log-watcher 7/0, employee 8/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5 tasks, words-only per house rule, each with its verify step: (1) manifest
grows the [deps] section + one-line inline-table value (only under [deps]);
(2) resolver — git-binary fetch into .wo-deps/, wo.lock pinning, warm-path
offline guarantee, drift diagnostic, --update-deps, guard rails (transitive
refusal, non-writeonce dep, name collision WO-E107, all fetch failures
WO-E106); (3) multi-root discovery + module_of prefixing dep roots by dep
name + entry restricted to the app's own files; (4) scripts/deps-accept.sh
gate over file:// remotes (8 checks, network-free) + just recipe; (5) docs
closeout (catalog E106/E107, README deps subsection, board/story/structure).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brainstorm outcome (forks locked with the developer):
- TLS: proxy-terminated (nginx/caddy gives browsers TLS+ALPN+h2; the
framework speaks HTTP/1.1 behind it) — zero TLS in the toolchain, no
doctrine fight; homegrown TLS refused outright.
- Dependencies: a real mini package manager — wo.toml [deps] with exact-rev
git deps, wo.lock, .wo-deps cache, `use <dep>` as a module root; fetch by
shelling to the git binary (no network code in woc); flat-only v1.
- HTTP/2: v1 is HTTP/1.1 keep-alive; h2c is the parked successor behind
iterations 8/9f/11 (multiplexing needs a scheduler to pay off); the
bytes/buffer type rides with it, not v1.
- Handler model: no function values by doctrine, so Handler/Middleware are
structural interfaces (ICALL dispatch, WO-E205-checked); middleware returns
?Resp and rides the shipped ?T narrowing.
- Incubation: framework at docs/examples/writeonce-framework/, consuming
storefront at docs/examples/web-app/ importing it THROUGH [deps] — the
sample exercises fetch -> lock -> build -> serve -> durable-restart.
- Iteration 10 relationship: service blocks later LOWER ONTO this library.
Files: specs/2026-08-18-web-framework-design.md (A deps normative, B
framework normative, C h2c parked); stories 15-deps-package-manager.md +
16-web-framework.md; roadmap + board rows.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes json's two documented fidelity limits (iteration 5 strictness):
- field_class gains WOB_FIELD_BOOL (a plain `Bool` field) and
WOB_FIELD_NIL_BOOL (a `?Bool`: WO_NIL_SCALAR nil + bool encoding) — the
kind byte alone cannot tell a Bool slot from an Int slot, so the metadata
carries it. Emitter writes them (field_class_meta); loader whitelists
them; json.c encodes `true`/`false` (and `null` for a ?Bool nil), decode's
null/omitted-key pre-write covers NIL_BOOL.
- A JSON number with a fraction or exponent is MALFORMED for an Int field:
the checked decode (`json.decode(t) as T`) yields nil for the whole
document instead of silently truncating 3.7 to 3 — the language has no
float, and corrupting data quietly was the one thing a "checked decode"
must never do. Floats stay representable through a raw `json.Value` field.
- corpus: run/json-bool-fidelity pins the round-trip (true/false both ways,
?Bool null both ways, fraction AND exponent rejected).
- Board's two known-gap entries struck; format doc's field_class marker list
extended.
Verified: oop-e2e 87/0; runtime test + test-iso OK; woc-test 540/0;
log-watcher 7/0; employee 8/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The verdict table's reject half is enforced: a Haxe habit fails loudly at
its own position with the doctrine reason, instead of a generic syntax
error — or, worst, compiling clean: `return super.f()` used to exit 0 (the
unresolved ident placeholder swallowed it).
- parser.ml: doctrine_reject_reason maps each rejected word to its spec
reason (inheritance quartet -> principle 4; cast; Dynamic/untyped ->
principle 13; macro; extern -> principle 10; operator). Fired at three
chokepoints: `class B extends A` (with skip-to-brace recovery so the body
still parses), an expression head (`super`, `cast 3`, `untyped x`), and a
top-level declaration head (`macro fn`, `extern fn`).
- types.ml: `Dynamic`/`untyped` as a TYPE name keep their WO-E225 site but
carry the doctrine message.
- corpus: compile-fail/{reject-inheritance,reject-cast,reject-dynamic}.
- catalog WO-E105 row; plan 8 Task 8 reject half ticked (#if still open);
board updated.
Verified: woc-test 540/0 + test_diag 14/0; oop-e2e 86/0; log-watcher 7/0;
employee 8/0; legit identifiers (`extended`) untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The hybrid-boundary inversion is closed: a statically provable interface
violation now fails at COMPILE time instead of reaching wovm as an ICALL
that traps WO_T_BOUNDS at runtime.
- types.ml class_satisfies: the same rule emit.ml's `satisfies` builds
vtable rows from (instance method with matching name + parameter count
for every interface method; `static fn` never satisfies) — one rule, two
consumers, so the check and the vtable can never disagree.
- check_iface_boundary fires wherever a confidently class-typed value flows
into an interface-typed slot: call arguments against the callee's declared
parameters (free fns, methods off confident receivers, interface-method
sigs, statics — resolved exactly as confident_typ resolves returns),
annotated `let`s, and `return`s. Silent when underivable.
- The same per-argument pass extends the ?T boundary to CALL ARGUMENTS
(the previous slice covered stores/returns/operands): nil into a
non-nullable parameter is WO-E212, an unnarrowed ?T argument is WO-E211.
- tests/corpus/trap/unsatisfied-interface -> compile-fail/ with
fixture.code WO-E205, per the fixture's own standing instruction; its
header comment rewritten to the wired reality.
- The new arg checks caught a real mistyped signature in the sample:
log-watcher's rpc_error/rpc_result/call_tool declared `id: json.Value`
while every caller legitimately passes nil (JSON-RPC id-absent) — now
`?json.Value`; dispatch/call_tool/cron-row sites moved to the
bind-then-narrow idiom (including an `or`-guard narrowing:
`if spath == nil or spat == nil { return }`).
- Catalog: E205 gains its main-table row; the "owed gap" section is
rewritten as closed. Board known-gap struck through.
Verified: woc-test 540/0 + test_diag 14/0; oop-e2e 83/0 (fixture now
compile-fail, satisfying-class negative probe compiles clean); oop-accept
ALL MET; log-watcher 7/0; employee 8/0; gc-cycle clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The type system now keeps its nullability promise: a `?T` value cannot be
used, stored, or dereferenced as a plain `T` without narrowing. The canonical
evidence probe (return b.v where v: ?Int, fn -> Int) that compiled clean for
months now fails with WO-E211.
- WO-E211 (un-narrowed use): arithmetic and </<=/>/>= operands, and/or
operands (?Bool), interpolation segments, for-iterables, and returns whose
declared type is not nullable.
- WO-E212 (boundary): nil or ?T stored into a non-nullable slot — annotated
let, assignment to a confidently-typed local (cenv, never the placeholder
env — a placeholder target must stay silent) or a resolvable class field.
- WO-E213 (deref): field/index access through a possibly-nil base.
- Narrowing (locals only — a field place can be re-assigned between check
and use, so chains bind to a local first): `if x != nil { }` narrows the
branch; a DIVERGING then-branch (`if x == nil { return }`) narrows after
the if; `x != nil and x.n > 3` narrows and/or right operands
(short-circuit); `while x != nil` narrows the body. The narrow is
un-applied when an else-less then-env leaks out un-diverged (the existing
env-leak convention must not leak the narrow).
- No false positives by construction: env/cenv types are declared or
confidently inferred; the placeholder fallbacks are plain scalars, never
?T. The whole golden suite passed untouched (540/0).
- Samples updated to the bind-then-narrow idiom (log-watcher config decode +
supervisor lock/next_fire, gc-cycle ring print) — 22 genuine unnarrowed-nil
sites; employee needed zero changes. All acceptances green.
- Corpus: compile-fail/{nullable-unnarrowed-use,nullable-nil-into-plain,
nullable-deref-unchecked} + run/nullable-narrowing (all four forms) — 83/0.
- Catalog: E211/E212/E213 move from "Reserved, not yet emitted" to the main
table; nullable-types-implementation.md status flipped to ENFORCED
(historical record kept); plan 8 Task 6 ticked (boxed scalar cells
superseded by WO_NIL_SCALAR); board updated.
Verified: woc-test 540/0 + test_diag 14/0; oop-e2e 83/0; oop-accept ALL MET;
log-watcher 7/0; employee 8/0; gc-cycle ring prints + reclaims.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The spec's §8 migration table, applied:
- 00-principles.md P3: "@gc is a per-class opt-in, reference-counted" ->
GC-ness is inferred; incremental per-shard mark-sweep in budgeted slices;
still no global pause by construction.
- OOP spec: decision-table GC row -> inferred (hybrid rule named); §3 rule 5
-> traced classes alias freely, which classes is inferred; §4 memory model
-> the RC + Bacon-Rajan paragraph replaced by tracing (snapshot roots,
Yuasa barrier, born-black, budgeted slices); header rc comment -> union'd
sweep link; mixing rule restated for tracing.
- 00-wob-format.md: header says version 4; opcodes 27-28 -> reserved (loader
rejects); the owned-temporary rule's @gc exclusion restated for tracing.
- 08-builtin-surface.md: the push RC_INC special case and the set(m,k,v)
retention gap DELETED — neither exists without RC; the corpus cycle is
collected by tracing.
- story 07b: status -> LANDED 2026-08-18 (with the historical note kept);
board: 7b row ✅ (supersedes iteration 2's RC memory model), pending row
removed.
- gc-cycle README: Phase 3 flipped to landed (the ring runs, is reclaimed,
ASan-clean; the ?Node RC_DEC-on-nil trap no longer exists); the barrier
prose corrected to the as-built design (snapshot-at-beginning + deletion
barrier + born-black, not per-slice root re-reads).
- plan 2026-08-18: all checkboxes ticked + a completion banner recording the
four deviations from the plan as written.
(Error catalog was already amended with the keyword-removal commit: WO-E104
added, WO-W201 retired.)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`@gc` is no longer part of the language: a developer never writes or mentions
it. GC-ness is decided entirely by inference (structural cycles + demand
promotion), which the earlier 7b commits made complete and precise.
- parser: `@gc` on a class is now WO-E104 ("GC-ness is inferred; run
`woc --dump-gc`. Remove it."). `is_gc` stays false; the class classifies by
inference. No `.wo` in the repo carries `@gc` anymore.
- types.ml: retired the WO-W201 machinery (suggest_gc_annotation,
has_recursive_structure(_type), has_unique_field, gc_suggestion_code) — it
suggested `@gc`, now obsolete since inference traces exactly those classes.
- runner.ml: deleted the 8 WO-W201 gc-suggestion test blocks; the @gc-exemption
test's `Cache` is made self-referential so inference classifies it gc without
an annotation.
- fixtures: dropped `@gc` from rc.wo (Cache demand-promotes via its escape),
elision.wo (Cache given a self-ref to stay structurally gc for the rc-elision
dump), pricing-demo.wo (PriceCache doesn't escape -> now owned), and the
abandoned-cycle/budget-steps corpus (Node is structurally gc). rc.wo keeps a
placeholder comment line so its line-indexed rc assertions hold. Goldens
re-blessed.
- docs: error catalog gains WO-E104 and marks WO-W201 retired; gc-cycle README
records the keyword removal.
Verified: woc-test 553/0 (was 566 minus the 13 retired WO-W201 checks),
test_diag 14/0, oop-e2e 79/0, employee 8/0, log-watcher 7/0. `git grep '@gc'`
finds only comments — success criterion 1 met.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Structural inference (2a) covers cyclic classes; this adds the DEMAND half for
the acyclic-but-aliased case, so @gc is now redundant everywhere.
- owner.ml: a `promote` sink on ctx. In collect mode, a class value that would
raise WO-E304 (escape) records its class instead of erroring — because a
class that MUST escape cannot be owned (second-class borrows can't be stored
or returned), so it must be traced. `class_of_ty` extracts the class from the
escaping local's type. analyze/analyze_fn take ?promote.
- main.ml typecheck_all: after structural injection, a fixpoint runs ownership
in collect mode over every file, unioning promotions into syms.traced (and
every module table), before owner/emit see it. Terminates (promotions only
grow, bounded by class count).
- gcinfer.render_final: --dump-gc now reads the authoritative is_gc_class
(structural + demand + the remaining @gc bridge), with the reason.
Effect: a class that escapes is inferred `gc` with no annotation — e.g. `Cache`
(rc.wo sans @gc) shows `gc (alias escape (demand))`; `Box` returned out of
`leak` is promoted and the program is valid. No false positives: employee's
Department/Employee stay owned; the 566 goldens + 14 test_diag unchanged.
Corpus: compile-fail/borrow-escape-return reclassified to run/ (prints 1) —
returning a borrowed class is now legal under demand promotion; the fixture
encoded pre-7b behavior.
Verified: woc-test 566/0 + test_diag 14/0; oop-e2e 79/0; employee 8/0;
log-watcher 7/0.
NOT in this slice: removing the `@gc` KEYWORD (parser rejection + rewriting the
RC/@gc golden + test_diag assertions + moving the inference injection into the
library so unit tests see it) — coupled to Phase 3, which deletes the RC
machinery those tests cover. The ring still needs Phase 3 to RUN (nullable
`?Node` gcref path).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GC-ness now comes from the inference pass, not only the annotation. A class is
traced if the structural SCC put it in `syms.traced`, OR (temporary bridge
until demand-promotion lands) it still carries `@gc`.
- Types.symbols gains a `traced : StringSet.t`; is_gc_class reads it (union'd
with the surviving @gc annotation). All symbols literals + both merges carry
the field.
- typecheck_all injects the classification once (Gcinfer.classify -> traced)
into the merged table AND every module table, before typecheck/owner/emit.
- emit.ml routes the class gc-flag and the union/drop decision through
is_gc_class instead of the raw `.is_gc`, so structurally-inferred gc classes
get the runtime flag. Field-kind derivation already routed through is_gc_class.
- gcinfer.traced_names exposes the traced set for injection.
Effect: docs/examples/gc-cycle now COMPILES with no annotation (the WO-E301
use-after-move at the ring-closing store is gone) — traced classes alias
freely. Bytecode is byte-identical to writing `@gc class Node`.
Verified: woc-test 566/0 (goldens unchanged — every current @gc class stays gc
via the annotation branch, and no golden has a structural-gc-non-annotated
class); oop-e2e 79/0 (gc corpus green).
Not in this slice: demand-promotion (the acyclic-aliased PriceCache case still
needs the @gc bridge) and @gc-in-source-as-error (Phase 2b); the ring RUNNING
(the RC runtime doesn't implement nullable-gcref `?Node` fields — Phase 3).
WO-W201 still fires on gc-cycle (retired in Phase 4).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Design-first deliverable for iteration 7b (no runtime/compiler code yet).
docs/examples/gc-cycle explains, by example, how pointers flow through the heap
and the collector's mark-sweep logic:
- types.wo: Node (self-referential ?Node -> inferred `gc`/traced) vs Segment
(acyclic -> `owned`, deterministically dropped)
- main.wo: ring_demo builds a->b->c->a and abandons it; owned_demo shows the
drop path with no collector
- README.md: the model (ownership frees the 99%, tracing only the cyclic/
aliased residue, inference decides), the 16-byte header rewrite (retire
rc+borrow -> 8-byte sweep-list link, colors in flag bits), where a traced
pointer lives (root via pc gc-mask / GCREF field / container), and the
tri-color incremental algorithm with the Yuasa deletion barrier. Two mermaid
step diagrams (heap+roots, collector cycle) + the owned contrast.
Grounded in the approved spec (2026-08-11-inferred-gc-mark-sweep-design.md) and
the real runtime structures (obj.h/wob.h: wo_hdr, WO_K_GCREF, arena, wo_obj_size).
Run status: honest — the sample does NOT build today; woc reports WO-E301
(use-after-move at the ring-closing store), which is exactly the aliasing that
"traced classes alias freely" unblocks under 7b. README records this.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Master now reflects only the current woc/wovm project. The Rust `wo` runtime
was the prior, abandoned architecture; it is fully independent of the woc/wovm
stack (dune + make, no Cargo dependency), so it lifts out cleanly. Recoverable
via git history.
Removed:
- crates/ (29 files) + Cargo.toml + Cargo.lock — the Rust runtime workspace
- prototypes/ — wo-rt-c (a stale duplicate of runtime/) + wo-db C++ ref
- justfile: the rt-c-demo / rt-c-bench recipes (drove prototypes/wo-rt-c)
- docs/plan/05..16 (11 Rust engineering plans) + docs/plan/done/ (4 Rust
Stage-2 done plans)
- docs/runtime/ (11): the old runtime overview + 7-phase DB design series +
async/fibers/gc/surreal concept essays
- docs/cm.md — legacy scratch note
Kept: compiler/, runtime/ (C VM), database/, the current-track docs
(stories, superpowers, plan/{oop-vm,compiler,exploration}, examples), the
discarded/learnings registers, and the syscall/postgres/assembly/c-runtime
studies. Follow-up commits fix the status board, project-structure doc, and
any dangling links to the removed docs.
Verified: woc + wovm still build; woc/wovm --version green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ##ui/.htmlx/LIVE track (plan 7) was removed as stale, so drop its dangling
references in 08-project-structure.md: the `ws, sub, htmlx, assets (plan 7)`
runtime/src line, the `06-ui-live` oop-vm contract entry, the `plan 7
UI/.htmlx/LIVE` server-track step, and `ui` in the build rule-of-thumb.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`client/` (wo-live.js live-patch runtime) never existed and was tied to the
removed plan 7 (##ui/.htmlx UI track). Dropped its tree entry, its proof-layer
section header + bullet, and its lifecycle-table mention (plans 4–7 -> 4–6,
http/ui -> http).
Left as-is: the Rust-track phase prose still names a `ui` crate scaffold and
plan-7 sequencing — that describes the maintained (non-advancing) Rust
roadmap, not a removed file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cleanup off master. None of this is referenced by the current woc/wovm
toolchain or the maintained Rust crates; it belongs to abandoned/scratch
state.
Removed:
- .planboard/ + .planboard.json — planboard task-tracking state for the
haxe-parity plan (tooling scratch)
- prompt.md — a stray one-line note about the old runtime's 13c task
- infra/ (deploy.sh, setup.sh, sync.sh) — deploy scripts for the old Rust
runtime (cargo build -p wo-rt, scp to writeonce.de, nginx/SSL/systemd)
- static/ + templates/ — the v1 blog's .htmlx frontend assets (svg/css +
about/article/home/layout templates); the UI track that used them was
already removed as stale
- content/ data/ wo-data/ — empty untracked v1 runtime dirs
Updated docs/08-project-structure.md: dropped the "v1 blog operating assets"
tree line and section. (cm.md keeps a historical mention of infra/sync.sh as
a legacy note — left as-is.)
Nothing kept references the removed paths (verified tree-wide; crates/rt hits
were `'static` lifetimes, not file reads).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The manifest pinned `[build] runtime = "../../../runtime/wovm"`, a repo-only
relative path that overrides woc's runtime resolution — so `woc <copied-dir>`
failed off-repo (e.g. an installed toolchain on a test server) with "runtime
binary not found".
- Drop the [build] section: the project no longer hardcodes a machine path, so
an installed `woc` self-locates `wovm` beside its own binary.
- The module justfile's `build` recipe now sets WO_RUNTIME=<repo>/runtime/wovm
so the in-repo build still uses the freshly built VM.
- Acceptance is unaffected (it compiles via `woc --emit` + an explicit $WOVM,
never the manifest [build] key).
Verified: just log-watcher::build OK; just log-watcher 7/0; and building a
copied tree with the installed-layout woc from an unrelated cwd self-locates
the sibling wovm and produces a runnable binary.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Analyzed the full 131-file docs tree (4 parallel classifiers) against the
shipped woc/wovm toolchain. Removed 21 stale docs, kept all intentional
history (Rust-track plans/done, the runtime/database design series cited by
current specs, syscall/postgres/assembly/c-runtime studies, discarded/
learnings). Deleted:
- old-runtime "front door": writeonce-pl.md, runtime/wo-language.md
(pitched the Rust wo runtime -- REST/LiveView/SQL+Cypher -- as the current
language; contradicted the new README)
- v1 design set: 02-recovery, 03-data, 04-ui, 05-datalayer,
06-markdown-render, 07-ssl; runtime/database/05-go-sdk
- future-scope/ai-agents-content-management (unfinished old-runtime CMS)
- the ##ui/.htmlx LiveView frontend track (product decision to abandon):
9 plan/exploration/ui/*, plan/14-mvc-ui-implementation,
superpowers/plans/2026-08-01-ui-htmlx-live; 13d pricing-UI board row
Tree left link-clean: 46 dead links to the removed docs neutralized to plain
text or deleted as pure see-also bullets across 20 kept docs; whole-tree
link-resolving scan reports zero links to any deleted file. Removal recorded
in discarded.md; board Frontend section + project-structure tree updated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- README.md now IS the getting-started page (moved from
docs/writeonce.md; single source, no duplication): current
woc -> .wob -> wovm toolchain, system requirements, build, hello-
world, language + stdlib, embedded database, samples, roadmap
- deletes the old README's Rust `wo` runtime pitch (cargo run, axum
REST, Postgres mirror, blog/ecommerce) — that runtime is not
advancing and no longer the project's front door
- content unchanged from the verified doc (hello-world + switch
compiled live before the prior commit)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- docs/writeonce.md — external-developer front door for the current
woc -> .wob -> wovm toolchain (NOT the stale root README's old Rust
wo runtime): what writeonce is, system requirements, how to build
the toolchain, hello-world + the two build paths, language surface,
stdlib, the embedded database, project/manifest layout, samples
- shipped-only by decision: every feature described compiles and runs
today; hello-world + switch snippet verified live before commit;
employee/log-watcher cited as the working acceptance samples
- unshipped roadmap (aggregates, HTTP service, concurrency/fibers,
cross-program attach + keypair auth, blue-green, @derive) kept in a
clearly-separated Roadmap section, plus named current limits (net
TCP-only, proc.run no timeout, no stdin/stdout, no FFI)
- note: root README.md is stale (documents the older Rust axum/REST
runtime, no mention of woc/wovm); left untouched, flagged for the
developer
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- frames a writeonce port of ~/projects/skillhost (C++ MCP host that
links libllama in-process, discovers filesystem skills, runs their
scripts confined) as the sample that drives host capabilities into
the open — the way log-watcher drove the systems stdlib
- names each gap as a candidate iteration (surveyed 2026-08-16 vs the
running compiler + skillhost source):
- Blocker A: in-process native-lib FFI (no FFI today) — fork:
FFI-as-language vs out-of-process model driver over proc/net+json
(llama-server, needs nothing new); leaning out-of-process
- Blocker B: stdio transport — no stdin/stdout builtins; port uses
a TCP socket meanwhile; io.stdin_read/stdout_write a candidate
- Blocker C: bounded/killable subprocess — proc.run has no timeout/
signal/process-group kill; smallest + most broadly useful, do 1st
- partials: recursive fs walk, exec-bit check, symlink-resolving
confinement (realpath) — one small fs-metadata iteration
- records what is already expressible (catalog via @table/9g skill-
catalog, discovery, frontmatter text-parse, config, single-thread
serve, context-gate arithmetic — no VRAM query needed)
- out of scope: in-process libllama/CUDA, VRAM introspection, exact
sampler chain / per-turn memory clear
- roadmap + board rows added
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- method: an embedded-SQL app is a grammar corpus; catalogue what it
actually uses and add only that, translating its statements 1:1 as
the acceptance (the postgres/System.Linq reference pattern applied to
a whole application)
- corpus #1 = ~/projects/skillhost (C++ MCP host, embedded SQLite skill
catalog): surveyed, entire SQL footprint is one file — 1 table, a
single-row parameterized INSERT, 4 SELECTs. 3 of 5 statements already
run on the 9b surface (insert, where name==?, order by name; PK ≈
@unique). Exactly 2 are the real gap:
- whole-query `count` (group-free; the degenerate aggregate, NOT
the parked group-by)
- correlated `not exists` subquery (skillhost's roots-of-the-tree)
- notable finding: skillhost's NOT EXISTS is naturally a `backlink`
emptiness in writeonce (children backlink + len==0), so the corpus
may be fully expressible once len(query) is confirmed — the iteration
may collapse to "confirm len(query) + add exists"; forks record this
- explicitly parks everything skillhost does NOT use (join/having/
offset/distinct/CTE/window/union/upsert/returning/json/fts/triggers)
and the full group-by; each enters only when a corpus demands it
- acceptance: docs/examples/skill-catalog mirroring skillhost's schema
+ its 5 catalog ops as writeonce translations
- roadmap + board rows added
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- FK restrict: deleting a row a non-nullable `ref` still points at traps
WO_T_FK (11), catchable. The compiler now records a `ref` field's
target class in the class-table field_class metadata; the engine
(wo_row_has_referrers) scans referencing scalar columns before a
delete. Correctness-first full scan; the backlink-index optimization
is recorded for later
- docs/examples/employee now COMPILES AND RUNS all six modes against a
WAL-durable database: seed (+@unique trap across restart), report
(per-dept aggregates + payroll), staff (unique probe + backlink +
ref nav), raise (update-through-row), drop (FK restrict), and
persistence via replay
- group-by SYNTAX parked to a future iteration (user decision): the
report mode is hand-rolled from the shipped primitives meanwhile
(same numbers). "table relations and FK" is complete
- scripts/employee-accept.sh (8 checks) + a `just employee` module;
manifest parser tolerates iteration 9c's [share]/[[share.clients]]
sections so `woc .` builds the sample on this branch
- fixtures trap/db-fk-restrict (code 11) + run/db-fk-restrict-catch;
oop-e2e 79/0, woc-test 566/0, 15 runtime suites, log-watcher 7/0,
employee-accept 8/0
- 9b story + status board updated
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- captures the toCSV/reflection thread: principle 13 forbids runtime
reflection, so a generic serializer can't be a user-written function;
Rust answers with derive macros (compile-time codegen), and
json.encode is already a single hand-built instance of exactly that
- iteration generalizes json.encode's mechanism into a reusable derive
facility: @derive(Json/Csv/Eq/Hash/Show) -> the compiler generates
per-type routines from the class-table metadata it already emits,
monomorphic, no runtime type tag, no dynamic dispatch
- closes the query-result-serialization gap
(csv.encode(from e in Employee ... select e)) that has no expression
today; acceptance requires json.encode retrofitted onto the framework
with byte-identical output, and disassembly proving no reflection
- four forks: request surface (lean @derive annotation), invocation
(lean compiler-recognized encode builtins, no UFCS/methods), Eq/Hash
sharing the engine's key comparison, static applicability checking
- out of scope: full trait/typeclass system, user proc-macros, general
generics, cross-channel derive -- a CLOSED compiler-known derivable
set, the pragmatic 80% without the type-system weight
- numbered 13 to echo the principle it lives inside; roadmap + board
rows added
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- 9e durability/throughput/scale: the measurement backbone -- run the
employee program, restart to prove persistence, benchmark read/write
through compiled .wo, ~1M-row mixed load with throughput floor + p99
ceiling + flat RSS; the gate every optimization signs (before/after
delta required, no measured delta = not accepted)
- 9f io_uring group-commit: replace fsync-per-commit with batched
io_uring durability overlapped on shard threads; same ack-after-
durable contract, crash battery unchanged, automatic fsync fallback
on kernels without it; deliberately LAST (needs 8's threads to
overlap and 9e's baseline to beat)
- wired the existing levers into the arc: iteration 8 (thread-per-core)
= "optimize multithreading", 7b (mark-sweep) = "implement GC" --
each now gated by re-running 9e and recording the delta
- explicit sequence recorded in 9e: 9b lands -> 9e baseline -> 7b
re-bench -> 8 re-bench -> 9f re-bench
- roadmap + board rows for 9e/9f; four forks each for the specs
(load generator, absolute vs relative budgets, what "1M" means,
durable vs RAM headline; ring model, liburing vs raw, batch
boundary, fallback testing)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Task 6 note: db fixtures live in existing corpus kinds; crash battery
proven at unit level; select fixtures wait on 9b's read surface
- board row updated
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- wo_row_update_field: encode new value, unique re-check against a
shadow BEFORE any mutation (violating update leaves the row
untouched, DB_ERR_UNIQUE), index entries moved old-hash -> new-hash,
old engine value freed; proven by test_table (unique refusal keeps
the row, released key becomes insertable)
- WAL UPDATE record: full-row re-log, replay = replace (remove +
re-create same id); prefix/suffix delta recorded as later
optimization; test_wal replays insert+update to the updated state
- builtins 62 DB_UPDATE_FIELD (cid,id,field,value) and 63 DB_DELETE
(cid,id), commit-before-ack like insert, WO_T_UNIQUE/WO_T_DB/WO_T_IO
mapping; dispatch range 61..63; loader arities; runner mirror
- plan Task 5 marked superseded-in-part with the recorded deviation:
the language surface (reads, queries, row views, delete statement)
is 9b's, where the comprehension design put it -- no interim brace-
select grammar to retire later
- gates: test_table 839/0, test_wal 102/0, 15 suites, oop-e2e 73/0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- docs/examples/employee-list: attaches to the running employee
program; modes list / report (byte-identical to A's own) /
staff <dept> / probe-write (registered read-only, insert must trap
access-denied, exit 4, A unchanged)
- the manifests ARE the design, written as a pair: A's [share] gains
listen = unix socket beside WO_DATA plus [[share.clients]] naming
B's public-key fingerprint with rights = "read"; B's
[connect.employee] carries A's ipc string, A's PINNED fingerprint,
and project = ../employee for compile-time shapes -- the connect
section's name is the code's namespace (employee.Employee)
- fingerprints are PASTE-HERE placeholders by design: keys generate
into WO_DATA at first boot (9d), tomls carry fingerprints only,
printed by --identity
- sample-first: compiles after 9/9b/9c/9d; README maps each mode to
the acceptance line it exists for; 9c/9d stories now name this
sample as their workload
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- promotes 9c's identity fork to its own iteration: program identity
is a keypair (first-boot generated into WO_DATA, 0600, printable
fingerprint); A's [share] grants name PUBLIC KEYS, B pins A's key
in [connect.a]; mutual challenge-response, fresh nonces, transcript-
hash signing (protocol tag + fingerprints + nonces + channel)
- acceptance criteria: registered-key attach carries 9c rights
unchanged; unregistered key refused pre-statement with fingerprint
logged; same-uid-wrong-key refused (uid SUPERSEDED, not
supplemented); impostor on A's socket path aborted by B's pinned-key
check; handshake replay refused; rotation = manifest change
- four forks recorded: crypto provenance (lean: vendored compact
Ed25519 as the one sanctioned vendored component), keygen home
(lean: first-boot into WO_DATA), signed-transcript layout, uid
survival (lean: keys only, peer-cred demoted to log enrichment)
- out of scope: transport encryption, CA machinery, key escrow,
root-attacker protection
- plan folds into 9c's when specced (neither ships alone); 9c fork 3
marked superseded-as-end-state; roadmap + board rows added
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- program B attaches to running program A's persistent database via an
IPC string in B's wo.toml [connect.<name>]; A registers clients by
name with read / read+write rights in its [share] manifest section;
unregistered = refused at connect, under-privileged = catchable trap
- doctrine preserved: A stays the single writer -- B's statements
execute inside A through the same choke-point row API, B never
touches A's WAL or slabs; typed statements checked by B's compiler
against A's table shapes, schema handshake at attach
- four forks recorded for the spec: channel carrier (lean: unix
socket + SO_PEERCRED), how B's compiler learns A's shapes (lean:
project reference + live handshake), grant granularity (lean:
whole-db rights, name+uid identity), blocking semantics (lean:
blocking round-trip, stop-flag rule applies)
- acceptance sketch: employee sample as A, a thin employee-report
client as B (read-only GroupBy over the wire) + audit-log writer
exercising the rights matrix
- slots after 9b (shares its typed surface), before 10 (HTTP is the
external face; this is the writeonce-native one); prior art:
04-client-api.md wire protocol + the WAL's value encoding
- roadmap + status board rows added
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- compiler: `insert Class { ... }` is a typed Ast.Insert in statement
AND expression position, sharing the ctor literal's field grammar;
typechecked with the ctor's omittable rule; result = the row id (Int)
- owner pass: the engine copies at the row API, so an insert BORROWS
its field values -- no transfer, no E304; node is trap-capable and
carries a live-mask drop entry like DbStub did
- emit: builtin 61 window = class-id const + one slot per DECLARED
field in declaration order; omitted defaults emitted, omitted ?scalar
gets WO_NIL_SCALAR, other omitted optionals the zero word; fresh
argument values reaped after (the push/set copy semantics)
- runtime: database/src/db.c executes via the choke-point row API;
rt.db/rt.wal opaque handles on wo_rt; WO_DATA=<dir> = replay
<dir>/shard-0.wal at boot + commit-before-ack per statement (the
builtin's return IS the ack until iteration 8 ticks); failed commit
un-applies the row and traps WO_T_IO; loader validates the class-id
slot (variable window documented in wob.h + format doc)
- the promised diff: trap/pricing-set-price-db-stub is now
run/pricing-set-price-insert printing engine-allocated ids;
durability smoke prints 1,2 then 3,4 across two WO_DATA runs
- old "bare insert is an Ident" unit test rewritten to the new
contract; runner's loader mirror accepts id 61; goldens re-blessed
- gates: oop-accept ALL CRITERIA MET, oop-e2e 71/0, woc-test 566/0,
wovm-test green, log-watcher 7/0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- database/src/wal.{c,h}: framed records len|crc32|payload|mark
("WOL1" written last -- no mark, no record), typed-row payloads
walking the class-table kinds (nested records, containers, nil
encodings), little-endian like the loader
- commit order verbatim from the shipped phase-D pattern: RAM apply,
stage, ONE pwrite + ONE fdatasync for the batch, ack after -- group
commit is everything staged riding one sync
- replay decodes straight into engine-owned values (no VM at boot)
and re-enters rows through the choke-point row API, so Task 4's
indexes will rebuild for free; next_id advances past replayed ids
this shard owns (wo_row_create_raw)
- torn tail = short/CRC-fail/no-mark/zero-len: intact prefix applies,
tear dropped whole, wo_wal_open positions AT the tear so the next
commit overwrites it; CRC-valid-but-undecodable = corruption, loud
- wo_wal_check: offline oracle, no engine needed -- the crash
battery's verifier
- test_wal 90/0 ASan+UBSan incl. five crash-battery rounds (fork,
insert/commit/ack-over-pipe, SIGKILL mid-stream: zero acked-but-
missing, zero acked-but-wrong); all runtime suites green, oop-e2e
71/0; binding doc WAL section + CODE-LOGIC + plan Task 2 checked
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- database/src/table.{c,h}: per-shard per-class slabs (256 rows,
malloc'd, never moved -- row addresses stable for 9b's row views),
occupancy bitmap, LIFO slot reuse, open-addressing id hash with
tombstones (ids never 0, never reused)
- field encoding walks the same .wob class-table kinds the VM walks:
scalars raw (WO_NIL_SCALAR passes through), Texts copied to db_text,
owned objects flattened recursively to db_rec, containers
element-wise; GCREF refused at encode (the GC bulkhead, defensively)
- two one-way copy gates: insert copies VM values in, read allocates
fresh VM values out -- no VM pointer in a slab, no slab pointer in
the VM, proven by mutating originals after insert
- id discipline: per table per shard, S+1 step N; owner = (id-1) % N;
N-parametric, runs at N=1 until iteration 8, tested at N=3
- choke points: wo_row_insert/wo_row_remove carry the INDEX HOOK
sites Task 4 attaches to; nothing else mutates storage
- runtime/Makefile links database/src into every wovm + test binary
- test_table 827/0 ASan+UBSan; oop-e2e 71/0; log-watcher 7/0;
binding doc docs/plan/oop-vm/04-db-binding.md; CODE-LOGIC.md beside
the code; plan Task 1 checked off
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- 9b spec gains section 6 "Ownership, borrows, and GC across the
engine boundary": two one-way copy gates (no VM pointer enters a
row, everything a select returns is copied out), so the collector
never traces engine memory and the engine never touches refcounts
- row views are borrows WITHOUT a runtime net: rows share the VM's
field encoding but not its header, so no borrow word backs them --
the compile-time escape rule is load-bearing alone
- cursor stability settled: scans materialize their id list before
the body, row updates through the view stay legal (raise mode
updates an indexed column mid-scan and is the proving fixture),
insert/delete on a table with an open cursor is a new WO-E5xx
- GC-pause interaction recorded: collector runs between statements,
a long scan delays slices -- accepted, documented
- iteration-7b ordering constraint: GC inference must classify before
table-field validation, diagnostic names the inference reason --
noted in 7b story, iteration-9 plan constraints, 9b plan tasks
- stories 09/09b Info sections point at the analysis; 9b plan Tasks
3/5 carry the enforceable checkboxes (ASan boundary assertion)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- docs/examples/employee: Department/Employee @table classes with
@unique, [dept] and [dept, salary] indexes, ref/backlink pair;
modes seed/report/staff/raise/drop per the 9b spec section 6 —
written AHEAD of the features (sample-first, like log-watcher);
README states it does not compile on today's toolchain and links
the plans that compile toward it
- report mode is the GroupBy showcase: group-and-reduce projection
{headcount, avg, min, max} ordered by avg desc, whole-query sum
for payroll; staff proves both navigation directions + index probe;
drop proves delete restrict (trap asserted)
- engine directory decision (user, 2026-08-15): database/ is its own
top-level dir, statically linked into wovm — file structures updated
in the iteration-9 plan and the 9b plan
- gap found by writing the sample: iteration 9's subset lacks a
`delete` statement and restrict needs one — added to 9b plan Task 3
- 9b plan Task 6 notes the sample is pre-authored and authoritative
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- spec settles 9b's three forks: SQL/Cypher layer superseded as the
program surface (design history + wo-db engine-semantics reference);
comprehension syntax desugared at compile time (no function values);
System.Linq = operator vocabulary + edge cases, PostgreSQL = execution
+ integrity vocabulary (both references surveyed 2026-08-15)
- aggregate semantics normative: count/sum total 0 on empty, avg/min/max
are ?T with nil (empty is data, not a fault); nil skipped; sum wraps
like language arithmetic; GroupBy lowers as group-and-reduce
(AggregateBy shape), transition/finalize ABI from nodeAgg
- relations: ref = FK with direct-index-probe check (nil passes,
unchanged-key skips), backlink = secondary-index scan, delete is
restrict-only; nil never joins, nil is a legal group key
- lowering: the compiler is the planner — queries become bytecode loops
over cursor/group builtins, longest-prefix index selection, no plan
tree, no SQL text in the image (disassembly-provable)
- plan: 6 tasks gated by a new docs/examples/employee sample
(Department/Employee, @unique, composite index, ref/backlink,
GroupBy report mode) with its own acceptance script + crash step;
blocked on iteration 9's engine plan
- story 09b + status board updated; 02-wo-language.md carries the
supersession note
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- docs/examples/{agent-loop,blog,ecommerce,hello,mcp-think,pricing}
removed; every deleted tree is preserved on branch
cleanup/non-logwatcher-examples (snapshot of this branch pre-delete)
- justfile: hello/pricing/pricing-demo/pricing-pg-demo/hello-demo
recipes removed with the examples they served (Rust-runtime demos);
rt-c-* prototype recipes and every gate recipe stay
- crates/rt parser test article_debug: the blog fixture it read from
disk now lives inline, same shape, so cargo test needs no example
- gates after cleanup: cargo test -p rt 69/0, oop-e2e 71/0, woc-test
green, log-watcher 7/0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- docs/examples/log-watcher/justfile carries build/accept/soak; the
root mounts it with `mod log-watcher`, so `just log-watcher` still
runs the acceptance (default recipe) and every doc reference stays
valid; `just log-watcher::build` / `::soak 60` reach the rest, and
plain `just build` works from inside the directory
- ROOT is source_directory()-based: inside a `mod`,
justfile_directory() names the ROOT justfile's directory and every
path would miss
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- a directory carrying wo.toml is a PROJECT: `woc .` inside it (or
`woc path/to/project` from anywhere) reads the manifest and produces
<target>/<name>, exactly what `woc build <dir> -o ...` produces
- schema is the one the sample already carried -- top-level name/
version/description, [runtime] wo (accepted, not yet enforced) --
plus a new [build] section: runtime (wovm to prepend) and target
(output dir, default "target"), both relative to the manifest's own
directory so the build is invocation-point independent
- unknown keys and sections are hard errors: a typo'd key silently
ignored would build the wrong thing
- directories WITHOUT wo.toml keep check-only semantics -- the corpus
is full of those; oop-e2e 71/0, woc-test 565/0, log-watcher 7/0
- sample's wo.toml gains the [build] section; target/ gitignored
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Task 5: net.close on every path out of a serve iteration (400
included) and the listener on stop; measured 4 -> 54 fds over 50
requests before, 4 -> 4 over 200 after. The loop's comment claimed
the iteration-end drop IS the close -- wrong twice (net.Conn is a
scalar, and a drop would not close an fd); it now says what is true
- Task 6: LW_SOAK=<seconds> in the acceptance script -- each mode under
load, resident+descriptor deltas against a WARMED baseline (warm-up
includes load: cold-to-high-water is not growth), 256 KiB / zero
tolerance; LW_ACCEPT_WOVM soaks another build
- the soak caught ~1.6 MiB/min of in-arena leaks ASan cannot see (the
arena is one allocation to LeakSanitizer); an arena size-class
census + pointer trace attributed five bugs:
- jparse_string sized every decoded string at "rest of the input"
and relabeled len after -- blocks filed on free lists their next
allocation never reads (fs.read_all's mis-size, again); copy out
exact, free at the taken size
- `!=` never dropped fresh operands (headers["authorization"] !=
"Bearer ${key}" leaked both sides per request); Ne now reaps as Eq
- an Int interpolation segment is a fresh int_to_text, not a borrow;
is_borrowed_value_t asks the segment's type
- json.encode(Ctor{...}) had no owner -- record + both field copies
leaked per tool call; its bespoke lowering now drops the argument
- a discarded expression statement owns its result: `pop(lines);`
leaked the popped element; reader builtins excluded
- after: arena live bytes flat per request on every handler; release
soak 30 s per mode watch 0 / run 0 / mcp +20 KiB, descriptors flat;
ASan build flat at 14600 KiB across 601686 requests in 90 s past its
~1200-request quarantine warm-up
- gates: oop-accept ALL CRITERIA MET, oop-e2e 71/0, woc-test 565/0,
wovm-test green, log-watcher 7/0 (soak opt-in, fast path <1 min)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- blocking stdlib calls that PARK (net.accept, socket read/write,
time.sleep, a child wait) no longer restart the syscall when the
stop flag is set on an interruption: a server sitting in accept
ignored SIGTERM and only `kill -9` ended it
- a stop is NOT a trap -- builtin.h's WO_SYS_STOPPED carries no error
record and no catch handler sees it (`try` must not swallow
SIGTERM); the VM unwinds the whole stack through the same drop
machinery an uncaught trap uses, so nothing leaks on the way out
- wo_vm_call gained a third outcome (1 = stopped); the CLI maps it to
the status the program's own `return 0` would have given, and a
regular-file read keeps its plain EINTR retry -- it does not park
- an ASSIGNMENT was not an ownership boundary: `api_key =
j.mcp.apiKey` moved the field pointer into the local, so the local
aliased the record and the first unwind freed the same string twice
(SIGSEGV in class_free). `let` copied a Text place, assignment now
does too -- the same double free was latent on the normal exit path,
hidden by the order the compiler happens to emit drops in
- log-watcher-accept is 7 checks: the seventh is the stop itself, with
the hard kill demoted to a fallback whose use is the failure
- measured under ASan: mcp parked, mcp after traffic, watch and run
all exit rc 0 with zero leaks; SIGINT behaves as SIGTERM
- gates: oop-accept ALL CRITERIA MET, oop-e2e 71/0, woc-test 565/0,
wovm-test green, log-watcher 7/0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- program mode built the entry's `multi Text` of arguments and never
freed it: the entry only BORROWS a parameter (never a `take`, and
the drop tables never drop one), so the runtime that built the
container owns it
- dropped after the entry returns and after a trap alike -- the
container outlives the unwind; `multi_free` recurses, so the
argument strings go with it
- one site covers both invocation shapes: `self_rc` picks the argv
offset, it does not build a second container
- measured: watch, run and the full MCP mix now report ZERO leaks
under ASan -- the clean baseline the soak (Task 6) reads against
- gates: oop-accept ALL CRITERIA MET, oop-e2e 71/0, wovm-test green,
log-watcher 6/0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- the drop tables track bindings only, so six shapes had no owner: a
comparison operand (`if parse_expr(s) == nil` abandoned a schedule
record and its five containers per cron line), a borrowed call
argument (a 1 KB string per MCP request), a container read's copy,
a loop's iterable, a projected record, and any of those escaped by
a `return` from inside the statement that built them
- `c[i]` is the one place expression whose register holds a COPY:
no second copy at a boundary (`let u = tokens[0]` copied twice and
abandoned the first), and a drop where every other place is left be
- never drop an argument register after a CALL — the callee's frame
overlaps it; the reap moved into call_window's pre-call stash
- a statement-owned temporary is parked in a LOCAL slot: a loop
reclaims every temp for its body, and the end-of-statement DROP was
releasing the loop counter instead of the record
- reader builtins (get/latest/key_at/val_at) keep arg0 alive — their
result points into it — but their key argument is ordinary
- measured: run 2 112 B -> 64 B, flat 8 s to 20 s; MCP mix 21 312 B /
63 -> 64 B / 1; every handler flat from 2 to 6 requests; the 64 B
left is Task 3's argv container
- gates: oop-e2e 71/0, woc-test 565/0, wovm-test green, log-watcher 6/0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Measured on the workload's supervisor mode, eight seconds, clean SIGTERM exit:
run 1 051 040 B in 24 allocations -> 2 112 B in 19; watch 128 B in 2 -> 64 B in
1. corpus 71/0, woc runtest 565/0, wovm unit gates green, just log-watcher 6/0.
- owner.ml: `oclass_of` called `Text` a builtin scalar, so it was Copy and NO
Text local was ever dropped — that, not the missing stdlib table, was the
leak. Text is now Owned, which forces an answer for what it does at an
ownership boundary, and the answer is uniform: it is COPIED. Into a
container (push/set/`m[i] = v`, already true), into a field (SETF), out of a
function (return), into a binding (`let s = other`), and into a loop cursor.
The source keeps its value; a freshly built Text stays the caller's and is
dropped at the site
- owner.ml: resolve_callee answers for three shapes it never knew — reserved
stdlib members, builtins, and a class's `static` members — so their results
get a type, an owner and a drop
- vm/builtin: WO_B_TEXT_COPY, the one new builtin the rule needs; SETF copies a
TEXT field in; emit copies a Text read out of a container, bound from a
place, returned from a place, or loaded into a cursor, and drops a freshly
built one after a copying store
- sysio.c: fs.read_all/net.read allocated their cap then relabelled the buffer
with the short length — but wo_str_free sizes a block by its len (no size
headers, obj.h), so a 1 MiB buffer wearing a 30-byte length went onto a
32-byte free list and never came back. They copy out at the true size now
- two regressions the corpus caught, fixed in the same pass: a @gc value read
out of a container is a plain borrow, not an rc-counted alias; and push's @gc
escape is keyed on "push is not a user-declared fn" rather than "the callee
did not resolve", which stopped being true once builtins resolved
- docs: Task 1 closed in the executable plan with its before/after numbers, and
the status board's item 1 records the deeper root cause
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every remaining item is now traced to a measurement on the sample; anything the
sample does not exercise is deferred by name with the measurement that says so.
- new plan docs/plan/compiler/2026-08-14-logwatcher-executable.md — six tasks
between "it runs" and "you can leave it running": the ownership pass learning
stdlib return types (>1 MB leaked in 8s of `run` mode, one fs.read_all
result), dropping a projected temporary (`for e in parse_dir(d).entries`
leaks the shell per rescan), the runtime's own argv container (128 B every
run), honouring the stop signal in blocking calls (a server in accept ignores
SIGTERM), closing accepted connections (net.close exists, unused), and a soak
that would have caught all of it. Opens with the measured starting point and
closes with an explicit out-of-scope list
- story 7 (log-watcher proof): status banner separating the met compile-and-run
half from the executable half, plus a new Given/When/Then — clean SIGTERM
exit, zero leaks, flat RSS and descriptors across a soak
- story 5: grammar half landed, strictness half deliberately deferred
- story 6: landed for the surface the workload uses, with the two lifetime
defects it exposed pointed at the new plan
- story 7b: recorded as off this workload's path, measured — the sample has no
@gc class, 0 RC_INC/RC_DEC against 78 DROPs
- 00-status.md: NEXT PLAN is the executable list; story table and in-progress
row point at the new plan; deferrals carry their evidence
- plan 8 (haxe-parity): banner now says on hold behind the executable plan, and
its task states are corrected — Task 5 shipped, Tasks 6 and 7 are half done
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- 08-builtin-surface.md: containers copy a TEXT element/key/value; a freshly
built Text is the caller's to drop, a value read out of a place keeps its
owner; OWNED/GCREF still move and set's @gc retention gap stays open
- 00-status.md: the borrowed-Text double free is struck through as closed by
copy-on-push, with the concrete failure it fixed; new gap recorded — a
blocking accept/read swallows SIGTERM, which belongs to iteration 8's event
loop rather than a patch to the blocking calls
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by driving the compiled log-watcher's third path — the MCP server. It now
answers real JSON-RPC over HTTP: initialize returns protocolVersion/serverInfo,
tools/list returns the full tool list (1049 bytes of generated JSON), and an
unauthorized request gets 401 {"error":"unauthorized"}. corpus 71/0, woc 565/0,
wovm gates green.
- lexer: `\r` and `\0` escapes. Without `\r` a program cannot write CRLF at
all — the server's `index_of(buf, "\r\n\r\n")` was searching for a literal
backslash-r, so it never found a header terminator and hung on every request
- parser: an interpolated sub-expression now mints node ids from the OUTER id
space. A fresh sub-parser started at 1, so `${...}` nodes collided with the
file's own nodes — and every side table (drops, moves, rc, masks, f_decl) is
keyed by node id. Surfaced as WO-E404 "ownership table names `headers`,
which has no register"; silent misattribution otherwise
- types.ml: `net.Conn` is a reserved SCALAR type (a file descriptor). It was
falling through as "some user class", i.e. WO_K_OWNED, so the frame would
DROP an integer at scope end
- types.ml: confident_typ knows `..` yields Text. Interpolation desugars to a
Concat chain, so without it every interpolated value looked underivable —
which is why the `+`-on-Text check missed two live sites in the workload
- `m[k]` on a map is now the OPTIONAL read (nil for a missing key), while
`get(m, k)` stays the asserting one that traps KEY. That is what makes
`let v = m[k]; if v != nil` — the workload's header lookup — work.
trap/missing-map-key now pins `get(...)`, and the surface doc records the
split
- json.encode of a `json.Value` emits it verbatim (kind 255): an echoed id was
coming back as "1" instead of 1
- disasm: TRY/ENDTRY render instead of ?OP32/?OP33
- status board: the push-of-a-borrowed-Text gap is now recorded with the
concrete failure it produces (tools/call tail_log), plus the leaked
temporary-record shell found in the same disassembly
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by running the compiled log-watcher, not by reading code: the supervisor
rejected every cron line ("malformed schedule: * * * * *") because a `*` field
expands to 0 and `?Int`'s nil was also 0, so `a == nil` was true for a real
value. Both log-watcher subcommands now behave: `watch` alerts on a live file,
`run` reports SCHEDULE /var/log/backup.log: * * * * *. corpus 71/0, woc 565/0,
wovm gates green.
- a nullable SCALAR (?Int/?Bool/?Timestamp/?Id) spells nil as WO_NIL_SCALAR
(-2^62), not the zero word. Heap-shaped optionals keep 0 — a null pointer is
unambiguous. The value is -2^62 and NOT INT64_MIN on purpose: the compiler's
integers are OCaml's 63-bit natives, so INT64_MIN is not expressible there
(and `min_int * 2` silently wraps to 0 — the first attempt did exactly that)
- the class table marks such fields (WOB_FIELD_NIL_SCALAR in field_class), so
the runtime writes the right absence where it produces absence itself:
json.decode leaving a key absent or seeing `null`, and parse_int on
unparseable input (so parse_int("0") is now distinguishable from a failure).
json.encode renders a nil scalar as JSON null
- emit.ml: `nil` takes its word from its destination (annotation, field,
return type); a comparison against `nil` emits the literal with the other
operand's type, so ?scalar compares against the sentinel and ?heap against 0
- vm.c: EQS accepts a nil operand — two `?Text` values compare with it, and the
answer is "both absent is equal, one absent is not". Trapping there made
`a != b` on optionals unusable (it was trapping BOUNDS "null text" in the
supervisor's rescan). A non-nil operand must still be a real Text
- docs: both normative docs now state the heap-vs-scalar nil split and the EQS
rule; the stale duplicate vm_unwind comment is gone
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- docs/00-status.md: NEXT PLAN is now iteration 5's strictness half (?T forced
handling, pub(read) writes, using, #if) plus an ASan run over the workload;
iterations 6 and 7 marked landed; a "Landed 2026-08-14" section records what
actually shipped, and a new known-gaps block records what did not — lenient
optionals, unenforced pub(read), the borrowed-Text-into-container hazard,
json's Bool/float limits, net fd lifetime, no ASan over the workload, and no
corpus fixtures for the new surface (by direction: the sample is the test)
- runtime/src/CODE-LOGIC.md: file map, the loader-is-the-only-validator and
traps-never-leak invariants, the catch stack, records the VM fills but cannot
name, class metadata + json, program mode, and where to look when it breaks
- compiler/src/CODE-LOGIC.md: the pipeline, why there are two type derivers and
the stay-silent-when-underivable rule, how contextual values get a
destination, the node-id/label contract between owner.ml and emit.ml, the
four kinds of qualified call, predeclared records, the constant-interning
trap, register discipline, and how to verify a change
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs/examples/log-watcher now COMPILES AND RUNS: `wovm lw.wob watch app.log 2 1`
tails a live file, classifies levels and fires its alert
("last entry is error, quiet for 2s"). corpus 71/0, woc 565/0, wovm gates green.
- program mode: the entry is `fn main` taking nothing or one `multi Text`;
runtime/src/main.c builds that list from the program's own arguments (not
the program name, not the image path) and the entry's return value is the
process exit code (low byte); the loader accepts a 0- or 1-arg free-fn entry
- `+` on Text is now WO-E201 pointing at `..`. This was a memory-safety hole,
not a style nit: the emitter lowered it to ADD on two heap pointers, and the
workload's own `out = out + char_of(c)` produced a wild pointer that
segfaulted the VM inside starts_with. Reported off confident types only
- `x == nil` / `x != nil` lower to EQ (a word compare), never EQS: nil is the
zero word and EQS dereferences its operands, so a nil guard would trap
instead of answering
- docs/examples/log-watcher: seven `+`-on-Text sites corrected to `..`
(logtail sanitize, mcp header/body/carry assembly, supervisor detections
line) — the sample was carrying the Haxe habit, and the language reserves
`+` for arithmetic by doctrine
- wovm CLI takes arguments after the image path (`wovm <file.wob> [args...]`)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs/examples/log-watcher (1285 lines, 7 files) now compiles clean: 0
diagnostics, a 35KB .wob written. corpus 71/0, woc runtest 565/0, every wovm
unit gate green (both dispatch flavors).
- .wob v2: each class row gains three u32 per-field arrays — the field's NAME
constant, the CLASS it refers to (or the json-raw marker), and a container
field's ELEMENT kinds. json is then a runtime service driven by metadata
instead of per-type generated code. loader/emitter/disassembler/test
assembler all read and write v2; field-name constants are interned with the
rest of the pool (interning during serialization silently loses them)
- runtime/src/json.c (new): encode by static kind + object headers + class
table (nested records need no static knowledge); decode parses and BINDS
straight into the target class — keys matched to field names, nested objects
built as the field's class, arrays as a multi of the field's element kind,
unknown keys skipped, absent keys nil. Malformed input is nil, never a trap
- `as`: `json.decode(text) as T` is the one cast this language has (WO-E403
for any other `as`, and for a bare json.decode with no target type). Its
result is `?T`, which is why the decode and the target are one instruction
- json.Value: a reserved type name for a value the source does not inspect —
the raw JSON slice, kind TEXT, re-emitted verbatim by encode
- docs: 00-wob-format.md is now the v2 reference (class metadata, TRY/ENDTRY,
the whole builtin surface, WO_T_IO); 08-builtin-surface.md documents the
text/container builtins, the OS modules with their predeclared records, and
json's two documented limits (Bool encodes 0/1, floats truncate)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Driving goal: compile docs/examples/log-watcher (1285 lines of .wo).
Diagnostics on that program: 481 -> 379; parse errors 85 -> 18.
tests/corpus via scripts/oop-e2e.sh stays 71 checks / 0 failures.
- ast.ml: `Let.ty` is a full `field_ty` (was a bare name), so `multi Text`,
`map<K, V>` and `?T` annotate a local; new `ListLit`/`MapLit` expressions;
`method_decl.is_static`; `field.pub_read`
- parser.ml: let annotations go through parse_field_ty; `[]`/`[a, b]` and
`{}` literals in expression position; `static const`/`static fn` in class
bodies (one token of lookahead — `static` stays an identifier);
`pub(read) field: T`; a `;` ends a statement on its own, so one-line
guard bodies (`{ skip(...); return; }`) parse
- emit.ml: list/map literals lower to multi_new/map_new + one multi_push per
element, element kinds from the destination's declared type (WO-E403 with
no typed destination, same rule multi_new already had); `static fn` gets a
receiverless method record (arg_cnt = params) and lowers `Cls.fn(args)`
through emit_direct with no `self`; a static can satisfy no interface
- types.ml: a written `let` annotation is now the authority for the binding's
type (the only thing that types `[]`/`{}`/`nil`); `static_method_of` +
static-call return types; method_info.is_static is wired from the AST
- owner.ml: container literals are fresh owned values, elements read in place
(inherits push()'s open borrowed-element gap, noted in the code)
- plan doc: `Spec:` header line so planboard's lint accepts the plan
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Modules: `use`/`pub`, directory-as-module, per-module symbol resolution (a
flat first-wins merge silently ran the wrong `pub fn` body), six reserved
stdlib namespaces typed UNKNOWN-BUT-RESERVED.
- Surface: `and`/`or` (own precedence tier, short-circuit, Bool-only), `${}`
interpolation desugared at parse time, `const`, break/continue with
drop-correct exits, do-while, inline-fn rejection.
- switch expr/stmt: required `default` over scalars/Text, arm unification,
EQ/EQS+JZ lowering, per-arm drop scopes with N-way JOIN-DROP; `default`
sorted last by a shared lowering order (textual order made arms dead).
- typedef records: structural, same shape = one class entry; `?name: T`
nullable-by-shape; emit_ctor fills omitted defaults; `type` as field name.
- Enum variants: all-bare unions = int ordinals; any-payload = one class
entry per variant, tag IS the header class_id (no header field, no format
bump); exhaustive switch without `default`; arity checked both directions.
- Payload escape modeled as move-out (pointer-kind fields only — a scalar
escape is a copy); caller reaps owned heap temps passed by borrow: two
unbounded LSan-blind leaks, 10.5 MB -> 1.5 MB flat over 300k iterations.
- Fixed en route, each with a RED repro: dead E209 builtin-arg check and
`int_to_text` missing from both types.ml builtin tables (both segfaulted
wovm), multi-file phantom double-report, emit_ctor's field temp clobbering
dst in tail position (pre-existing), warnings swallowed without an error.
- Two fenced VM builtins: `int_to_text` (13), `variant_tag` (14).
- 14+565 unit (was 14+401), corpus 71 (was 32) plain and under wovm_asan,
wovm-test + cli_smoke green. Log-watcher 307 -> 93 diagnostics (85 E101 /
4 E207 / 1 E208 / 3 W202); the 5 non-E101 residuals await Task 7 grammar.
- `woc` now emits `.wob` that `wovm` runs: emit.ml lowers the typed,
owner-annotated AST (scope-stack registers with a >64 WO-E401 diagnostic,
Lua-style call windows, ICALL by slot, dedup const pool, drop maps, line
tables, implicit terminators); disasm.ml backs `--dump-bc` goldens.
- Ownership lowering consumes the four owner tables verbatim; RESIDUAL is the
only source of borrow ops, coalesced per operand. Review caught the emitter
consuming only 2 of owner.ml's 4 residual producers — an assignment-anchored
aliasing violation ran to exit 0 instead of trapping; fixed, plus a backstop
raising WO-E404 for any residual region left unconsumed.
- Conformance harness `scripts/oop-e2e.sh` (`just oop-e2e`): four fixture
kinds with exact outcomes — byte-exact stdout, one WO-E### anchored on
`error CODE:`, numeric trap code, gc trace. 25 fixtures incl. pricing-demo
logic, the ownership suite, and DB_STUB's parse-but-trap. `tests/` un-ignored
so the corpus is actually tracked.
- `woc build` produces a self-contained binary: wovm copy + appended image +
20-byte trailer, self-exec via /proc/self/exe. Verified relocated outside
the repo, argless, and against adversarial trailer corruption.
- Milestone 1's five spec criteria all MET (`just oop-accept`). Criterion 3
closed by WO-E405 — the entry must return `Int`, since program mode already
says its return value is the exit code — which deletes the leak class
without adding return-type metadata to the format. `gc/held-cycle` retired:
an externally-held cycle is not expressible in a post-exit pump.
- New spec: inferred GC + incremental per-shard tri-color mark-sweep, retiring
`@gc` and reference counting. Story gains iterations 7b (that work) and 9b
(`@table`, relations, compiler-checked query); `.dev/reference` gains a
sparse System.Linq checkout. Priority: 5→6→7 (log-watcher) then 7b, 8, 9, 9b.
- Board renamed docs/plan/00-kanban.md -> docs/00-status.md and rebuilt: ▶ NEXT
PLAN pointer (iteration 4 — emitter, corpus, `woc build`) then six buckets —
stories, in progress, done, pending, discarded, learnings. It covered only the
Rust runtime before, so the whole OOP track was invisible. All 16 inbound refs
repointed; `Kanban:` banners renamed to `Status:`.
- New discarded.md (settled rejections with reasons: inheritance, `abstract`,
Money/SKU/Float, Dynamic/cast/macro/extern, AOT-to-C, Menhir, shared engine
state) and learnings.md (plumbed≠enforced, vacuous goldens, exit-0-wrong-
output, malloc-path ASan trick, deferred checks that never reach the VM).
- RECOVERED docs/plan/exploration/blue-green-vm/00-vision.md — gone from disk,
never committed (gitignored path), cited by five docs incl. principle 12.
Root cause was broader: all seven forward-roadmap plans in
docs/superpowers/plans/ were untracked and ignored, on one disk only. Dropped
the docs ignore rules with a do-not-re-add note; added __pycache__/*.pyc.
- Repaired broken links across docs/, 270 -> 36: fixes a regression from the
earlier reference/ -> .dev/reference/ move (relative paths at ../../ and
deeper were skipped), plus depth and reorg drift. The 36 residual point at
content that does not exist and need decisions, not paths.
- New spec docs/superpowers/specs/2026-08-10-logwatcher-gap-closure-design.md,
applied: `and`/`or` verdict row; Part 3 gains `env` (six modules), swaps
time.mono for iso/local, adds 22 bare core builtins; throw/time.mono/is cut
(0 uses in the sample). Plan 8: Task 2 gains and/or, Task 5 drops throw,
abstract+`is` task deleted, 8/9 renumber to 7/8. Plan 9 gains core builtins.
Plan 10 gains the 307 -> 0 diagnostic gate. WO-E205 re-filed unreachable-by-
design. types.ml header drops its false satisfaction-set claim. 00-code-
review.md reduced to a stub — its rival Phase 1-4 roadmap retired.
- 00-kanban.md rebuilt: ▶ NEXT PLAN pointer (iteration 4 — emitter, corpus,
`woc build`) then six buckets — stories, in progress, done, pending,
discarded, learnings. It tracked only the Rust runtime before, so the whole
OOP track (wovm shipped, woc Tasks 1-8 shipped) was invisible.
- Board now records iteration 3's known gaps instead of silently owing them:
`?T` plumbed but unenforced; E205/E201/E203/E204 dead, so structural
interface satisfaction is unchecked.
- New discarded.md — settled rejections with reasons so they are not
re-proposed: inheritance, `abstract` newtypes, Money/SKU/Float, Dynamic/cast/
macro/extern, AOT-to-C, Menhir, shared engine state, external deployer.
- RECOVERED docs/plan/exploration/blue-green-vm/00-vision.md — gone from disk,
never committed (gitignored path), cited by five docs incl. principle 12.
- Root cause was broader: all seven forward-roadmap plans in
docs/superpowers/plans/ were untracked and ignored, on one disk only. Rules
were half-fiction — 33 of 34 exploration files were already tracked, so they
swallowed only *new* files.
- Dropped the docs ignore rules (exploration, oop-vm, superpowers/plans,
examples/agent-loop, examples/mcp-think) with a do-not-re-add comment; added
__pycache__/*.pyc. `tests/` stays ignored but warns that the next plan lands
the corpus there.
Completes plan 2 Tasks 7-8. owner.ml: mutable-value-semantics flow analysis
producing the four plan-3 emitter tables (moves, drops incl. LIVE-MASK for trap
unwinding, rc with elision, residual borrow sites) plus WO-E301-304 two-site
diagnostics. Alias questions run over canonicalized places, so a double-mut
reached through let-bound aliases lands in the residual table like the direct
form; dump.ml's contract notes the emitter must coalesce guards per operand.
main.ml: directory discovery, cross-file programs (symbols merge before bodies
check), diagnostics ordered by (file,line,col), new WO-E214 for a name declared
in two files. New docs/plan/oop-vm/01-error-catalog.md (14 emitted + 10 reserved
codes), un-ignored so both plan tracks can cite it; justfile regains woc-*.
builtin_scalars is now the five that work: Int, Bool, Text, Timestamp, Id.
Money/SKU/Float and the abstract_types allowlist are gone — `abstract` never
lexed, and Float had no literal syntax and no wob kind, so no value could exist.
Fixtures and samples retype Money->Int, SKU->Text. The abstract newtype feature
is rejected outright (verdict row adopt->reject); haxe-parity Task 7 keeps `is`.
nullable-types-implementation.md corrected: ?T is plumbed but UNENFORCED
(E211-213 declared, never emitted; probe exits 0), handed to haxe-parity Task 6
as next work item. Records all 10 dead codes incl. E205 — interface satisfaction
is unchecked. crates/rt keeps its Money/SKU fixtures (opaque strings, Stage 2).
Gate: build warning-clean, 14 + 264 checks 0 failures, pricing golden exit 0,
docs/examples histograms unchanged (13/70, zero WO-E225).
- ast.ml: Added Nullable variant to field_ty; param.ty/method_sig.ret/method_decl.ret now use field_ty
- parser.ml: Parse ? prefix for field types, return types, parameters
- dump.ml: Render ?T in --dump-ast output
- types.ml: New typechecker (Task 6) with nullable field-kind derivation (WO_K_NULLABLE=6)
- dune: Added types module
- Added golden test fixtures for nullable types and statement/expression parser
- Added implementation plan doc
prototypes/wo-rt-c — single-file C reference of the writeonce runtime
layer, zero deps beyond libc + kernel uapi: thread-per-core io_uring
event loops (raw syscalls, no liburing), SO_REUSEPORT listeners, one
mlock'd mmap arena sharded by address, WAL dual-write with group commit
(HTTP ack only after the fsync CQE), boot-time snapshot + WAL replay
recovery, and a bench harness with a Go net/http comparison server.
Measured on 20 cores: 908k reads/s p99 154us and 643k fsync-acked
commits/s p99 177us on 8 shards, vs Go net/http 495k/355k (no
durability) on 20 cores. Crash-under-load testing found and fixed an
ack-before-fsync race and an fd-reuse ABA hazard in commit-ack parking.
docs/plan/exploration/c-runtime — the phased plan (00, exit evidence
per phase), the one-address architecture trace (01), and the
single-binary end-goal contract (02). justfile carries the demo and
bench recipes; .gitignore covers binaries and data dirs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>