From b19042bca082162bff24ba4a8afaf2a3ebd755c6 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Sun, 23 Aug 2026 19:11:21 +0200 Subject: [PATCH 01/25] feat(site): go.dev-style homepage + shared nav/footer chrome - wo-html grows generic nav_bar/card/btn_link + the utility classes they need (sticky nav, footer, 2-col grid, hero sizes); library stays content-free - home: hero (tagline + Get started/View source CTAs), real actor+ table code showcase, four why-cards, chapters strip (gate's 'Learn writeonce' anchor kept); every page shares nav + footer - site gate 11/0 unchanged; loopback bind untouched Co-Authored-By: Claude Fable 5 --- docs/examples/site/content.wo | 14 ++++++++++ docs/examples/site/main.wo | 50 ++++++++++++++++++++++++++++------- docs/examples/wo-html/html.wo | 36 +++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 9 deletions(-) diff --git a/docs/examples/site/content.wo b/docs/examples/site/content.wo index 7026ec8..0eb8340 100644 --- a/docs/examples/site/content.wo +++ b/docs/examples/site/content.wo @@ -7,6 +7,20 @@ -- site that teaches it. use html +-- The homepage's code showcase: a real flavor of the language — an +-- actor per chat room, rows in the built-in database, one binary. +fn home_snippet() -> Text { + let s = "@table\nclass Message {\n room: Text\n body: Text\n}\n\n"; + s = s .. "class Room {\n name: Text\n fn receive(msg: Post) {\n"; + s = s .. " insert Message { room: self.name, body: msg.body };\n"; + s = s .. " print(\"[\${self.name}] \${msg.body}\");\n }\n}\n\n"; + s = s .. "fn main() -> Int {\n"; + s = s .. " let general: actor Post = spawn Room { name: \"general\" };\n"; + s = s .. " send(general, Post { body: \"hello, writeonce\" });\n"; + s = s .. " time.sleep(50);\n return 0;\n}"; + return code_block(s); +} + fn ch_hello() -> Text { let b = el("p", "leading-relaxed mb-4", "A writeonce program is one directory of .wo files and one entry: a free " .. "function named main. It returns the process exit code. There is no " .. "runtime to install separately and no build pipeline — woc build produces " .. "ONE self-contained binary with the VM and your bytecode inside."); diff --git a/docs/examples/site/main.wo b/docs/examples/site/main.wo index 52b7b24..7df67e9 100644 --- a/docs/examples/site/main.wo +++ b/docs/examples/site/main.wo @@ -43,14 +43,26 @@ fn ok_html(body: Text) -> Resp { return Resp { status: 200, headers: h, body: body }; } -fn site_header() -> Text { - let brand = link("/", "text-xl font-bold text-gray-900 no-underline", "writeonce.de"); - let tag = el("span", "text-sm text-gray-500", "one language, one runtime, one database, one binary"); - return el("div", "flex items-center justify-between mb-8", brand .. tag); +fn site_nav() -> Text { + let links = link("/ch/hello", "text-gray-700", "Tutorial"); + links = links .. link("/health", "text-gray-700", "Health"); + links = links .. link("https://github.com/shoneyj", "text-gray-700", "GitHub"); + return nav_bar("/", "writeonce.de", links); +} + +fn site_footer() -> Text { + return el("div", "footer", "writeonce.de — served by the language it teaches. " .. "One binary: compiler, runtime, database, this page."); } fn shell(title: Text, inner: Text) -> Text { - let body = el("div", "mx-auto max-w-3xl px-4 py-8", site_header() .. inner); + let body = site_nav() .. el("div", "mx-auto max-w-3xl px-4 py-8", inner) .. site_footer(); + return page(title, body); +} + +-- The homepage uses the wider container (go.dev shape); chapter pages +-- keep the reading width above. +fn shell_wide(title: Text, inner: Text) -> Text { + let body = site_nav() .. el("div", "mx-auto max-w-5xl px-4", inner) .. site_footer(); return page(title, body); } @@ -72,10 +84,30 @@ fn chapter_nav(current: Int) -> Text { class Home { pad: Int fn handle(req: Req) -> Resp { - let hero = el("h1", "text-3xl font-bold mb-4", "Learn writeonce"); - let intro = el("p", "leading-relaxed mb-4", "A language where the runtime, the database and the web server are one thing. " .. "This site is written in it — every page you read here is a " .. "Resp built by .wo code, stored in the language's own " .. "tables, served by its own framework. Work through the chapters in order:"); - let body = el("div", "bg-white rounded-lg border shadow-sm p-6", hero .. intro .. chapter_nav(0)); - return ok_html(shell("writeonce — learn the language", body)); + -- hero: tagline + the two CTAs (the go.dev shape, no JS anywhere) + let h1 = el("h1", "text-4xl font-bold mb-4", "One language. One runtime.
One database. One binary."); + let sub = el("p", "text-lg text-gray-700 leading-relaxed mb-6", "writeonce is a language whose compiler, runtime, web server and " .. "database ship as a single never-stopping Linux binary. Ownership-" .. "checked memory, inferred GC where ownership cannot reach, actors " .. "on every core — and the page you are reading is served by it."); + let ctas = el("div", "flex items-center justify-center gap-4", btn_link("/ch/hello", "Get started", true) .. btn_link("https://github.com/shoneyj", "View source", false)); + let hero = el("div", "text-center py-16", h1 .. sub .. ctas); + + -- code showcase: a real flavor of the language + let show_head = el("h2", "text-2xl font-bold mb-2 text-center", "An actor per chat room, rows in the built-in database"); + let show_cap = el("p", "text-sm text-gray-500 text-center mb-4", "No broker, no ORM, no async keyword — ownership moves the message, the WAL makes the row durable."); + let showcase = el("div", "mx-auto max-w-3xl mb-8", show_head .. show_cap .. home_snippet()); + + -- why-cards (2x2 grid, collapses on small screens) + let cards = card("One binary", "woc build emits a self-contained executable: VM, your bytecode, the database engine. Deploys are a file copy; the runtime swaps code in place."); + cards = cards .. card("Memory safety, no tax", "Rust-shaped ownership checked at compile time; where ownership cannot express the shape, the compiler infers GC — per shard, no global pause."); + cards = cards .. card("The database is built in", "Every class is a table. Inserts are WAL-logged before they acknowledge; restart replays. No server to operate, no connection string."); + cards = cards .. card("Actors on every core", "spawn returns an address, send moves ownership. Fibers park on io_uring instead of blocking threads — no async/await, ever."); + let grid = el("div", "grid grid-cols-2 gap-6 mb-8", cards); + + -- chapters (the gate's anchor string lives here) + let learn = el("h2", "text-2xl font-bold mb-4", "Learn writeonce"); + let learn_p = el("p", "leading-relaxed mb-4", "The tutorial is written in the language and stored in its tables — work through the chapters in order:"); + let chapters = el("div", "bg-white rounded-lg border shadow-sm p-6 mb-8", learn .. learn_p .. chapter_nav(0)); + + return ok_html(shell_wide("writeonce — learn the language", hero .. showcase .. grid .. chapters)); } } diff --git a/docs/examples/wo-html/html.wo b/docs/examples/wo-html/html.wo index e8a5ce8..70adfdf 100644 --- a/docs/examples/wo-html/html.wo +++ b/docs/examples/wo-html/html.wo @@ -71,6 +71,29 @@ pub fn form_post(action: Text, inner: Text) -> Text { return "
${inner}
"; } +-- A sticky top navigation bar: brand on the left, a prebuilt row of +-- links on the right. The bar spans the viewport; the inner row shares +-- the page's container widths. +pub fn nav_bar(brand_href: Text, brand: Text, right: Text) -> Text { + let b = link(brand_href, "text-xl font-bold text-gray-900 no-underline", brand); + let row = el("div", "mx-auto max-w-5xl px-4 flex items-center justify-between", b .. el("div", "flex items-center gap-4", right)); + return el("nav", "nav", row); +} + +-- A titled card for feature grids. +pub fn card(title: Text, body: Text) -> Text { + let t = el("h3", "text-lg font-bold mb-2", title); + return el("div", "bg-white rounded-lg border shadow-sm p-6", t .. el("p", "leading-relaxed text-gray-700", body)); +} + +-- A link styled as a button; primary = filled, otherwise outline. +pub fn btn_link(href: Text, label: Text, primary: Bool) -> Text { + if primary { + return link(href, "btn no-underline", label); + } + return link(href, "btn-outline no-underline", label); +} + -- The utility sheet. Tailwind's names, one hand-written static sheet — -- only the utilities this ecosystem's pages actually use; growing it is -- adding a line, not adopting a toolchain. `.code-block`, `.field` and @@ -107,6 +130,19 @@ pub fn tw_css() -> Text { c = c .. "padding:.5rem;font-family:ui-monospace,monospace;font-size:.875rem}"; c = c .. ".btn{background:#2563eb;color:#fff;border:0;border-radius:.25rem;"; c = c .. "padding:.5rem 1rem;font-weight:700;cursor:pointer}"; + c = c .. "a.btn{color:#fff;display:inline-block}a.btn:hover{text-decoration:none;background:#1d4ed8}"; + c = c .. ".btn-outline{display:inline-block;border:1px solid #2563eb;color:#2563eb;"; + c = c .. "border-radius:.25rem;padding:.5rem 1rem;font-weight:700}"; + c = c .. "a.btn-outline:hover{text-decoration:none;background:#eff6ff}"; + c = c .. ".nav{position:sticky;top:0;background:#fff;border-bottom:1px solid #e5e7eb;"; + c = c .. "padding:.75rem 0;z-index:10}"; + c = c .. ".footer{border-top:1px solid #e5e7eb;color:#6b7280;font-size:.875rem;"; + c = c .. "padding:2rem 1rem;text-align:center;margin-top:4rem}"; + c = c .. ".max-w-5xl{max-width:64rem}.text-4xl{font-size:2.25rem;line-height:1.2}"; + c = c .. ".py-16{padding-top:4rem;padding-bottom:4rem}.text-center{text-align:center}"; + c = c .. ".mb-6{margin-bottom:1.5rem}.gap-6{gap:1.5rem}.justify-center{justify-content:center}"; + c = c .. ".grid{display:grid}.grid-cols-2{grid-template-columns:repeat(2,1fr)}"; + c = c .. "@media(max-width:640px){.grid-cols-2{grid-template-columns:1fr}}"; return c; } From 8f965846825a26e7910e8fb420bf81aadd0aded5 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Sun, 23 Aug 2026 19:35:41 +0200 Subject: [PATCH 02/25] =?UTF-8?q?docs:=20story=2037=20=E2=80=94=20wo-html?= =?UTF-8?q?=20components=20(MVC=20view=20layer,=20Angular=20format=20studi?= =?UTF-8?q?ed)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Component interface (render->Text), layouts/slots, site migrates as acceptance; framework stays micro (view layer = library) - Angular map recorded: inputs/templates/ngFor/projection translate, DI/bindings/client-side rejected by doctrine (no closures, no JS) - four forks for the spec; unscheduled, off-chain; table + board rows Co-Authored-By: Claude Fable 5 --- docs/stories/00-status.md | 1 + .../language-runtime-database/00-story.md | 1 + .../refine/37-wo-html-components.md | 118 ++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 docs/stories/language-runtime-database/refine/37-wo-html-components.md diff --git a/docs/stories/00-status.md b/docs/stories/00-status.md index c38305c..3d205fe 100644 --- a/docs/stories/00-status.md +++ b/docs/stories/00-status.md @@ -244,6 +244,7 @@ that sequences its tasks. Read one, approve, then the next starts. | 32 | [WAL checkpoint](language-runtime-database/refine/32-wal-checkpoint.md) | ⬜ last in chain, after 23 — disk reclamation + bounded replay (story written 2026-08-21) | | 33 | [Single-file store](language-runtime-database/refine/33-single-file-db.md) | ⬜ off-chain, small — `WO_DATA=.db` file form; driver-only (story written 2026-08-22) | | 34 | [Crypto builtins](language-runtime-database/refine/34-crypto-builtins.md) | ⬜ off-chain but GATES 24 (WS handshake needs SHA-1) — digests + HMAC as vector-verified C builtins (story written 2026-08-22) | +| 37 | [wo-html components](language-runtime-database/refine/37-wo-html-components.md) | ⬜ off-chain — MVC-shaped view layer in wo-html (Component interface, layouts/slots, site migrates as acceptance); Angular format studied, client-side half rejected (story written 2026-08-23) | | 35 | [net runtime seams](language-runtime-database/refine/35-net-runtime-seams.md) | ⬜ off-chain — fd deadlines on the park plane, Unix sockets, peer address; owns the ledger's three 🔧 rows (story written 2026-08-22) | | 20 | [Cross-program tables](language-runtime-database/hold/20-cross-program-tables.md) | ⏸ hold (2026-08-21); channel done (branch ipc-attach keeps its manifest) | | 21 | [Keypair attach auth](language-runtime-database/hold/21-keypair-attach-auth.md) | ⏸ hold (2026-08-21); crypto+handshake done (branch keypair-auth keeps its manifest) | diff --git a/docs/stories/language-runtime-database/00-story.md b/docs/stories/language-runtime-database/00-story.md index d7b515e..434bf52 100644 --- a/docs/stories/language-runtime-database/00-story.md +++ b/docs/stories/language-runtime-database/00-story.md @@ -109,6 +109,7 @@ still pending IS the runtime-concurrency chain; order: | 20 | 32 | [WAL checkpoint](refine/32-wal-checkpoint.md) | **NEW 2026-08-21** (stage-3 guarantee refinement found the hole) — the WAL is append-only forever: snapshot + truncate reclaims disk and bounds replay time; every durability guarantee byte-identical; crash mid-checkpoint recovers from the previous snapshot + full tail. After 23 (composes with group-commit); RAM slot-reuse already contracted in `04-db-binding.md`. | | 21 | 33 | [Single-file store](refine/33-single-file-db.md) | **NEW 2026-08-22** — `WO_DATA=.db`: a file path IS the wal (the store already lives in exactly one file; this makes the surface say so). Driver-only, independent of the chain; composes with 32's rename-swap. | | 22 | 34 | [Crypto builtins](refine/34-crypto-builtins.md) | **NEW 2026-08-22** — SHA-1/SHA-256/HMAC-SHA256 as C builtins over Bytes (no bitwise ops in the language, hand-rolled per doctrine, vector-verified). GATES 24's WS handshake; digest floor for held 21 and the ETag row. | +| 23 | 37 | [wo-html components](refine/37-wo-html-components.md) | **NEW 2026-08-23** — an MVC-shaped view layer in the wo-html LIBRARY (framework stays micro): structural `Component` interface (`render() -> Text`), layout components with slots, the site sample migrated as acceptance. Angular's component FORMAT studied and translated to server-rendered no-JS `.wo`; DI/bindings rejected. Unscheduled. | | 23 | 35 | [net runtime seams](refine/35-net-runtime-seams.md) | **NEW 2026-08-22** — the ledger's three 🔧 rows owned: fd deadlines composing with the park plane, Unix-socket listeners, peer address (trusted-proxy check). Framework knobs stay framework slices; pairs naturally with 24 (dead-client eviction). | | 24 | 25 | [HTTP service layer](../../superpowers/plans/2026-08-01-http-service-layer.md) | `service` blocks lower onto the framework (after 9b + 20 by their own precedence notes). **HELD 2026-08-21** — story file removed; the plan doc remains. *(was 10)* | | 25 | 18 | [framework v2: memory-rich features](hold/18-memory-db-features.md) | spec+plan approved: TTL cache, @table flags, durable job queue, `transaction { }` over the WAL's staged batch. **Demoted from seq 14**: more surface on a framework with one consumer, and the cache still stores `Text` because there are no generics | diff --git a/docs/stories/language-runtime-database/refine/37-wo-html-components.md b/docs/stories/language-runtime-database/refine/37-wo-html-components.md new file mode 100644 index 0000000..bd9ca68 --- /dev/null +++ b/docs/stories/language-runtime-database/refine/37-wo-html-components.md @@ -0,0 +1,118 @@ +--- +iteration: "37" +status: refine +--- + +# Iteration 37 — wo-html components: an MVC-shaped view layer (Angular's format, studied) + +> Format: `product/story-iteration-template`. Part of +> [Story — one language, one runtime, one database, one binary](../00-story.md). +> +> **Inserted 2026-08-23** (developer ask: "enhance wo-html like MVC; +> understand Angular format"). Grows the wo-html LIBRARY, never the +> framework — the 2026-08-20 micro-framework directive stands: routing/ +> middleware/`Req`/`Resp` stay MVC-free, and the view layer lives in its +> own dependency (the site sample's two-dep lesson). Unscheduled — +> independent of the concurrency chain; needs its spec brainstormed +> first. + +## Why this iteration exists + +wo-html today is element builders + one utility sheet: pages are +functions concatenating Text. That works (the site proves it) but has +no unit of reuse bigger than a function — no way to say "this fragment +owns its data, its markup, and its place in a layout" and hand it +around. Angular's component FORMAT — a class declaring its inputs, a +template rendering them, composition by nesting, structural directives +for repetition and choice — is the studied precedent: the FORMAT +translates to server-rendered `.wo`; the client-side half (change +detection, event bindings, SPA router) deliberately does not. + +## What Angular's format maps to (the study, summarized) + +| Angular | wo-html translation | doctrine fit | +| --- | --- | --- | +| `@Component` class with `@Input()`s | a class whose FIELDS are the inputs, satisfying a structural `Component` interface (`fn render() -> Text`) | behavior-as-class; no closures needed | +| template (`{{ expr }}`) | the render method's interpolation — `.wo` already has `${...}` in Text | no template dialect: templates ARE code | +| `*ngFor` / `*ngIf` | explicit `for`/`if` in render() building Text — the language's own control flow | no structural-directive mini-language | +| content projection (``) | a layout component taking pre-rendered `Text` slots as fields | slots are ordinary values | +| services/DI | no translation — a component reads its fields; queries stay in handlers (M and V stay separate) | rejected: DI needs function values | +| event bindings `(click)` / two-way `[(ngModel)]` | no translation — server-rendered, no JS doctrine; forms stay `form_post` round trips | rejected surface | + +## Goals + +- **A `Component` interface in wo-html**: structural (`fn render() -> + Text`), so any class with fields + render satisfies it — the view + twin of the framework's `Handler`. Composition is nesting: a parent's + render calls children's render. +- **The MVC seam stated**: Model = `@table` rows queried in the HANDLER, + moved into component fields; View = components rendering Text; + Controller = the framework handler wiring them. The library documents + the seam; it never queries. +- **Layout components with slots**: the site's nav/shell/footer become + the proof — a `Layout { title, nav, content, footer }` component + replacing today's `shell()` functions, chapter pages and homepage + composing it. +- **The site sample migrates** as acceptance: same rendered bytes (or + deliberately better), gate stays green — the library grew a floor, not + a rewrite. + +## Acceptance Criteria (draft — the spec refines) + +- **Given** a class with fields and `fn render() -> Text`, **when** a + handler moves data in and calls render, **then** the page it serves is + byte-deterministic from the fields — no hidden state, no globals. +- **Given** nested components (layout → section → card), **when** the + outer render runs, **then** children render through the same + structural interface, and escaping stays the caller-explicit `esc()` + rule at every level. +- **Given** the migrated site sample, **when** `just site` runs, + **then** 11/0 — the gate is the proof the component layer reproduces + the existing pages. +- **Given** a component reused across two pages (the chapters card on + home and chapter pages), **when** either page changes its data, + **then** the other's markup is untouched — reuse is real, not copied. + +## Out Of Scope + +- Client-side anything: change detection, event/two-way bindings, SPA + routing, hydration — the no-JS posture stands; interactivity is form + round trips until a directive says otherwise. +- A template LANGUAGE (files parsed at build or runtime) — templates are + `.wo` code by doctrine (no closures also means no template lambdas). +- Dependency injection / services — components are data-in, Text-out. +- Moving wo-html into the framework — settled 2026-08-23: separate + libraries, composed via `[deps]`. +- CSS componentization (scoped styles) — the utility sheet stays one + static string; measure pain first. + +## Info + +Forks the spec must settle: + +1. **Interface shape** — `render() -> Text` alone, or `render(ctx) -> + Text` with a context record (e.g. the request's principal for + view-level decisions)? Leaning: bare `render()` — context smells like + DI; whatever the view needs arrives as a field. +2. **The framework seam** — does `ok_html(body)` move into the framework + beside `ok_text`/`ok_json` (transport, not rendering — flagged + 2026-08-23), and does wo-html gain `respond(c: Component)` sugar? +3. **Slot typing** — slots as pre-rendered `Text` fields (simple, order + forced by the parent) vs slots as `Component` fields (uniform, but + ownership of nested components needs care under move semantics). +4. **Migration depth** — site only, or web-app's HTML-less JSON pages + deliberately untouched as the counter-example (a framework consumer + that never links the view layer)? + +Study sources: Angular's component/`@Input`/`ng-content` docs (format +only; no Angular code enters the repo — candidate `.dev/reference` +addition if deeper study is wanted), Go's `html/template` as the +server-side contrast, and the site sample as the living consumer. + +## Proposed Solution + +Brainstorm → spec → plan (the superpowers path): settle the four forks, +grow wo-html by the `Component` interface + a `Layout` proof, migrate +the site sample as acceptance, keep the framework untouched. Ships +independently of the concurrency chain; slots wherever the developer +schedules it. From 67cd039533a6e2a38bdd8f1238f26fb268da6db3 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Sun, 23 Aug 2026 19:36:18 +0200 Subject: [PATCH 03/25] docs: fix stale story-35 links (file moved to done/ at landing) Co-Authored-By: Claude Fable 5 --- docs/stories/00-status.md | 2 +- docs/stories/language-runtime-database/00-story.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/stories/00-status.md b/docs/stories/00-status.md index 3d205fe..2bcf6e9 100644 --- a/docs/stories/00-status.md +++ b/docs/stories/00-status.md @@ -245,7 +245,7 @@ that sequences its tasks. Read one, approve, then the next starts. | 33 | [Single-file store](language-runtime-database/refine/33-single-file-db.md) | ⬜ off-chain, small — `WO_DATA=.db` file form; driver-only (story written 2026-08-22) | | 34 | [Crypto builtins](language-runtime-database/refine/34-crypto-builtins.md) | ⬜ off-chain but GATES 24 (WS handshake needs SHA-1) — digests + HMAC as vector-verified C builtins (story written 2026-08-22) | | 37 | [wo-html components](language-runtime-database/refine/37-wo-html-components.md) | ⬜ off-chain — MVC-shaped view layer in wo-html (Component interface, layouts/slots, site migrates as acceptance); Angular format studied, client-side half rejected (story written 2026-08-23) | -| 35 | [net runtime seams](language-runtime-database/refine/35-net-runtime-seams.md) | ⬜ off-chain — fd deadlines on the park plane, Unix sockets, peer address; owns the ledger's three 🔧 rows (story written 2026-08-22) | +| 35 | [net runtime seams](language-runtime-database/done/35-net-runtime-seams.md) | ⬜ off-chain — fd deadlines on the park plane, Unix sockets, peer address; owns the ledger's three 🔧 rows (story written 2026-08-22) | | 20 | [Cross-program tables](language-runtime-database/hold/20-cross-program-tables.md) | ⏸ hold (2026-08-21); channel done (branch ipc-attach keeps its manifest) | | 21 | [Keypair attach auth](language-runtime-database/hold/21-keypair-attach-auth.md) | ⏸ hold (2026-08-21); crypto+handshake done (branch keypair-auth keeps its manifest) | | 25 | [HTTP service layer](../superpowers/plans/2026-08-01-http-service-layer.md) | ⏸ hold (2026-08-21) — story file removed; the plan doc remains | diff --git a/docs/stories/language-runtime-database/00-story.md b/docs/stories/language-runtime-database/00-story.md index 434bf52..d98b6a2 100644 --- a/docs/stories/language-runtime-database/00-story.md +++ b/docs/stories/language-runtime-database/00-story.md @@ -110,7 +110,7 @@ still pending IS the runtime-concurrency chain; order: | 21 | 33 | [Single-file store](refine/33-single-file-db.md) | **NEW 2026-08-22** — `WO_DATA=.db`: a file path IS the wal (the store already lives in exactly one file; this makes the surface say so). Driver-only, independent of the chain; composes with 32's rename-swap. | | 22 | 34 | [Crypto builtins](refine/34-crypto-builtins.md) | **NEW 2026-08-22** — SHA-1/SHA-256/HMAC-SHA256 as C builtins over Bytes (no bitwise ops in the language, hand-rolled per doctrine, vector-verified). GATES 24's WS handshake; digest floor for held 21 and the ETag row. | | 23 | 37 | [wo-html components](refine/37-wo-html-components.md) | **NEW 2026-08-23** — an MVC-shaped view layer in the wo-html LIBRARY (framework stays micro): structural `Component` interface (`render() -> Text`), layout components with slots, the site sample migrated as acceptance. Angular's component FORMAT studied and translated to server-rendered no-JS `.wo`; DI/bindings rejected. Unscheduled. | -| 23 | 35 | [net runtime seams](refine/35-net-runtime-seams.md) | **NEW 2026-08-22** — the ledger's three 🔧 rows owned: fd deadlines composing with the park plane, Unix-socket listeners, peer address (trusted-proxy check). Framework knobs stay framework slices; pairs naturally with 24 (dead-client eviction). | +| 23 | 35 | [net runtime seams](done/35-net-runtime-seams.md) | **NEW 2026-08-22** — the ledger's three 🔧 rows owned: fd deadlines composing with the park plane, Unix-socket listeners, peer address (trusted-proxy check). Framework knobs stay framework slices; pairs naturally with 24 (dead-client eviction). | | 24 | 25 | [HTTP service layer](../../superpowers/plans/2026-08-01-http-service-layer.md) | `service` blocks lower onto the framework (after 9b + 20 by their own precedence notes). **HELD 2026-08-21** — story file removed; the plan doc remains. *(was 10)* | | 25 | 18 | [framework v2: memory-rich features](hold/18-memory-db-features.md) | spec+plan approved: TTL cache, @table flags, durable job queue, `transaction { }` over the WAL's staged batch. **Demoted from seq 14**: more surface on a framework with one consumer, and the cache still stores `Text` because there are no generics | | 26 | 27 | [Query grammar corpus](hold/27-query-grammar-corpus.md) | grow the query grammar from real corpora; likely collapses to "confirm `len(query)` + add `exists`"; precedes 28. *(was 9g)* | From 89df51761325a10a85bffb5f5f14972380256999 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Sun, 23 Aug 2026 23:57:44 +0200 Subject: [PATCH 04/25] =?UTF-8?q?feat(shop):=20the=20program=20template=20?= =?UTF-8?q?=E2=80=94=20MVC=20on=20disk;=20story=2037=20redirected?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/examples/shop: types.wo model, per-feature view modules (render() classes), root controller files, layout shell/header/ footer, static assets controller + real style.css, README with Angular file map + run + DX referendum notes - buy flow proven by hand: stock check/decrement, 409s, traversal 404, css typed, WAL-durable; builds on the UNPATCHED toolchain - two language gaps recorded, not fixed (per directive): pub+@table cannot combine (controllers forced into root module); @view projection classes absent - story 37 REDIRECTED per Vue-SFC review: view.html compiled by woc ({{}} auto-escaped, w:if/w:for, typed against view class, no runtime engine); render()-as-concatenation failed the referendum; forks revised; shop named the acceptance consumer Co-Authored-By: Claude Fable 5 --- docs/examples/shop/README.md | 69 +++++++++++++++++++ docs/examples/shop/assets/style.css | 47 +++++++++++++ docs/examples/shop/layout/app.wo | 34 +++++++++ docs/examples/shop/layout/footer.wo | 6 ++ docs/examples/shop/layout/header.wo | 8 +++ docs/examples/shop/main.wo | 49 +++++++++++++ docs/examples/shop/orders.controller.wo | 55 +++++++++++++++ docs/examples/shop/orders/view.wo | 44 ++++++++++++ docs/examples/shop/product_list.controller.wo | 20 ++++++ docs/examples/shop/product_list/view.wo | 33 +++++++++ docs/examples/shop/product_page.controller.wo | 21 ++++++ docs/examples/shop/product_page/view.wo | 25 +++++++ docs/examples/shop/static_files/controller.wo | 33 +++++++++ docs/examples/shop/types.wo | 29 ++++++++ docs/examples/shop/wo.toml | 12 ++++ .../refine/37-wo-html-components.md | 56 +++++++++++---- 16 files changed, 527 insertions(+), 14 deletions(-) create mode 100644 docs/examples/shop/README.md create mode 100644 docs/examples/shop/assets/style.css create mode 100644 docs/examples/shop/layout/app.wo create mode 100644 docs/examples/shop/layout/footer.wo create mode 100644 docs/examples/shop/layout/header.wo create mode 100644 docs/examples/shop/main.wo create mode 100644 docs/examples/shop/orders.controller.wo create mode 100644 docs/examples/shop/orders/view.wo create mode 100644 docs/examples/shop/product_list.controller.wo create mode 100644 docs/examples/shop/product_list/view.wo create mode 100644 docs/examples/shop/product_page.controller.wo create mode 100644 docs/examples/shop/product_page/view.wo create mode 100644 docs/examples/shop/static_files/controller.wo create mode 100644 docs/examples/shop/types.wo create mode 100644 docs/examples/shop/wo.toml diff --git a/docs/examples/shop/README.md b/docs/examples/shop/README.md new file mode 100644 index 0000000..0d99509 --- /dev/null +++ b/docs/examples/shop/README.md @@ -0,0 +1,69 @@ +# shop — the writeonce program template + +A small store you can buy from, structured the way a real writeonce web +app should be. **Copy this directory to start a new app**; every file +has one concern, and the module system (one directory = one module, +`pub` = the export line) enforces the separation the layout promises. + +## Run it + +``` +cd docs/examples/shop +woc . && WO_DATA=./data ./target/shop 8080 # durable store +./target/shop 8080 # RAM-only (dev) +``` + +Browse http://127.0.0.1:8080/ — products → product page → buy (stock +checked and decremented) → confirmation → /orders. With `WO_DATA`, kill +it and restart: the orders are still there (WAL replay). + +## The file map (Angular equivalents) + +| this template | concern | Angular analog | +| --- | --- | --- | +| `types.wo` | MODEL — `@table` classes ARE the WAL database | `models/*.ts` (+ the entire database) | +| `layout/app.wo` | app shell: document, header+footer composition, `ok_html`/`html_error` transport helpers | `app.component.html` | +| `layout/header.wo` / `footer.wo` | shared chrome fragments | `header.html` / `footer.html` | +| `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` (one file per feature, root module) | component `.ts` + service | +| `product_page/`, `orders/` | one view module per feature + its root controller file | feature folders | +| `static_files/controller.wo` | `/assets/*` from disk, traversal-safe, typed | `angular.json` assets | +| `assets/style.css` | ONE real stylesheet, sectioned per feature | the `.scss` files | +| `main.wo` | bootstrap: seed, routes, serve — nothing else | `app-routing.module.ts` + `main.ts` | + +Separation is compiler-enforced where the language allows it today: +each feature's VIEW directory is a module — the root controllers see +only its `pub` classes and `layout`'s exports. Controllers themselves +sit in the root module beside `types.wo`, because of gap #1 below. + +## What is deliberately different (doctrine) + +- **No `.html`/`.scss` template files.** Views are `.wo` code — the + compiler type-checks them, `esc()` is the one escaping rule, and no + template engine runs at request time. 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()`; + a controller is a class satisfying `Handler`. Capture = a field. +- **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. +- **`pub` + `@table` cannot combine yet (recorded gap #1).** An + annotated class cannot be exported, so a shared `types/` MODULE is + impossible today — which is why the controllers live in the root + module with `types.wo` instead of inside their feature folders. A + one-clause grammar fix closes this; until then the template shows the + honest layout. +- **`@view` projection classes (recorded gap #2):** today controllers + copy row fields into view classes by hand. The wished-for form — + `class ProductCard @view { ... }` filled by + `from p in Product select p.name, p.price` — needs projection + queries; recorded, not worked around. + +## Judging the DX — what to look at + +1. `types.wo` — the entire persistence layer is 20 lines. +2. `orders.controller.wo` — the whole buying flow (validate, stock + check, decrement, durable insert, render) with no framework magic. +3. `product_list/view.wo` — is markup-as-code readable enough without + templates? This file is the referendum. +4. `main.wo` — the app at a glance: five routes, one middleware, serve. diff --git a/docs/examples/shop/assets/style.css b/docs/examples/shop/assets/style.css new file mode 100644 index 0000000..cdafc59 --- /dev/null +++ b/docs/examples/shop/assets/style.css @@ -0,0 +1,47 @@ +/* shop/assets/style.css — one real stylesheet, sectioned per feature. + Served by static_files/controller.wo; app_shell links it. This file is + the .scss stand-in: the language ships no preprocessor, so styles are + plain CSS kept OUT of the markup code. */ + +/* ---- layout (app.wo, header.wo, footer.wo) ---- */ +* { box-sizing: border-box; margin: 0; padding: 0; } +body { font-family: system-ui, sans-serif; background: #f9fafb; color: #111827; line-height: 1.6; } +.site-header { display: flex; align-items: center; justify-content: space-between; + padding: .75rem 1.5rem; background: #fff; border-bottom: 1px solid #e5e7eb; + position: sticky; top: 0; } +.brand { font-size: 1.25rem; font-weight: 700; color: #111827; text-decoration: none; } +.site-nav a { margin-left: 1rem; color: #2563eb; text-decoration: none; } +.site-nav a:hover { text-decoration: underline; } +.site-main { max-width: 64rem; margin: 0 auto; padding: 2rem 1rem; } +.site-footer { border-top: 1px solid #e5e7eb; color: #6b7280; font-size: .875rem; + padding: 2rem 1rem; text-align: center; margin-top: 4rem; } +h1 { margin-bottom: 1rem; } + +/* ---- product_list ---- */ +.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; } +@media (max-width: 640px) { .grid { grid-template-columns: 1fr; } } +.card { background: #fff; border: 1px solid #e5e7eb; border-radius: .5rem; + padding: 1.5rem; box-shadow: 0 1px 2px rgba(0,0,0,.05); } +.card h3 { margin-bottom: .5rem; } +.card h3 a { color: #111827; text-decoration: none; } +.card h3 a:hover { color: #2563eb; } +.price { font-weight: 700; } +.stock { color: #6b7280; font-size: .875rem; } +.stock.out { color: #b91c1c; font-weight: 700; } + +/* ---- product_page ---- */ +.card.detail { max-width: 28rem; } +.card.detail form { margin-top: 1rem; display: flex; flex-direction: column; gap: .5rem; } +.card.detail label { font-size: .875rem; color: #374151; } +.field { display: block; width: 100%; border: 1px solid #e5e7eb; border-radius: .25rem; + padding: .5rem; font-size: .875rem; } +.btn { background: #2563eb; color: #fff; border: 0; border-radius: .25rem; + padding: .5rem 1rem; font-weight: 700; cursor: pointer; } +.btn:hover { background: #1d4ed8; } + +/* ---- orders ---- */ +table.orders { width: 100%; border-collapse: collapse; background: #fff; + border: 1px solid #e5e7eb; border-radius: .5rem; } +table.orders th, table.orders td { text-align: left; padding: .5rem .75rem; + border-bottom: 1px solid #e5e7eb; } +table.orders th { background: #f9fafb; font-size: .875rem; color: #374151; } diff --git a/docs/examples/shop/layout/app.wo b/docs/examples/shop/layout/app.wo new file mode 100644 index 0000000..7e2905b --- /dev/null +++ b/docs/examples/shop/layout/app.wo @@ -0,0 +1,34 @@ +-- layout/app.wo — the app shell (app.html's analog): one full document +-- wrapping every page with the shared header and footer. Styles are NOT +-- inlined here — the shop links /assets/style.css, a real stylesheet +-- served by the static_files controller, so view markup and styling +-- stay separate files exactly as the template promises. +use framework/http +use html + +pub fn app_shell(title: Text, content: Text) -> Text { + let d = ""; + d = d .. ""; + d = d .. "${esc(title)}"; + d = d .. ""; + d = d .. ""; + d = d .. header(); + d = d .. el("main", "site-main", content); + d = d .. footer(); + d = d .. ""; + return d; +} + +-- The one transport helper every controller shares: a 200 HTML Resp. +pub fn ok_html(body: Text) -> Resp { + let h: map = {}; + h["content-type"] = "text/html; charset=utf-8"; + return Resp { status: 200, headers: h, body: body }; +} + +pub fn html_error(status: Int, title: Text, msg: Text) -> Resp { + let content = el("h1", "", esc(title)) .. el("p", "", esc(msg)) .. el("p", "", link("/", "", "Back to products")); + let h: map = {}; + h["content-type"] = "text/html; charset=utf-8"; + return Resp { status: status, headers: h, body: app_shell("shop — ${title}", content) }; +} diff --git a/docs/examples/shop/layout/footer.wo b/docs/examples/shop/layout/footer.wo new file mode 100644 index 0000000..e840075 --- /dev/null +++ b/docs/examples/shop/layout/footer.wo @@ -0,0 +1,6 @@ +-- layout/footer.wo — the footer fragment (footer.html's analog). +use html + +pub fn footer() -> Text { + return el("footer", "site-footer", "writeonce shop — one binary: server, database, these pages."); +} diff --git a/docs/examples/shop/layout/header.wo b/docs/examples/shop/layout/header.wo new file mode 100644 index 0000000..3426d4a --- /dev/null +++ b/docs/examples/shop/layout/header.wo @@ -0,0 +1,8 @@ +-- layout/header.wo — the header fragment (header.html's analog). +use html + +pub fn header() -> Text { + let brand = link("/", "brand", "writeonce shop"); + let nav = link("/", "", "Products") .. link("/orders", "", "Orders"); + return el("header", "site-header", brand .. el("nav", "site-nav", nav)); +} diff --git a/docs/examples/shop/main.wo b/docs/examples/shop/main.wo new file mode 100644 index 0000000..af005d6 --- /dev/null +++ b/docs/examples/shop/main.wo @@ -0,0 +1,49 @@ +-- shop/main.wo — the bootstrap (app-routing.module's analog): seed the +-- store on first boot, wire routes to the feature controllers, serve. +-- No rendering and no queries here beyond the seed. +-- +-- WO_DATA=./data ./target/shop 8080 (run from the shop directory: +-- /assets/* serves from ./assets) +use framework +use framework/router +use product_list +use product_page +use orders +use static_files + +fn seed_if_empty() { + let n = 0; + for p in from x in Product take 1 select x { + n = n + 1; + } + if n > 0 { + return; + } + insert Product { sku: "keyb-75", name: "75% mechanical keyboard", price: 89.0, stock: 12 }; + insert Product { sku: "mug-wal", name: "WAL-backed coffee mug", price: 14.5, stock: 40 }; + insert Product { sku: "tee-own", name: "Ownership-checked t-shirt", price: 24.9, stock: 25 }; + insert Product { sku: "desk-pad", name: "Deskmat (one binary edition)", price: 19.0, stock: 0 }; +} + +fn main(args: multi Text) -> Int { + if len(args) < 1 { + print_err("usage: shop (WO_DATA= makes the store durable)"); + return 2; + } + let port = parse_int(args[0]); + if port == nil { + print_err("shop: must be a number"); + return 2; + } + + seed_if_empty(); + + let app = App { middleware: [], routes: [] }; + app.use_mw(Mw { m: Logging { pad: 0 } }); + app.get("/", ListProducts { pad: 0 }); + app.get("/p/:sku", ShowProduct { pad: 0 }); + app.post("/orders/:sku", CreateOrder { pad: 0 }); + app.get("/orders", ListOrders { pad: 0 }); + app.get("/assets/*path", StaticFiles { dir: "assets" }); + return app.serve("127.0.0.1", port); +} diff --git a/docs/examples/shop/orders.controller.wo b/docs/examples/shop/orders.controller.wo new file mode 100644 index 0000000..71cd0d5 --- /dev/null +++ b/docs/examples/shop/orders.controller.wo @@ -0,0 +1,55 @@ +-- 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 layout +use orders +use time + +pub class CreateOrder { + pad: Int + fn handle(req: Req) -> Resp { + let sku = req.params["sku"]; + if sku == nil { + return html_error(404, "No such product", "The order names no product."); + } + let f = form_values(req); + if f == nil { + return html_error(400, "Bad order", "The form did not arrive form-encoded."); + } + let qraw = f["qty"]; + if qraw == nil { + return html_error(400, "Bad order", "How many? The qty field is missing."); + } + let qty = parse_int(trim("${qraw}")); + if qty == nil or qty < 1 { + return html_error(400, "Bad order", "qty must be a positive number."); + } + let hits = from p in Product where p.sku == sku take 1 select p; + if len(hits) == 0 { + return html_error(404, "No such product", "Nothing is listed under that sku."); + } + let p = hits[0]; + if p.stock < qty { + return html_error(409, "Not enough stock", "Only ${p.stock} left of ${p.name}."); + } + let total = p.price * float(qty); + p.stock = p.stock - qty; + insert Order { product: p, qty: qty, total: total, placed: time.now(), status: "placed" }; + let page = OrderOk { name: p.name, qty: qty, total: total }; + return ok_html(app_shell("shop — order placed", page.render())); + } +} + +pub class ListOrders { + pad: Int + fn handle(req: Req) -> Resp { + let rows = ""; + for o in from x in Order select x { + let row = OrderRow { name: o.product.name, qty: o.qty, total: o.total, status: o.status }; + rows = rows .. row.render(); + } + let page = OrdersPage { rows: rows }; + return ok_html(app_shell("shop — orders", page.render())); + } +} diff --git a/docs/examples/shop/orders/view.wo b/docs/examples/shop/orders/view.wo new file mode 100644 index 0000000..c7f60ba --- /dev/null +++ b/docs/examples/shop/orders/view.wo @@ -0,0 +1,44 @@ +-- orders/view.wo — the confirmation page and the orders table. +use html + +pub class OrderOk { + name: Text + qty: Int + total: Float + + fn render() -> Text { + let head = el("h1", "", "Order placed"); + let what = el("p", "", "${self.qty} × ${esc(self.name)} — total € ${self.total}"); + let links = el("p", "", link("/orders", "", "See all orders") .. " · " .. link("/", "", "Keep shopping")); + return el("div", "card", head .. what .. links); + } +} + +pub class OrderRow { + name: Text + qty: Int + total: Float + status: Text + + fn render() -> Text { + let cells = el("td", "", esc(self.name)); + cells = cells .. el("td", "", "${self.qty}"); + cells = cells .. el("td", "", "€ ${self.total}"); + cells = cells .. el("td", "", esc(self.status)); + return el("tr", "", cells); + } +} + +pub class OrdersPage { + rows: Text + + fn render() -> Text { + let head = el("h1", "", "Orders"); + if self.rows == "" { + return head .. el("p", "", "No orders yet — " .. link("/", "", "go buy something") .. "."); + } + let thead = el("tr", "", el("th", "", "Product") .. el("th", "", "Qty") .. el("th", "", "Total") .. el("th", "", "Status")); + let table = el("table", "orders", thead .. self.rows); + return head .. table; + } +} diff --git a/docs/examples/shop/product_list.controller.wo b/docs/examples/shop/product_list.controller.wo new file mode 100644 index 0000000..6035d08 --- /dev/null +++ b/docs/examples/shop/product_list.controller.wo @@ -0,0 +1,20 @@ +-- product_list.controller.wo — the CONTROLLER for /: query the model, +-- fill the view classes, answer a Resp. One controller file per feature; +-- they live in the ROOT module because the @tables do (see types.wo's +-- note on the pub+@table gap) — the views stay behind their module line. +use framework/http +use layout +use product_list + +pub class ListProducts { + pad: Int + fn handle(req: Req) -> Resp { + let cards = ""; + for p in from x in Product order by x.name select x { + let card = ProductCard { sku: p.sku, name: p.name, price: p.price, stock: p.stock }; + cards = cards .. card.render(); + } + let page = ProductListPage { cards: cards }; + return ok_html(app_shell("shop — products", page.render())); + } +} diff --git a/docs/examples/shop/product_list/view.wo b/docs/examples/shop/product_list/view.wo new file mode 100644 index 0000000..981406d --- /dev/null +++ b/docs/examples/shop/product_list/view.wo @@ -0,0 +1,33 @@ +-- product_list/view.wo — the VIEW (view.html's analog). Classes with +-- `fn render() -> Text`: fields are exactly the values displayed, the +-- markup reads top to bottom. No queries here — the controller fills +-- the fields. (A future `@view` class would let a projection query fill +-- them directly; today the controller copies the fields in.) +use html + +pub class ProductCard { + sku: Text + name: Text + price: Float + stock: Int + + fn render() -> Text { + let h = el("h3", "", link("/p/${esc(self.sku)}", "", esc(self.name))); + let price = el("p", "price", "€ ${self.price}"); + let stock = el("p", "stock", "${self.stock} in stock"); + if self.stock == 0 { + stock = el("p", "stock out", "sold out"); + } + return el("div", "card", h .. price .. stock); + } +} + +pub class ProductListPage { + cards: Text + + fn render() -> Text { + let head = el("h1", "", "Products"); + let grid = el("div", "grid", self.cards); + return head .. grid; + } +} diff --git a/docs/examples/shop/product_page.controller.wo b/docs/examples/shop/product_page.controller.wo new file mode 100644 index 0000000..8023481 --- /dev/null +++ b/docs/examples/shop/product_page.controller.wo @@ -0,0 +1,21 @@ +-- product_page.controller.wo — the CONTROLLER for /p/:sku. +use framework/http +use layout +use product_page + +pub class ShowProduct { + pad: Int + fn handle(req: Req) -> Resp { + let sku = req.params["sku"]; + if sku == nil { + return html_error(404, "No such product", "The address is missing a product."); + } + let hits = from p in Product where p.sku == sku take 1 select p; + if len(hits) == 0 { + return html_error(404, "No such product", "Nothing is listed under that sku."); + } + let p = hits[0]; + let page = ProductPage { sku: p.sku, name: p.name, price: p.price, stock: p.stock }; + return ok_html(app_shell("shop — ${p.name}", page.render())); + } +} diff --git a/docs/examples/shop/product_page/view.wo b/docs/examples/shop/product_page/view.wo new file mode 100644 index 0000000..2acbc04 --- /dev/null +++ b/docs/examples/shop/product_page/view.wo @@ -0,0 +1,25 @@ +-- product_page/view.wo — the product detail view with the order form. +use html + +pub class ProductPage { + sku: Text + name: Text + price: Float + stock: Int + + fn render() -> Text { + let head = el("h1", "", esc(self.name)); + let price = el("p", "price", "€ ${self.price}"); + let stock = el("p", "stock", "${self.stock} in stock"); + let form = ""; + if self.stock > 0 { + let qty = text_input("qty", "1"); + let buy = submit_btn("Buy"); + form = form_post("/orders/${esc(self.sku)}", el("label", "", "Quantity") .. qty .. buy); + } else { + form = el("p", "stock out", "sold out"); + } + let back = el("p", "", link("/", "", "← all products")); + return el("div", "card detail", head .. price .. stock .. form) .. back; + } +} diff --git a/docs/examples/shop/static_files/controller.wo b/docs/examples/shop/static_files/controller.wo new file mode 100644 index 0000000..5533d36 --- /dev/null +++ b/docs/examples/shop/static_files/controller.wo @@ -0,0 +1,33 @@ +-- 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 = {}; + h["content-type"] = ct; + return Resp { status: 200, headers: h, body: "${body}" }; + } +} diff --git a/docs/examples/shop/types.wo b/docs/examples/shop/types.wo new file mode 100644 index 0000000..2dd41a3 --- /dev/null +++ b/docs/examples/shop/types.wo @@ -0,0 +1,29 @@ +-- types.wo — the MODEL. Every @table class IS a WAL-backed table: rows +-- persist under WO_DATA and replay on restart; without WO_DATA the +-- store is RAM-only (handy while developing). Nothing else lives here — +-- no rendering, no request handling. +-- +-- Root module by NECESSITY, not choice: `pub` and `@table` cannot +-- combine yet (recorded language gap), so tables cannot be exported to +-- other modules — everything that queries them (the controllers) lives +-- in the root module too. When the gap closes, this file becomes a +-- `types/` module and the controllers move into their feature folders. + +@table(name: "products", index: [sku]) +class Product { + sku: Text @unique + name: Text + price: Float + stock: Int + + orders: backlink Order.product +} + +@table(name: "orders", index: [product]) +class Order { + product: ref Product + qty: Int + total: Float + placed: Int -- epoch ms (time.now at purchase) + status: Text -- "placed" in v1; a fulfilment flow would grow this +} diff --git a/docs/examples/shop/wo.toml b/docs/examples/shop/wo.toml new file mode 100644 index 0000000..ee459bc --- /dev/null +++ b/docs/examples/shop/wo.toml @@ -0,0 +1,12 @@ +name = "shop" +version = "0.1.0" +description = "The writeonce program template: an MVC-separated shop — @table model, render() view classes, controller handlers, static assets" + +[runtime] +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" } diff --git a/docs/stories/language-runtime-database/refine/37-wo-html-components.md b/docs/stories/language-runtime-database/refine/37-wo-html-components.md index bd9ca68..db22229 100644 --- a/docs/stories/language-runtime-database/refine/37-wo-html-components.md +++ b/docs/stories/language-runtime-database/refine/37-wo-html-components.md @@ -15,6 +15,29 @@ status: refine > own dependency (the site sample's two-dep lesson). Unscheduled — > independent of the concurrency chain; needs its spec brainstormed > first. +> +> **REDIRECTED 2026-08-23** (developer review of the shop template +> against a Vue SFC): render()-as-string-concatenation failed the DX +> referendum — markup must be markup-FIRST. New direction: +> **`view.html` template files COMPILED BY `woc` into render code** — +> `{{ self.name }}` interpolation (auto-escaped; a raw opt-out spelling +> for prebuilt fragments), `w:if`/`w:for` structural attributes, typed +> against the view class's fields at compile time (a typo'd field is a +> compile error), NO template engine at runtime — the doctrine's real +> meaning becomes "no template interpreted at request time", not "no +> template files". A runtime mustache-lite was considered and REJECTED: +> reflection-free means values degrade to `map` — typing +> lost. Client-side reactivity from the Vue sample (`ref`, `@click`, +> `v-model`) stays out under the no-JS posture; qty steppers are form +> fields, actions are POSTs. This makes 37 a COMPILER iteration too +> (template-to-code lowering), gated behind the standing +> "no compiler/VM/database changes yet" directive — schedule +> accordingly. The shop template +> ([`docs/examples/shop`](../../../examples/shop/README.md)) is the +> consumer: its `view.wo` classes become `view.html` + view classes, +> and its README's recorded gaps ride along (gap #1: `pub` + `@table` +> cannot combine — blocks a shared model module; gap #2: `@view` +> projection classes). ## Why this iteration exists @@ -78,8 +101,9 @@ detection, event bindings, SPA router) deliberately does not. - Client-side anything: change detection, event/two-way bindings, SPA routing, hydration — the no-JS posture stands; interactivity is form round trips until a directive says otherwise. -- A template LANGUAGE (files parsed at build or runtime) — templates are - `.wo` code by doctrine (no closures also means no template lambdas). +- A RUNTIME template engine (files parsed per request, mustache-style) — + rejected 2026-08-23: reflection-free means untyped `map` + values. Templates compile to code at build time or they don't exist. - Dependency injection / services — components are data-in, Text-out. - Moving wo-html into the framework — settled 2026-08-23: separate libraries, composed via `[deps]`. @@ -88,21 +112,25 @@ detection, event bindings, SPA router) deliberately does not. ## Info -Forks the spec must settle: +Forks the spec must settle (REVISED 2026-08-23 for the compiled-template +direction): -1. **Interface shape** — `render() -> Text` alone, or `render(ctx) -> - Text` with a context record (e.g. the request's principal for - view-level decisions)? Leaning: bare `render()` — context smells like - DI; whatever the view needs arrives as a field. -2. **The framework seam** — does `ok_html(body)` move into the framework +1. **Template pairing** — Vue-SFC style (one `view.html` whose + frontmatter/`