feat(serve+view): file serving, downloads, supported systems; rename

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
shoney.arickathil 2026-08-25 04:41:00 +02:00
parent 24245438cf
commit 47a920f19a
62 changed files with 310 additions and 158 deletions

View file

@ -269,7 +269,7 @@ dependencies, declared in the manifest:
```toml
[deps]
niceframework = { git = "https://github.com/shoneyj/niceframework", rev = "v0.1.0" }
niceserve = { git = "https://github.com/shoneyj/niceframework", rev = "v0.1.0" }
```
`woc` fetches each dep (via the `git` binary) into `.wo-deps/<name>/`, pins
@ -303,7 +303,7 @@ acceptance tests:
just employee # compile + run every mode against a durable database
```
- **`docs/examples/writeonce-framework/` + `docs/examples/web-app/`** — a web
- **`docs/examples/writeonce-serve/` + `docs/examples/web-app/`** — a web
framework written in writeonce (HTTP/1.1 behind a TLS-terminating proxy,
router with `:param` captures, interface-based handlers) and a storefront
consuming it **as a `[deps]` dependency**, with `@table` persistence. Run:

View file

@ -332,7 +332,7 @@ Things worth knowing before editing them:
- **`{{ e }}` desugars to `esc(${e})`, resolved by ordinary name
lookup.** `desugar_interp` in `parser.ml` builds a `Call` on an
`Ident "esc"` — precisely what a developer wrote by hand before. The
compiler learns nothing about HTML, `esc` stays wo-html's ordinary
compiler learns nothing about HTML, `esc` stays writeonce-view's ordinary
`pub fn`, a typo'd field inside the hole is a normal name/type error,
and a locally defined `esc` shadows deliberately (a custom escaper is
a feature). `${ }` inside the same literal stays raw — that is the

View file

@ -79,7 +79,7 @@ the number or produce the benchmark.
| 22's battery never run | 22 is ⬜ "needs a spec first"; no `bench/baseline.json`, no `just db-bench`; `runtime/bench/` is the retired C prototype's harness |
| TSan covers one demo | only `scripts/fibers-accept.sh` builds and runs `wovm_tsan` |
| no fuzzing, no CI | no `.github/`, no fuzz target |
| one framework, five samples, one consumer | exact: `writeonce-framework`; employee, employee-list, fibers, gc-cycle, log-watcher; `web-app` |
| one framework, five samples, one consumer | exact: `writeonce-serve`; employee, employee-list, fibers, gc-cycle, log-watcher; `web-app` |
### Consequence

View file

@ -38,19 +38,19 @@ fn render() -> Text {
}
```
Every `render()` makes its class a **component** — wo-html's structural
Every `render()` makes its class a **component** — writeonce-view's structural
`Component` interface, satisfied by having the method, never declared.
Parent components hold children directly (`cards: multi Component`) and
render them with `render_all`, so `ProductListPage` knows nothing about
`ProductCard` beyond `render()`. The document itself is a component too:
`AppShell { title, content }` in `layout/app.wo`, which links a real
stylesheet rather than inlining one — that is why it is its own shell and
not wo-html's `Layout`.
not writeonce-view's `Layout`.
Two holes, and the difference is the whole escaping story:
- `{{ expr }}` **HTML-escapes** — it compiles to a call to the `esc` in
scope (wo-html's, unless the app declares its own). Display data goes
scope (writeonce-view's, unless the app declares its own). Display data goes
here; a typo'd field is a compile error, not a broken page.
- `${ expr }` is **raw** — for markup you built yourself, like the
`${stock}` fragment above or `${content}` in the app shell.
@ -70,7 +70,7 @@ up, or sandbox.
| `product_list/view.wo` | VIEW — classes with `fn render() -> Text`, fields = exactly what is displayed | `product-list/view.html` |
| `product_list/controller.wo` | CONTROLLER — query the model, fill the view, answer a `Resp`; beside its view in the same module | component `.ts` + service |
| `product_page/`, `orders/` | one module per feature: `view.wo` + `controller.wo` | feature folders |
| `static_files/controller.wo` | `/assets/*` from disk, traversal-safe, typed | `angular.json` assets |
| `/assets/*` | served by the framework's `StaticFiles` — the template no longer carries its own copy | `angular.json` assets |
| `assets/style.css` | ONE real stylesheet, sectioned per feature | the `.scss` files |
| the `render()` bodies | ONE raw text literal each: real newlines, real double-quoted attributes, source indentation removed at compile time, `${}` raw holes and `{{ }}` auto-escaping ones | Vue `<template>` (in-SFC) |
| `layout/app.wo` → `AppShell` | the document, as a two-slot component | `app.component.html` |
@ -94,12 +94,12 @@ it looks like.
this directory was its consumer. Styles stay a real CSS file, served
statically (there is no scss preprocessor).
- **No closures, no DI.** A view is a class with fields + `render()`
(wo-html's `Component`); a controller is a class satisfying `Handler`.
(writeonce-view's `Component`); a controller is a class satisfying `Handler`.
Capture = a field.
- **The MVC seam is enforced by where the query sits.** Controllers hold
every `from … select`; a view receives VALUES — for the list page, a
`multi Component` of already-filled cards. No view in this template
touches the database, and wo-html contains no query at all.
touches the database, and writeonce-view contains no query at all.
- **No sessions/cart yet.** Buying is per-product (qty → order). A cart
needs a session story that does not exist yet.
- **No client-side JS.** Every interaction is a form round trip.

View file

@ -2,11 +2,11 @@
-- wrapping every page with the shared header and footer. Styles are NOT
-- inlined — pages link /assets/style.css (served by static_files), so
-- markup and styling stay separate files.
use framework/http
use html
use serve/http
use view
-- The app shell as a COMPONENT (wo-html's `Component`: fields in, Text
-- out), the same shape as wo-html's own `Layout` — two slots instead of
-- The app shell as a COMPONENT (writeonce-view's `Component`: fields in, Text
-- out), the same shape as writeonce-view's own `Layout` — two slots instead of
-- four, and its own document rather than `page()`'s, because this
-- template deliberately LINKS a stylesheet instead of inlining one.
-- That is the whole reason it is not just `Layout`: markup and styling

View file

@ -4,12 +4,12 @@
--
-- WO_DATA=./data ./target/shop 8080 (run from the shop directory:
-- /assets/* serves from ./assets)
use framework
use framework/router
use serve
use serve/http
use serve/router
use product_list
use product_page
use orders
use static_files
fn seed_if_empty() {
let n = 0;
@ -44,6 +44,8 @@ fn main(args: multi Text) -> Int {
app.get("/p/:sku", ShowProduct {});
app.post("/orders/:sku", CreateOrder {});
app.get("/orders", ListOrders {});
app.get("/assets/*path", StaticFiles { dir: "assets" });
-- the framework's file server (serve/http): 2 MiB ceiling is well
-- above this template's stylesheet.
app.get("/assets/*path", StaticFiles { dir: "assets", max_bytes: 2097152 });
return app.serve("127.0.0.1", port);
}

View file

@ -1,7 +1,7 @@
-- orders/controller.wo — the buying flow: stock-checked order creation
-- (decrement + insert are each WAL-committed before they acknowledge)
-- and the orders list (ref navigation: o.product.name).
use framework/http
use serve/http
use layout
use time

View file

@ -1,7 +1,7 @@
-- orders/view.wo — the confirmation page and the orders table.
-- Raw text literals for the bodies; see product_list/view.wo for the
-- convention.
use html
use view
pub class OrderOk {
name: Text

View file

@ -3,7 +3,7 @@
-- directory = one module, view and controller together. The @tables it
-- queries live in the root module, which is fine: a CLASS is reachable
-- across module lines (only free `fn`s are module-scoped).
use framework/http
use serve/http
use layout
pub class ListProducts {

View file

@ -4,7 +4,7 @@
-- source indentation removed at compile time. `${}` interpolates raw,
-- `{{ }}` HTML-escapes — so display data goes through `{{ }}` and never
-- through a hand-written esc() call.
use html
use view
pub class ProductCard {
sku: Text

View file

@ -1,5 +1,5 @@
-- product_page/controller.wo — the CONTROLLER for /p/:sku.
use framework/http
use serve/http
use layout
pub class ShowProduct {

View file

@ -1,7 +1,7 @@
-- product_page/view.wo — the product detail view with the order form.
-- Raw text literals for the bodies; see product_list/view.wo for the
-- convention.
use html
use view
pub class ProductPage {
sku: Text

View file

@ -1,33 +0,0 @@
-- static_files/controller.wo — serves /assets/* from disk. Traversal-
-- safe (any ".." answers 404, never touches the filesystem), extension-
-- mapped content types, 2 MiB cap per file. Text is binary-safe, so
-- images travel as-is. (Story 38 lifts this into the framework; until
-- then the template carries its own copy — it is ~40 lines.)
use framework/http
use fs
pub class StaticFiles {
dir: Text
fn handle(req: Req) -> Resp {
let rel = req.params["path"];
if rel == nil {
return not_found();
}
if index_of("${rel}", "..") != -1 {
return not_found();
}
let body = try fs.read_all("${self.dir}/${rel}", 2097152) catch (e) nil;
if body == nil {
return not_found();
}
let ct = "application/octet-stream";
if ends_with("${rel}", ".css") { ct = "text/css; charset=utf-8"; }
if ends_with("${rel}", ".js") { ct = "text/javascript"; }
if ends_with("${rel}", ".svg") { ct = "image/svg+xml"; }
if ends_with("${rel}", ".png") { ct = "image/png"; }
if ends_with("${rel}", ".webp") { ct = "image/webp"; }
let h: map<Text, Text> = {};
h["content-type"] = ct;
return Resp { status: 200, headers: h, body: "${body}" };
}
}

View file

@ -8,5 +8,5 @@ wo = ">= 0.1"
# Two library dependencies, the site sample's proven shape. The [deps]
# KEY is the module name `use` imports.
[deps]
framework = { git = "https://github.com/shoneyj/writeonce-framework", rev = "v0.1.0" }
html = { git = "https://github.com/shoneyj/wo-html", rev = "v0.1.0" }
serve = { git = "https://github.com/shoneyj/writeonce-serve", rev = "v0.1.0" }
view = { git = "https://github.com/shoneyj/writeonce-view", rev = "v0.1.0" }

View file

@ -10,7 +10,7 @@ samples now read the same way.
| --- | --- | --- |
| `types.wo` | MODEL | the `Chapter` `@table`, the `ChapterLink` projection, `Chapters.links()`, and `seed_if_empty()` |
| `content.wo` | MODEL (content) | the nine chapter bodies as fragment-returning functions, plus `seed_chapters()` |
| `layout/app.wo` | VIEW (chrome) | `AppShell` — the component that fills wo-html's `Layout` — the two named widths, and `html_error` |
| `layout/app.wo` | VIEW (chrome) | `AppShell` — the component that fills writeonce-view's `Layout` — the two named widths, and `html_error` |
| `layout/header.wo`, `layout/footer.wo` | VIEW (chrome) | the shared nav bar (brand = mark + wordmark) and footer |
| `layout/logo.wo` | VIEW (chrome) | the mark as inline SVG, plus the `<head>` links |
| `install/view.wo`, `install/controller.wo` | VIEW + CONTROLLER | `/install` — the toolchain guide. Static copy, so `InstallPage` has no fields |
@ -43,7 +43,7 @@ class — recorded gap #1 — but nothing needs it to.)
- **Chapters are rows, not constants.** `seed_if_empty()` inserts them
only when the table answers empty, so a WAL restart keeps admin edits
instead of reseeding over them — the sample's own proof of chapter 6's
claim. The seed bodies are BUILT with wo-html's builders at boot; after
claim. The seed bodies are BUILT with writeonce-view's builders at boot; after
that the table is the truth and the builders are never consulted again.
- **The seam is enforced by where the query sits.** `Chapters.links()`
lives with the MODEL and hands the view a `multi ChapterLink` —
@ -87,10 +87,19 @@ class — recorded gap #1 — but nothing needs it to.)
hole; it is written with `&#123;` entities instead. This is the same
limitation the chapter code samples hit with `${`, and the reason both
doors exist.
- **Downloads are the framework's, not the site's.** `/dl/*path` is
`StaticFiles` mounted in `main.wo` with a 16 MiB ceiling — no
controller, because there is no decision to make. `WO_DIST` says where
the tarballs are (default `./dist`). The install page offers the GitHub
release as primary and this as a mirror, with the `.sha256` beside it.
- **The supported-systems list is read off the binaries**, not off a
wish list: `file` gives the triple, and the highest `GLIBC_` symbol
version they import gives the libc floor (2.38 today). Overstating
support costs a reader an afternoon.
- **`SITE_HOST` picks the interface.** Loopback by default — right behind
a proxy — with the env var for reaching a dev instance across the LAN.
The bound address is printed at startup.
- **wo-html's sheet is static.** Tailwind's class NAMES, one hand-written
- **writeonce-view's sheet is static.** Tailwind's class NAMES, one hand-written
CSS string inlined per page by `page()` — self-contained responses, no
toolchain; growing the sheet is appending a line in `tw_css()`.

View file

@ -2,13 +2,13 @@
The language tutorial, served BY the language. One binary carries the HTTP
server, the router, the pages and the database; the chapters you read are
rows in a `@table`, the markup is built by the `wo-html` dependency, and
rows in a `@table`, the markup is built by the `writeonce-view` dependency, and
the whole thing is chapter 9's own example.
```
[deps]
framework = { git = "https://github.com/shoneyj/writeonce-framework", rev = "v0.1.0" }
html = { git = "https://github.com/shoneyj/wo-html", rev = "v0.1.0" }
serve = { git = "https://github.com/shoneyj/writeonce-serve", rev = "v0.1.0" }
view = { git = "https://github.com/shoneyj/writeonce-view", rev = "v0.1.0" }
```
## Run it
@ -29,6 +29,9 @@ SITE_HOST=0.0.0.0 SITE_TOKEN=change-me WO_DATA=./data ./target/site 8080
`GET /packages/<name>` — the package catalogue with copy-paste
`[deps]` lines and usage.
- `GET /favicon.svg` — the mark, inline SVG, no asset pipeline.
- `GET /dl/<file>` — release tarballs, served by the framework's
`StaticFiles` from `$WO_DIST` (default `./dist`, where `just dist`
writes them).
- `POST /admin/ch/<slug>` — edit a chapter (`title`/`body`, form-encoded,
`authorization: Bearer $SITE_TOKEN`). Edits are WAL-durable under
`WO_DATA` and replay on restart — that is chapter 6, demonstrated by
@ -54,7 +57,7 @@ MVC, laid out exactly like the program template
| `layout/logo.wo` | the mark as inline SVG: one source for the nav brand and `/favicon.svg` |
| `main.wo` | bootstrap: seed, routes, serve — nothing else |
Every `render()` makes its class a component (wo-html's structural
Every `render()` makes its class a component (writeonce-view's structural
`Component`). `HomePage` and `ChapterPage` each take the chapter nav as
an already-built child component in a slot, so neither knows what a
chapter is; `ChapterNav` is therefore literally the same component on the
@ -64,7 +67,7 @@ current.
The seam that keeps it honest: **every query lives in a controller.**
`chapter_links()` sits in `chapter.controller.wo` and hands the views a
`multi ChapterLink` projection — no component in this sample touches the
database, and wo-html contains no query at all.
database, and writeonce-view contains no query at all.
One feature = one directory = one module, holding that feature's view
and its controller. The `@table` lives in `types.wo` at the root and is
@ -96,5 +99,5 @@ Chapters 1–9 teach the language (values, containers, classes, optionals,
tables, actors, deps, serving); the app itself exercises the framework's
routing/:params, the Logging middleware, bearer auth (mechanism from
`http/auth.wo`, policy here), `form_values`, `@table` + query + update by
assignment, and `wo-html`'s escaping/builders/Tailwind-style utility
assignment, and `writeonce-view`'s escaping/builders/Tailwind-style utility
sheet — self-contained pages, no CDN, no JS, no build step.

View file

@ -2,7 +2,7 @@
-- form-encoded, bearer-gated. Mechanism (bearer_token, constant-time
-- ct_eq) is the framework's; POLICY — which routes, which token — is
-- this app's, right here. No rendering: the answer is a redirect.
use framework/http
use serve/http
pub class AdminEdit {
token: Text

View file

@ -1,7 +1,7 @@
-- chapter/controller.wo — the CONTROLLER for `/ch/:slug`. It queries the
-- model and fills the view components that sit beside it in this module;
-- a view receives VALUES, never a cursor.
use framework/http
use serve/http
use layout
pub class ShowChapter {

View file

@ -6,7 +6,7 @@
-- links), so it renders identically on the homepage and on a chapter
-- page — the only difference is which ord is `current`. Reuse is the
-- same component with different fields, never copied markup.
use html
use view
-- `ChapterLink` is the MODEL's projection type (types.wo); a class is
-- reachable across module lines, so the view just names it.

View file

@ -1,12 +1,12 @@
-- content.wo — the tutorial chapters, seeded into the Chapter table on
-- first boot (types.wo's seed_if_empty). Model CONTENT, so it sits in
-- the root module beside types.wo: it inserts rows. Bodies are HTML fragments
-- BUILT with the wo-html dep — prose in el(), code samples through
-- BUILT with the writeonce-view dep — prose in el(), code samples through
-- code_block() which escapes them. Editing a chapter later (the admin
-- route) overwrites body/title in place; the WAL keeps the edit across
-- restarts, which is exactly chapter 6's lesson demonstrated by the
-- site that teaches it.
use html
use view
fn ch_hello() -> Text {
let b = el("p", "leading-relaxed mb-4",
@ -61,15 +61,15 @@ fn ch_actors() -> Text {
fn ch_deps() -> Text {
let b = el("p", "leading-relaxed mb-4",
"Dependencies are git repositories pinned in <code>wo.toml</code>; <code>wo.lock</code> " .. "records the exact revision, and locked builds work offline. The [deps] KEY names the " .. "module you <code>use</code>. This site has two: the web framework, and the wo-html " .. "library that rendered the page you are reading.");
"Dependencies are git repositories pinned in <code>wo.toml</code>; <code>wo.lock</code> " .. "records the exact revision, and locked builds work offline. The [deps] KEY names the " .. "module you <code>use</code>. This site has two: the web framework, and the writeonce-view " .. "library that rendered the page you are reading.");
b = b .. code_block(`
[deps]
framework = { git = "https://github.com/shoneyj/writeonce-framework", rev = "v0.1.0" }
html = { git = "https://github.com/shoneyj/wo-html", rev = "v0.1.0" }`);
serve = { git = "https://github.com/shoneyj/writeonce-serve", rev = "v0.1.0" }
view = { git = "https://github.com/shoneyj/writeonce-view", rev = "v0.1.0" }`);
b = b .. code_block(`
use framework
use framework/http
use html
use serve
use serve/http
use view
-- html's builders + tailwind-style utilities, zero JS, no build step:
let body = el("h1", "text-3xl font-bold", "Hello");

View file

@ -1,6 +1,6 @@
-- favicon/controller.wo — GET /favicon.svg. The one route that answers
-- something other than HTML or text, so it builds its own Resp.
use framework/http
use serve/http
use layout
pub class Favicon {

View file

@ -1,6 +1,6 @@
-- health.controller.wo — GET /health: the liveness probe the accept
-- script and any proxy poll. Text, not HTML, on purpose.
use framework/http
use serve/http
pub class Health {
fn handle(req: Req) -> Resp {

View file

@ -5,7 +5,7 @@
-- It reaches the chapter nav through `use chapter` and the query through
-- the model's `Chapters.links()` static — a class crosses module lines,
-- a free fn does not.
use framework/http
use serve/http
use layout
use chapter

View file

@ -1,10 +1,10 @@
-- home/view.wo — the VIEW for `/`. A component: fields in, Text out.
-- Everything on this page is static copy EXCEPT the chapter list, so
-- the one field is that list's already-built component — content
-- projection, the same slot pattern wo-html's `Layout` uses. HomePage
-- projection, the same slot pattern writeonce-view's `Layout` uses. HomePage
-- therefore knows nothing about chapters, the Chapter table, or how the
-- nav decides which entry is current.
use html
use view
pub class HomePage {
chapter_nav: Component

View file

@ -1,6 +1,6 @@
-- install/controller.wo — GET /install. Nothing to query: the page is
-- static copy, so the controller only wraps it in the shell.
use framework/http
use serve/http
use layout
pub class ShowInstall {

View file

@ -1,7 +1,7 @@
-- install/view.wo — the VIEW for /install. Static copy: no fields, so
-- the component has none. It is still a component, and still renders
-- through the same interface as every other page.
use html
use view
pub class InstallPage {
fn render() -> Text {
@ -13,43 +13,58 @@ pub class InstallPage {
language runtime to install on the machines you deploy to, and no
build toolchain beyond these two files.</p>`;
let s1 = section("1. Get the toolchain",
`<p class="leading-relaxed mb-4">Download the release tarball and extract
it into <code>/usr/local</code>. Run this as root, or through
let sys = supported();
let s1 = section("1. Download",
`<p class="leading-relaxed mb-4">One tarball, two binaries. Take it from
the GitHub release, or from this site as a mirror — they are the same
bytes, and the checksum below proves it:</p>
<p class="mb-4">
<a class="btn no-underline" href="https://github.com/shoneyj/writeonce/releases/download/v0.1.0/writeonce-0.1.0-linux-amd64.tar.gz">Download 0.1.0 (linux-amd64)</a>
<a class="btn-outline no-underline" href="/dl/writeonce-0.1.0-linux-amd64.tar.gz">Mirror</a>
</p>
<p class="leading-relaxed mb-4">Verify it before you extract it. The
expected digest is served beside the archive at
<a href="/dl/writeonce-0.1.0-linux-amd64.tar.gz.sha256">.sha256</a>:</p>`,
code_block("curl -O https://writeonce.de/dl/writeonce-0.1.0-linux-amd64.tar.gz\ncurl -O https://writeonce.de/dl/writeonce-0.1.0-linux-amd64.tar.gz.sha256\nsha256sum -c writeonce-0.1.0-linux-amd64.tar.gz.sha256"));
let s2 = section("2. Extract it",
`<p class="leading-relaxed mb-4">Extract into <code>/usr/local</code>,
replacing any previous install. Run this as root, or through
<code>sudo</code>:</p>`,
code_block("rm -rf /usr/local/writeonce\ntar -C /usr/local -xzf writeonce-0.1.0-linux-amd64.tar.gz"));
let s2 = section("2. Put it on your PATH",
let s3 = section("3. Put it on your PATH",
`<p class="leading-relaxed mb-4">Add one line to your
<code>$HOME/.profile</code> (or <code>/etc/profile</code> for every user
on the box), then restart your shell:</p>`,
code_block("export PATH=$PATH:/usr/local/writeonce/bin"));
let s3 = section("3. Check it",
let s4 = section("4. Check it",
`<p class="leading-relaxed mb-4">Both should print the same version.
<code>woc</code> finds <code>wovm</code> beside itself, so a tarball
install needs no further configuration.</p>`,
code_block("woc version # writeonce 0.1.0 linux/amd64\nwovm --version # wovm 0.1.0"));
let s4 = section("4. Your first project",
let s5 = section("5. Your first project",
`<p class="leading-relaxed mb-4">A project is a directory with a
<code>wo.toml</code> manifest and one or more <code>.wo</code> files.
Nothing else — no lockfile to create by hand, no scaffolding step.</p>`,
code_block("mkdir hello && cd hello\n\ncat > wo.toml <<'EOF'\nname = \"hello\"\nversion = \"0.1.0\"\n\n[runtime]\nwo = \">= 0.1\"\nEOF\n\ncat > main.wo <<'EOF'\nfn main() -> Int {\n print(\"hello, writeonce\");\n return 0;\n}\nEOF"));
let s5 = section("5. Build and run",
let s6 = section("6. Build and run",
`<p class="leading-relaxed mb-4"><code>woc &lt;dir&gt;</code> emits ONE
standalone binary at <code>target/&lt;name&gt;</code>. Copy that file to a
server and run it — the VM is inside it, and so is the database.</p>`,
code_block("woc .\n./target/hello # hello, writeonce"));
let s6 = section("6. Add a dependency",
let s7 = section("7. Add a dependency",
`<p class="leading-relaxed mb-4">Dependencies are git repositories pinned
by revision. The <code>[deps]</code> KEY is the module name your code
<code>use</code>s. <code>woc</code> writes a <code>wo.lock</code> with the
exact revision, and a locked build works offline. See
<a href="/packages">Packages</a> for what is available.</p>`,
code_block("[deps]\nhtml = { git = \"https://github.com/shoneyj/wo-html\", rev = \"v0.1.0\" }"));
code_block("[deps]\nview = { git = \"https://github.com/shoneyj/writeonce-view\", rev = \"v0.1.0\" }"));
let note = el("div", "bg-white rounded-lg border shadow-sm p-6 mt-8",
el("h2", "text-lg font-bold mb-2", "Where things go") ..
@ -62,10 +77,30 @@ pub class InstallPage {
<code>[runtime] wo = "&gt;= 0.1"</code>; <code>woc</code> refuses to build
a project that needs a newer toolchain than itself.</p>`);
return head .. s1 .. s2 .. s3 .. s4 .. s5 .. s6 .. note;
return head .. sys .. s1 .. s2 .. s3 .. s4 .. s5 .. s6 .. s7 .. note;
}
}
-- What the release actually runs on. Every claim here is read off the
-- shipped binaries (`file`, and the highest GLIBC_ symbol version they
-- import), not off a wish list — an install page that overstates its
-- support costs someone an afternoon.
fn supported() -> Text {
return `
<div class="bg-white rounded-lg border shadow-sm p-6 mb-8">
<h2 class="text-2xl font-bold mb-2">Supported systems</h2>
<ul class="list-disc pl-6 leading-relaxed">
<li class="mb-2"><b>Linux on x86-64</b> — the only target built today. There is no ARM, macOS or Windows build.</li>
<li class="mb-2"><b>glibc 2.38 or newer.</b> The binaries link the system C library dynamically and import symbols up to <code>GLIBC_2.38</code>. That covers Ubuntu 24.04+, Debian 13+, and Fedora 39+ — and rules out Ubuntu 22.04 (2.35), Debian 12 (2.36) and RHEL 9 (2.34).</li>
<li class="mb-2"><b>Not musl.</b> Alpine needs a build against musl, which does not exist yet.</li>
<li class="mb-2"><b>Nothing else.</b> No JVM, no Node, no Python, no package manager. The two binaries and libc are the whole dependency list.</li>
</ul>
<p class="leading-relaxed mt-4 text-gray-700">Check yours with
<code>ldd --version</code>. If it is older than 2.38, build from source
until a wider-compatibility release exists.</p>
</div>`;
}
-- One numbered step: heading, prose, and the commands to run.
fn section(title: Text, prose: Text, code: Text) -> Text {
return el("div", "mb-8",

View file

@ -1,13 +1,13 @@
-- layout/app.wo — the app shell: one component wrapping every page with
-- the shared header and footer. Unlike the shop template, this site
-- INLINES its stylesheet (wo-html's `page()` does that), so the shell
-- fills wo-html's own `Layout` rather than writing its own document.
-- INLINES its stylesheet (writeonce-view's `page()` does that), so the shell
-- fills writeonce-view's own `Layout` rather than writing its own document.
--
-- The only thing that varies between pages is the container width, so
-- that is the one extra slot — and the two widths are named once, here,
-- instead of as class strings scattered through the controllers.
use framework/http
use html
use serve/http
use view
pub class AppShell {
title: Text

View file

@ -1,5 +1,5 @@
-- layout/footer.wo — the site footer, shared by every page.
use html
use view
pub fn footer() -> Text {
return el("div", "footer", "writeonce.de — served by the language it teaches. " .. "One binary: compiler, runtime, database, this page.");

View file

@ -1,5 +1,5 @@
-- layout/header.wo — the site navigation bar, shared by every page.
use html
use view
pub fn header() -> Text {
let links = link("/install", "text-gray-700", "Install");

View file

@ -1,11 +1,12 @@
-- site — writeonce.de: the language tutorial, served BY the language.
-- Full stack in one binary: writeonce-framework ([deps]) for HTTP/routing/
-- auth, wo-html ([deps]) for server-rendered pages with Tailwind-style
-- Full stack in one binary: writeonce-serve ([deps]) for HTTP/routing/
-- auth, writeonce-view ([deps]) for server-rendered pages with Tailwind-style
-- utilities, @table + WAL for the chapters themselves. The site is its own
-- final chapter: /ch/serving shows this file's shape.
--
-- SITE_TOKEN=... WO_DATA=./data ./site 8080
-- SITE_HOST=0.0.0.0 ... ./site 8080 (reachable from the network)
-- WO_DIST=/srv/dist ... (where /dl serves tarballs from)
--
-- Behind nginx/caddy for writeonce.de: the proxy terminates TLS and
-- forwards to 127.0.0.1:8080 (the framework speaks HTTP/1.1 keep-alive).
@ -14,8 +15,9 @@
-- model is types.wo; every feature is a directory holding its view and
-- its controller.
use env
use framework
use framework/router
use serve
use serve/http
use serve/router
use home
use chapter
use install
@ -49,6 +51,14 @@ fn main(args: multi Text) -> Int {
host = "${h}";
}
-- Where the release tarballs live. `just dist` writes them to ./dist;
-- a deployment points WO_DIST at wherever it keeps them.
let dist = "dist";
let d = env.get("WO_DIST");
if d != nil {
dist = "${d}";
}
seed_if_empty();
let app = App { middleware: [], routes: [] };
@ -59,6 +69,9 @@ fn main(args: multi Text) -> Int {
app.get("/packages/:name", ShowPackage {});
app.get("/health", Health {});
app.get("/favicon.svg", Favicon {});
-- 16 MiB ceiling: the toolchain tarball is under 1 MiB today, and
-- `max_bytes` is a hard truncation point, not a hint.
app.get("/dl/*path", StaticFiles { dir: dist, max_bytes: 16777216 });
app.get("/ch/:slug", ShowChapter {});
app.post("/admin/ch/:slug", AdminEdit { token: "${token}" });
print_err("site: listening on ${host}:${port}");

View file

@ -3,8 +3,8 @@
-- The catalogue is static data, not rows, so it lives here on the DATA
-- side of the feature rather than in the view: the components are handed
-- values exactly as they would be if this were a table one day.
use framework/http
use html
use serve/http
use view
use layout
typedef PackageInfo = {
@ -54,25 +54,26 @@ fn catalogue() -> multi PackageInfo {
let out: multi PackageInfo = [];
push(out, PackageInfo {
name: "framework",
summary: "A web framework written in writeonce: an HTTP/1.1 keep-alive server core, a router with :param captures, and Handler/Middleware structural interfaces.",
git: "https://github.com/shoneyj/writeonce-framework",
name: "serve",
summary: "The web framework: an HTTP/1.1 keep-alive server core, a router with :param captures, and Handler/Middleware structural interfaces.",
git: "https://github.com/shoneyj/writeonce-serve",
rev: "v0.1.0",
what: `
<ul class="list-disc pl-6 leading-relaxed">
<li class="mb-2"><code>App</code> — the route table and the middleware chains; <code>app.get</code>/<code>post</code>/<code>delete_</code>, <code>use_mw</code>, <code>use_after</code>, <code>mount</code>, <code>serve</code>.</li>
<li class="mb-2"><code>StaticFiles</code> — serve a directory over a wildcard route. Traversal is refused, not normalised; <code>max_bytes</code> is a hard ceiling. This site's <code>/dl</code> downloads run through it.</li>
<li class="mb-2"><code>Handler</code>, <code>Middleware</code>, <code>After</code> — structural interfaces. A handler is a CLASS; its fields are the closure this language does not have.</li>
<li class="mb-2"><code>Req</code>/<code>Resp</code> plus builders: <code>ok_text</code>, <code>ok_html</code>, <code>ok_json</code>, <code>created_json</code>, <code>not_found</code>, <code>bad_request</code>, <code>unauthorized</code>, <code>conflict</code>, <code>redirect</code>.</li>
<li class="mb-2">Auth MECHANISM only — <code>bearer_token</code>, <code>basic_credentials</code>, constant-time <code>ct_eq</code>. Which routes are gated stays your policy.</li>
<li class="mb-2">Bodies: <code>form_values</code>, <code>multipart_parts</code>, content negotiation, ETags, security headers, CORS, WebSocket frames.</li>
</ul>`,
usage: code_block("use framework\nuse framework/http\nuse framework/router\n\nclass Hello {\n fn handle(req: Req) -> Resp {\n return ok_text(\"hello\");\n }\n}\n\nfn main(args: multi Text) -> Int {\n let app = App { middleware: [], routes: [] };\n app.use_mw(Mw { m: Logging {} });\n app.get(\"/\", Hello {});\n return app.serve(\"127.0.0.1\", 8080);\n}")
usage: code_block("use serve\nuse serve/http\nuse serve/router\n\nclass Hello {\n fn handle(req: Req) -> Resp {\n return ok_text(\"hello\");\n }\n}\n\nfn main(args: multi Text) -> Int {\n let app = App { middleware: [], routes: [] };\n app.use_mw(Mw { m: Logging {} });\n app.get(\"/\", Hello {});\n return app.serve(\"127.0.0.1\", 8080);\n}")
});
push(out, PackageInfo {
name: "html",
name: "view",
summary: "Server-rendered HTML as plain Text: escaping, element builders, a component layer, and a Tailwind-style utility stylesheet inlined into every page.",
git: "https://github.com/shoneyj/wo-html",
git: "https://github.com/shoneyj/writeonce-view",
rev: "v0.1.0",
what: `
<ul class="list-disc pl-6 leading-relaxed">
@ -82,7 +83,7 @@ fn catalogue() -> multi PackageInfo {
<li class="mb-2">Builders: <code>el</code>, <code>link</code>, <code>card</code>, <code>nav_bar</code>, <code>code_block</code>, <code>form_post</code>, <code>text_input</code>, <code>text_area</code>, <code>submit_btn</code>, <code>btn_link</code>.</li>
<li class="mb-2"><code>tw_css()</code> + <code>page()</code> — one hand-written utility sheet, inlined, so a page is one self-contained response. No CDN, no build step, no JS.</li>
</ul>`,
usage: code_block("use html\n\nclass Card {\n name: Text\n fn render() -> Text {\n return `\n <div class=\"card\">\n <h3>{{ self.name }}</h3>\n </div>`;\n }\n}\n\n-- in a handler:\nlet c = Card { name: user_supplied };\nreturn ok_html(page(\"Hello\", c.render()));")
usage: code_block("use view\n\nclass Card {\n name: Text\n fn render() -> Text {\n return `\n <div class=\"card\">\n <h3>{{ self.name }}</h3>\n </div>`;\n }\n}\n\n-- in a handler:\nlet c = Card { name: user_supplied };\nreturn ok_html(page(\"Hello\", c.render()));")
});
return out;

View file

@ -3,7 +3,7 @@
-- Three components: a card for the index, the index itself (holding its
-- cards as CHILDREN through the structural interface), and the detail
-- page. None of them knows where the catalogue came from.
use html
use view
pub class PackageCard {
name: Text

View file

@ -1,13 +1,13 @@
name = "site"
version = "0.1.0"
description = "writeonce.de — the language tutorial served by the language: framework + wo-html [deps], @table chapters, server-rendered pages"
description = "writeonce.de — the language tutorial served by the language: framework + writeonce-view [deps], @table chapters, server-rendered pages"
[runtime]
wo = ">= 0.1"
# Two real dependencies (the gate substitutes file:// remotes built from
# docs/examples/writeonce-framework and docs/examples/wo-html, so CI never
# docs/examples/writeonce-serve and docs/examples/writeonce-view, so CI never
# touches the network). The [deps] KEY is the module name `use` imports.
[deps]
framework = { git = "https://github.com/shoneyj/writeonce-framework", rev = "v0.1.0" }
html = { git = "https://github.com/shoneyj/wo-html", rev = "v0.1.0" }
serve = { git = "https://github.com/shoneyj/writeonce-serve", rev = "v0.1.0" }
view = { git = "https://github.com/shoneyj/writeonce-view", rev = "v0.1.0" }

View file

@ -1,7 +1,7 @@
# web-app — the storefront sample
A small store: `Product`/`Order` as `@table` classes, JSON routes, one auth
middleware — built on [`writeonce-framework`](../writeonce-framework/), which
middleware — built on [`writeonce-serve`](../writeonce-serve/), which
it imports **through `[deps]`** (iteration 15). This app is iteration 16's
acceptance workload: `just web-app` runs the whole chain — fetch → lock →
build → serve → curl matrix → restart persistence → SIGTERM.

View file

@ -1,4 +1,4 @@
-- web-app — the storefront: writeonce-framework (via [deps]) + @table
-- web-app — the storefront: writeonce-serve (via [deps]) + @table
-- persistence. Every handler is a class satisfying Handler; the auth gate is
-- a Middleware; the data layer is the language's own database — no ORM, no
-- separate process, one binary.
@ -6,9 +6,9 @@ use env
use json
use net
use time
use framework
use framework/http
use framework/router
use serve
use serve/http
use serve/router
-- decode target for POST /products, encode shape for every product answer
typedef ProductView = { name: Text, price: Float, stock: Int }

View file

@ -1,6 +1,6 @@
name = "web-app"
version = "0.1.0"
description = "Storefront sample: consumes writeonce-framework through [deps]; @table persistence; iteration 16's acceptance workload"
description = "Storefront sample: consumes writeonce-serve through [deps]; @table persistence; iteration 16's acceptance workload"
[runtime]
wo = ">= 0.1"
@ -11,9 +11,9 @@ wo = ">= 0.1"
# The framework is a real dependency, never a relative path — extraction of
# the framework to its own repository changes only this URL. The acceptance
# gate (scripts/web-app-accept.sh) substitutes a run-time file:// remote
# built from docs/examples/writeonce-framework, so CI never needs the
# built from docs/examples/writeonce-serve, so CI never needs the
# network and this repo never carries .wo-deps/wo.lock artifacts.
# The [deps] KEY is the module name `use` imports (hyphens are not identifier
# characters, so the key is `framework` while the repository keeps its name).
[deps]
framework = { git = "https://github.com/shoneyj/writeonce-framework", rev = "v0.1.0" }
serve = { git = "https://github.com/shoneyj/writeonce-serve", rev = "v0.1.0" }

View file

@ -1,11 +1,14 @@
# writeonce-framework
# writeonce-serve
> Renamed 2026-08-25: this library was `writeonce-framework`, imported as
> `use framework`. Stories, specs and plans dated before that still say the
> old name — they are dated records and were left as written.
A web framework **written in writeonce**, consumed as a `[deps]` dependency
(iteration 15). Spec: `docs/superpowers/specs/2026-08-18-web-framework-design.md` §B.
```toml
[deps]
writeonce-framework = { git = "https://github.com/shoneyj/writeonce-framework", rev = "v0.1.0" }
writeonce-serve = { git = "https://github.com/shoneyj/writeonce-serve", rev = "v0.1.0" }
```
## What it is
@ -167,7 +170,7 @@ naming the kind, unless a demo `main` is added (lib+bin is allowed).
- **`internal/` — not importable by a consumer.** The connection-level request
parser and carry-state record (`parse.wo`) and the serve loop, status text,
and response serializer (`serve.wo`) live here. A consuming app that writes
`use writeonce-framework/internal` gets **WO-E108** at that `use`. The rule
`use writeonce-serve/internal` gets **WO-E108** at that `use`. The rule
is Go's: a path segment named `internal` is refused across the `[deps]`
boundary only — the framework's own modules import it freely.
@ -180,3 +183,20 @@ elimination); a consumer simply cannot name them.
`docs/examples/web-app` — a small storefront importing this framework
through `[deps]`. Its acceptance (`just web-app`) exercises the whole chain:
fetch → lock → build → serve → durable restart.
## Serving files
`StaticFiles { dir, max_bytes }` mounts a directory on a wildcard route:
```
app.get("/assets/*path", StaticFiles { dir: "assets", max_bytes: 2097152 })
app.get("/dl/*path", StaticFiles { dir: "dist", max_bytes: 16777216 })
```
Two rules, both refusals rather than repairs: a path containing `..` is a
404 and never reaches the filesystem, and `max_bytes` is a hard ceiling —
`fs.read_all` truncates above it, so set it above the largest file you
mean to serve. Content types come from the extension; archives
(`.tar.gz`, `.tgz`, `.zip`) also get `content-disposition: attachment`.
Text is binary-safe in this language, so archives and images travel
unchanged. Lifted out of the shop template 2026-08-25.

View file

@ -0,0 +1,72 @@
-- http/files.wo — serving a file from disk, the framework's answer to
-- "let people download something". Lifted out of the shop template
-- 2026-08-25, which had carried its own copy and said in a comment that
-- this belonged here.
--
-- Mount it on a wildcard route and it answers that subtree:
--
-- app.get("/assets/*path", StaticFiles { dir: "assets", max_bytes: 2097152 })
-- app.get("/dl/*path", StaticFiles { dir: "dist", max_bytes: 8388608 })
--
-- Safety is two rules, both refusals rather than repairs: a path holding
-- `..` is a 404 and never reaches the filesystem, and a file bigger than
-- `max_bytes` is truncated by `fs.read_all` — so `max_bytes` is a real
-- ceiling you must set above the largest file you intend to serve, not a
-- hint. Text is binary-safe in this language, so archives and images
-- travel unchanged.
use fs
pub class StaticFiles {
dir: Text
max_bytes: Int
fn handle(req: Req) -> Resp {
let rel = req.params["path"];
if rel == nil {
return not_found();
}
-- Traversal: refuse, never normalise. A rewritten path is a second
-- chance to get it wrong.
if index_of("${rel}", "..") != -1 {
return not_found();
}
let body = try fs.read_all("${self.dir}/${rel}", self.max_bytes) catch (e) nil;
if body == nil {
return not_found();
}
let h: map<Text, Text> = {};
h["content-type"] = content_type("${rel}");
if is_download("${rel}") {
h["content-disposition"] = "attachment";
}
return Resp { status: 200, headers: h, body: "${body}" };
}
}
-- Extension to content type. Unknown extensions are octet-stream: a
-- wrong guess is worse than no guess.
pub fn content_type(name: Text) -> Text {
if ends_with(name, ".html") { return "text/html; charset=utf-8"; }
if ends_with(name, ".css") { return "text/css; charset=utf-8"; }
if ends_with(name, ".js") { return "text/javascript"; }
if ends_with(name, ".json") { return "application/json"; }
if ends_with(name, ".svg") { return "image/svg+xml"; }
if ends_with(name, ".png") { return "image/png"; }
if ends_with(name, ".webp") { return "image/webp"; }
if ends_with(name, ".ico") { return "image/x-icon"; }
if ends_with(name, ".woff2") { return "font/woff2"; }
if ends_with(name, ".txt") { return "text/plain; charset=utf-8"; }
if ends_with(name, ".sha256") { return "text/plain; charset=utf-8"; }
if ends_with(name, ".tar.gz") { return "application/gzip"; }
if ends_with(name, ".tgz") { return "application/gzip"; }
if ends_with(name, ".zip") { return "application/zip"; }
return "application/octet-stream";
}
-- Archives are offered as a save, not rendered into a tab.
fn is_download(name: Text) -> Bool {
if ends_with(name, ".tar.gz") { return true; }
if ends_with(name, ".tgz") { return true; }
if ends_with(name, ".zip") { return true; }
return false;
}

View file

@ -40,7 +40,7 @@ pub fn ok_text(body: Text) -> Resp {
-- iteration 37: HTML is transport here, exactly like text and JSON —
-- the status line and the content-type, nothing about rendering. It
-- lives beside its two siblings because both HTML apps had hand-rolled
-- the identical four lines; wo-html stays a pure Text library and never
-- the identical four lines; writeonce-view stays a pure Text library and never
-- learns what a Resp is.
pub fn ok_html(body: Text) -> Resp {
let h: map<Text, Text> = {};

View file

@ -1,4 +1,4 @@
name = "writeonce-framework"
name = "writeonce-serve"
kind = "library"
version = "0.1.0"
description = "A web framework written in writeonce: HTTP/1.1 keep-alive server core, router with :param captures, Handler/Middleware structural interfaces (iteration 16)"
@ -9,4 +9,4 @@ wo = ">= 0.1"
# A LIBRARY project (declared above since iteration 17): no `fn main` here —
# the consuming app owns the entry. `woc <dir>` typechecks the whole project.
# Apps import this repo through `wo.toml [deps]` (iteration 15) and
# `use writeonce-framework` / `use writeonce-framework/http` / `.../router`.
# `use serve` / `use serve/http` / `use serve/router`.

View file

@ -1,4 +1,7 @@
# wo-html — server-rendered HTML as plain Text
# writeonce-view — server-rendered HTML as plain Text
> Renamed 2026-08-25: this library was `wo-html`, imported as `use html`.
> Stories, specs and plans dated before that still say the old name — they
> are dated records and were left as written.
A view library, not a framework and not a template engine. Everything in
it is a pure function or a class with a `render()`; nothing here opens a
@ -6,7 +9,7 @@ socket, reads a file, or touches the database.
```
[deps]
html = { git = "https://github.com/shoneyj/wo-html", rev = "v0.1.0" }
view = { git = "https://github.com/shoneyj/writeonce-view", rev = "v0.1.0" }
```
## The four layers
@ -78,7 +81,7 @@ component like any other.
`ok_html` is the **framework's** (`framework/http`, beside `ok_text` and
`ok_json`): a status line plus a content-type is transport, not
rendering, so wo-html never learns what a `Resp` is.
rendering, so writeonce-view never learns what a `Resp` is.
The seam is what makes a view testable without a server and a query
testable without markup. Breaking it looks like one convenience — a

View file

@ -1,4 +1,4 @@
-- wo-html — server-rendered HTML as plain Text. Four layers, all pure:
-- writeonce-view — server-rendered HTML as plain Text. Four layers, all pure:
-- esc() HTML-escape untrusted text. `{{ }}` in a raw text
-- literal compiles to a call to this, so display data is
-- escaped by construction; esc() by hand is the fallback

View file

@ -1,4 +1,4 @@
name = "wo-html"
name = "writeonce-view"
kind = "library"
version = "0.1.0"
description = "HTML building for writeonce apps: escaping, element builders, and a Tailwind-style utility stylesheet — server-rendered pages as plain Text"
@ -7,4 +7,4 @@ description = "HTML building for writeonce apps: escaping, element builders, and
wo = ">= 0.1"
# A LIBRARY project: no `fn main`. Apps import it through wo.toml [deps]
# (the key names the module — `html = { git = ... }` gives `use html`).
# (the key names the module — `view = { git = ... }` gives `use view`).

View file

@ -52,7 +52,7 @@ Status board: [`00-status.md`](../stories/00-status.md) · Doctrine: [`../00-pri
| **Per-example `principle.md` files** | 2026-08-08: one canonical repo-level [`docs/00-principles.md`](../00-principles.md) instead; examples link to it. |
| **Minimal 3-file log-watcher sample** | Breaks the file-for-file `.hx` → `.wo` mapping and leaves the "could not express" column unproven — which is the sample's entire acceptance criterion. |
| **Raw code in plan documents** | Plans carry concept, reason, and required behavior in words; the executor writes the code. |
| **`##ui` / `.htmlx` LiveView frontend track** | 2026-08-17: removed the 9-doc `exploration/ui/` design set, the `14-mvc-ui-implementation` plan, and the `ui-htmlx-live` plan. All were built on the non-advancing Rust runtime (`.dev/reference/crates/wo-htmlx`, `cargo run`, WebSocket live-patches) and contradict the current woc/wovm direction. The 13d pricing-UI row went with them. Revisit only if a UI story is re-opened on the woc/wovm stack. |
| **`##ui` / `.htmlx` LiveView frontend track** | 2026-08-17: removed the 9-doc `exploration/ui/` design set, the `14-mvc-ui-implementation` plan, and the `ui-htmlx-live` plan. All were built on the non-advancing Rust runtime (`.dev/reference/crates/writeonce-viewx`, `cargo run`, WebSocket live-patches) and contradict the current woc/wovm direction. The 13d pricing-UI row went with them. Revisit only if a UI story is re-opened on the woc/wovm stack. |
| **Old-runtime "front door" + v1 design docs** | 2026-08-17: removed `writeonce-pl.md`, `runtime/wo-language.md`, `future-scope/ai-agents-content-management.md`, the numbered v1 set `02-recovery`/`03-data`/`04-ui`/`05-datalayer`/`06-markdown-render`/`07-ssl`, and `runtime/database/05-go-sdk.md`. They pitched the old Rust `wo` runtime (REST + LiveView + SQL/Cypher) as the current language and contradicted the shipped woc/wovm toolchain. |
| **The 2026-08-01 shard-actor plan (epoll-based)** | 2026-08-21: [`superpowers/plans/2026-08-01-shard-actor-vm-runtime.md`](../superpowers/plans/2026-08-01-shard-actor-vm-runtime.md) marked discarded, file kept as reference. Superseded by the arc plan of record ([`2026-08-20-shard-fiber-arc.md`](../superpowers/plans/2026-08-20-shard-fiber-arc.md), stages 1+2 landed): io_uring is a MUST and the epoll-based approach is discarded — the old plan's "epoll now / io_uring later" premise is inverted, and its substrate (`runtime/wo-rt.c`) left with the Rust track 2026-08-18. |
| **The entire Rust `wo` runtime track** | 2026-08-18: removed `crates/` (the Stage-2 Rust runtime), `Cargo.toml`/`Cargo.lock`, `prototypes/` (wo-rt-c stale duplicate + wo-db C++ ref), the `rt-c-*` justfile recipes, the Rust engineering plans (`docs/plan/05..16`, `docs/plan/done/`), and `docs/runtime/` (the old runtime overview + 7-phase DB design series + async/fibers/gc/surreal essays). It was the prior, abandoned architecture — fully independent of the woc/wovm stack. Master now reflects only the current single-language project; the removed track lives in git history if ever needed as reference. Kept: the syscall/postgres/assembly/c-runtime **exploration studies** (they fed the current C runtime) and the discarded/learnings registers. |

View file

@ -183,7 +183,7 @@ Lowered by the emitter, not added to the format:
| `a == b` on `Text` | `EQS` (content equality); `EQ` otherwise |
| `a .. b` | `CONCAT` — `+` is arithmetic only, never string addition |
| `` `...${e}...` `` | the same `StrLit`/`Interp`/`CONCAT` chain a `"..."` string produces — the raw literal is a LEXER form (verbatim content, common margin removed at lex time), not a new node or opcode |
| `` `...{{ e }}...` `` | `esc(${e})` — the parser wraps the interpolation in a call to whatever `esc` is in scope (wo-html's `pub fn esc`, or a local one that deliberately shadows it). No builtin, no opcode, no HTML knowledge in the compiler or the VM |
| `` `...{{ e }}...` `` | `esc(${e})` — the parser wraps the interpolation in a call to whatever `esc` is in scope (writeonce-view's `pub fn esc`, or a local one that deliberately shadows it). No builtin, no opcode, no HTML knowledge in the compiler or the VM |
| `a and b` | evaluate `a`; `JZ` past evaluating `b` (result stays `a`'s value); else evaluate `b` into the same register (haxe-parity Task 2) |
| `a or b` | evaluate `a`; `JZ` + `JMP` past evaluating `b` when `a` is already true; else evaluate `b` (haxe-parity Task 2) |

View file

@ -168,7 +168,7 @@ sees through Interp; same corpus pin). Body-parsing hooks: all three ✅.
Scope split (2026-08-20): the surface above plus the remaining transport/
routing/security gaps is **framework v1**, tracked item-by-item in the
[framework README's status ledger](../examples/writeonce-framework/README.md)
[framework README's status ledger](../examples/writeonce-serve/README.md)
(✅/🔶/⬜/⏸/🔧 per feature — timeouts and Unix sockets need `net` runtime
seams, crypto hashes need C builtins since the language has no bitwise
operators, streaming/cancellation park behind 8/11). The memory-rich

View file

@ -7,7 +7,7 @@ status: hold
> **Scope label (2026-08-20): this iteration is FRAMEWORK V2.** Framework
> v1 is the transport/routing/body/security surface tracked in the
> [framework README's status ledger](../../../examples/writeonce-framework/README.md);
> [framework README's status ledger](../../../examples/writeonce-serve/README.md);
> v2 is what the embedded store adds on top. v1 gaps land before or
> alongside v2 as slices, per the ledger.

View file

@ -41,7 +41,7 @@ deps-accept:
./scripts/deps-accept.sh
# web-app: iteration 16's gate — the whole chain at run time, network-free:
# a temp git remote from docs/examples/writeonce-framework, file:// URL
# a temp git remote from docs/examples/writeonce-serve, file:// URL
# substituted into a temp copy of docs/examples/web-app, then fetch -> lock
# -> build -> serve -> the storefront matrix (auth/CRUD/@unique/FK/404/400/
# pipelining) -> SIGTERM -> WAL restart persistence.
@ -49,7 +49,7 @@ web-app:
./scripts/web-app-accept.sh
# site: the writeonce.de tutorial (docs/examples/site) — TWO deps
# (framework + wo-html), server-rendered pages, page matrix -> 401/edit
# (serve + view), server-rendered pages, page matrix -> 401/edit
# -> SIGTERM -> WAL restart persistence. `just site` runs it.
site:
./scripts/site-accept.sh

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash
# scripts/site-accept.sh — the writeonce.de tutorial site's gate: TWO deps
# (framework + wo-html) resolved from run-time file:// remotes, build,
# (serve + view) resolved from run-time file:// remotes, build,
# serve, the page matrix (render/escape/404/401/authed edit), SIGTERM,
# and WAL restart persistence of an admin edit.
set -uo pipefail
@ -27,8 +27,8 @@ cleanup() {
trap cleanup EXIT
# ---- both deps as git remotes; the app pointed at them ----
cp -r "$ROOT/docs/examples/writeonce-framework" "$W/fw"
cp -r "$ROOT/docs/examples/wo-html" "$W/lib"
cp -r "$ROOT/docs/examples/writeonce-serve" "$W/fw"
cp -r "$ROOT/docs/examples/writeonce-view" "$W/lib"
for d in "$W/fw" "$W/lib"; do
git -C "$d" init -q
git -C "$d" add -A
@ -36,9 +36,15 @@ for d in "$W/fw" "$W/lib"; do
git -C "$d" tag v0.1.0
done
cp -r "$ROOT/docs/examples/site" "$W/app"
sed -i "s|https://github.com/shoneyj/writeonce-framework|file://$W/fw|; s|https://github.com/shoneyj/wo-html|file://$W/lib|" "$W/app/wo.toml"
sed -i "s|https://github.com/shoneyj/writeonce-serve|file://$W/fw|; s|https://github.com/shoneyj/writeonce-view|file://$W/lib|" "$W/app/wo.toml"
printf '[build]\nruntime = "%s"\n' "$WOVM" >> "$W/app/wo.toml"
# a stand-in release tarball so /dl can be exercised without running
# `just dist` first — the bytes do not matter, the serving path does
mkdir -p "$W/app/dist"
printf 'not-a-real-tarball' | gzip > "$W/app/dist/writeonce-0.1.0-linux-amd64.tar.gz"
( cd "$W/app/dist" && sha256sum writeonce-0.1.0-linux-amd64.tar.gz > writeonce-0.1.0-linux-amd64.tar.gz.sha256 )
# ---- 1. two-dep fetch + lock + build ----
if "$WOC" "$W/app" >"$W/build.out" 2>&1 && [[ -x "$W/app/target/site" && -f "$W/app/wo.lock" ]]; then
ok "deps chain: two remotes fetched + wo.lock + build"
@ -69,6 +75,20 @@ except urllib.error.HTTPError as e:
PYEOF
}
# binary-safe: status + content-type + byte count, no decoding. The
# release tarball is gzip, which `hit` cannot represent.
hit_bin() {
python3 - "$PORT" "$1" <<'PYEOF'
import sys, urllib.request, urllib.error
port, path = sys.argv[1], sys.argv[2]
try:
r = urllib.request.urlopen(f"http://127.0.0.1:{port}{path}", timeout=5)
print(f"{r.status}|{r.headers.get('content-type','')}|{len(r.read())}")
except urllib.error.HTTPError as e:
print(f"{e.code}|{e.headers.get('content-type','')}|{len(e.read())}")
PYEOF
}
expect() { # name got want_status want_substr
local name="$1" got="$2" want="$3" sub="$4"
local st="${got%%|*}" body="${got#*|}"
@ -94,10 +114,17 @@ expect "chapter renders a code sample" "$(hit /ch/hello)" 200 "fn main"
expect "escaped interpolation visible" "$(hit /ch/values)" 200 '${port}'
expect "unknown chapter is a 404 page" "$(hit /ch/nope)" 404 "No such chapter"
expect "install guide renders" "$(hit /install)" 200 "tar -C /usr/local"
expect "packages index lists both" "$(hit /packages)" 200 "/packages/framework"
expect "package detail shows its dep" "$(hit /packages/html)" 200 "wo-html"
expect "packages index lists both" "$(hit /packages)" 200 "/packages/serve"
expect "package detail shows its dep" "$(hit /packages/view)" 200 "writeonce-view"
expect "unknown package is a 404 page" "$(hit /packages/nope)" 404 "No such package"
expect "favicon is served as svg" "$(hit /favicon.svg)" 200 "<svg"
expect "install lists supported systems" "$(hit /install)" 200 "glibc 2.38 or newer"
got="$(hit_bin /dl/writeonce-0.1.0-linux-amd64.tar.gz)"
if [[ "$got" == 200\|application/gzip\|* && "${got##*|}" -gt 0 ]]; then
ok "the release tarball downloads as gzip"
else bad "tarball download" "$got"; fi
expect "its checksum downloads" "$(hit /dl/writeonce-0.1.0-linux-amd64.tar.gz.sha256)" 200 "writeonce-0.1.0-linux-amd64.tar.gz"
expect "traversal out of /dl is a 404" "$(hit /dl/../wo.toml)" 404 ""
expect "the logo is inline in the nav" "$(hit /)" 200 "aria-label=\"writeonce\""
expect "admin without token is 401" "$(hit /admin/ch/hello POST "title=X")" 401 "unauthorized"
expect "admin edit answers a redirect" "$(hit /admin/ch/hello POST "title=Hello v2" s3cr3t)" 302 ""

View file

@ -1,7 +1,7 @@
#!/usr/bin/env bash
# scripts/web-app-accept.sh — iteration 16's acceptance gate. Proves the whole
# chain at run time, network-free: a temp git remote is built from
# docs/examples/writeonce-framework, its file:// URL is substituted into a
# docs/examples/writeonce-serve, its file:// URL is substituted into a
# temp copy of docs/examples/web-app, then: fetch -> lock -> build -> serve ->
# the storefront matrix -> SIGTERM -> restart persistence, plus iteration 17's
# library-kind and internal/-boundary checks. The repo itself
@ -33,13 +33,13 @@ cleanup() {
trap cleanup EXIT
# ---- the framework as a git remote; the app pointed at it ----
cp -r "$ROOT/docs/examples/writeonce-framework" "$W/fw"
cp -r "$ROOT/docs/examples/writeonce-serve" "$W/fw"
git -C "$W/fw" init -q
git -C "$W/fw" add -A
git -C "$W/fw" -c user.email=t@t -c user.name=t commit -qm v01
git -C "$W/fw" tag v0.1.0
cp -r "$ROOT/docs/examples/web-app" "$W/app"
sed -i "s|https://github.com/shoneyj/writeonce-framework|file://$W/fw|" "$W/app/wo.toml"
sed -i "s|https://github.com/shoneyj/writeonce-serve|file://$W/fw|" "$W/app/wo.toml"
printf '[build]\nruntime = "%s"\n' "$WOVM" >> "$W/app/wo.toml"
# ---- 1. fetch + lock + build ----
@ -63,10 +63,10 @@ fi
# A consumer reaching past the privacy line is WO-E108 at its own `use`.
cp -r "$W/app" "$W/app-internal"
rm -rf "$W/app-internal/target" "$W/app-internal/wo.lock" "$W/app-internal/.wo-deps"
sed -i '1i use framework/internal' "$W/app-internal/main.wo"
sed -i '1i use serve/internal' "$W/app-internal/main.wo"
out="$("$WOC" "$W/app-internal" 2>&1)"; rc=$?
if [[ "$rc" == "1" ]] && printf '%s' "$out" | grep -q 'WO-E108'; then
ok "dep boundary: importing framework/internal is WO-E108"
ok "dep boundary: importing serve/internal is WO-E108"
else
bad "internal-boundary" "exit=$rc out=$(printf '%s' "$out" | head -1)"
fi

View file

@ -4,7 +4,7 @@
-- carry the markup's own nesting and not the method's.
-- `${...}` interpolates raw; `{{ ... }}` HTML-escapes through the `esc`
-- in scope (here a local one, which is exactly how a program overrides
-- wo-html's).
-- writeonce-view's).
fn esc(t: Text) -> Text {
let out = "";
let i = 0;