writeonce/docs/stories/language-runtime-database/37-wo-html-components.md
shoney.arickathil 1fe808b7a4 docs(stories): add readiness, retire status: refine, sweep all 47 iterations
- `readiness: ready | refine` is a SECOND axis, orthogonal to status.
  `ready` = the brainstorm is complete and the decisions are LOCKED (a spec
  approved, or the forks explicitly confirmed). `refine` = open forks remain
  and it cannot be planned yet
- `status: refine` RETIRED because it carried both meanings at once, so a held
  iteration with an approved spec (language 18, 26) was indistinguishable from
  one nobody had thought about. status is now purely where the WORK is:
  done | in-progress | pending | hold — `pending` was already the board's own
  rendering word, so nothing new was invented
- all 47 iterations classified from EVIDENCE in their own text, not by guess:
  "the four forks are SETTLED" / "spec + plan approved" / "Approved spec:" for
  ready; "Forks the spec must settle" / "no spec exists yet" for refine. Every
  shipped iteration is ready by definition. 19 done, 5 in-progress, 15
  pending, 8 hold; 27 ready, 20 refine
- two iterations moved refine -> in-progress rather than -> pending: language
  31 and 34 are absorbed into 24 and work on them is literally happening, which
  the board already showed as 🔄 while their frontmatter said otherwise. That
  disagreement is now gone
- board legend, board-views' frontmatter contract, and two new Dataview
  queries updated — the useful one being `readiness: ready AND status:
  pending`, the startable set

WHAT THE NEW AXIS IMMEDIATELY SURFACED: of 15 pending iterations, exactly ONE
is startable — databasev2 4, io_uring group-commit, whose forks were confirmed
settled 2026-08-20. Everything else pending needs a brainstorm first. That was
invisible while one key carried both meanings, and it is now on the board.

Also caught by the sweep, unrelated to readiness but found by cross-checking
frontmatter against the board: SIX duplicate rows. Every iteration moved into
databasev2 was still listed in the LANGUAGE pending table under its retired id
(23, 32, 33, 20, 21, 27) as well as its new one. Stale copies removed. And two
databasev2 rows made claims the sweep contradicts — iteration 1 was billed
"startable today" while its forks are open, and 6 still called itself the
ceiling-raiser after 2 took that role.

Docs only. linkcheck 0 broken / 0 anchors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 16:54:45 +02:00

15 KiB

iteration status readiness
37 done ready

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.

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.

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. RE-POINTED 2026-08-24 (developer counter-proposal): the leaning is now in-class raw template literals, not separate .html files — render() RETURNS a raw multi-line string literal whose {{ expr }} holes are auto-escaped, compile-time-checked interpolations in class scope (a typo'd field is a compile error). Compiler surface shrinks to one lexer addition (the raw literal — a general language win: multi-line text without \" noise) plus one desugar; no file pairing, no w:component sections. Structural control stays the language's own if/for composing literals; w:if/w:for attributes and separate .html files are DEMOTED to a later option that can layer on without breaking this form. Escaping: {{ }} always escapes; the raw/slot spelling is the one greppable door. A runtime mustache-lite stays REJECTED (reflection-free means untyped map<Text, Text>); client- side reactivity (ref, @click, v-model) stays out under the no-JS posture — forms and POSTs instead. Still a COMPILER iteration, parked behind the standing "no compiler/VM/database changes yet" directive. The shop template (docs/examples/shop) is the consumer: each render() body becomes one raw template literal, and its README's recorded gaps ride along (gap #1: pub + @table cannot combine — blocks a shared model module; gap #2: @view projection classes).

CODE LANDED 2026-08-24 — the compiler slice only. Forks #1 and #3 are settled and shipped: the raw literal is BACKTICK-delimited with verbatim content, {{ }} always HTML-escapes and ${ } stays raw, and the common source margin is removed at lex time. {!! !!} as the raw slot is DISCARDED — ${ } already was the raw spelling and a second one would have been a synonym. The escaping desugar turned out to need no compiler knowledge of HTML at all: {{ e }} becomes a Call on whatever esc is in scope, so typecheck, ownership, codegen, the .wob format and the VM are all untouched, and the standing "no VM/database changes" directive was never in play. Two new lexing diagnostics ride along (WO-E004 unterminated raw literal, WO-E005 newline inside a quoted string — the latter closing a silent file-swallowing hole that existed since Task 3). The COMPONENT half of this story — the Component interface, Layout with slots, the site migrating onto them — is untouched and still pending.

LANDED 2026-08-25 — the component half, and with it the iteration. wo-html gained Component (structural, the view twin of the framework's Handler), render_all and Layout; fork #4 is settled by MOVING ok_html into the framework beside ok_text/ok_json (transport, not rendering — both HTML samples had hand-rolled the same four lines), and fork #5 by migrating BOTH samples. Fork #2 stands as written: no directive surface ships. Layout slots are pre-rendered Text, not multi Component — the compositional variant was proven possible and deliberately not taken for the layout, though the PAGE components do use it.

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 (<ng-content>) 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 RUNTIME template engine (files parsed per request, mustache-style) — rejected 2026-08-23: reflection-free means untyped map<Text, Text> 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].
  • CSS componentization (scoped styles) — the utility sheet stays one static string; measure pain first.

Info

Forks the spec must settle (REVISED 2026-08-23 for the compiled-template direction):

  1. The raw-literal spelling — SETTLED 2026-08-24 and landed: backticks, content verbatim (no escape processing), common margin removed at lex time, ${ } raw and {{ }} escaping. A literal backtick, ${ or {{ is written by concatenating a "..." string with .. — one greppable door instead of an escape character in the one form whose point is not having any.
  2. Directive surface — SETTLED 2026-08-25 as written: v1 ships NONE. Structural control is if/for composing literals, and two migrated samples produced no case that wanted more. w:if/w:for and separate .html files remain the later option — and a COMPILE-TIME include is the only shape either could honestly take, since a file read per request is the rejected engine.
  3. Escaping default — SETTLED 2026-08-24 and landed: {{ }} escapes ALWAYS, through whatever esc is in scope (wo-html's, or a local one that shadows it deliberately). ${ } is the raw door. Proven byte-identical against the hand-written esc() calls it replaces, hostile input included.
  4. The framework seam — SETTLED 2026-08-25: ok_html MOVED into framework/http/types.wo; no respond(c) sugar, because ok_html(c.render()) already is it.
  5. Migration depth — SETTLED 2026-08-25: BOTH, site gated and shop hand-driven; web-app stays HTML-less as the counter-example.

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.

Landed 2026-08-24 — the raw text literal

The compiler slice, and nothing else. compiler/src/lexer.ml gained one branch: a backtick opens a literal whose content is verbatim to the closing backtick, newlines included, with two hole forms — ${ } raw and {{ }} escaped — and the common source margin removed before the token is emitted (Java's text-block rule). token.ml gained one str_part variant to carry the escaped hole; parser.ml's desugar_interp wraps it in a call to esc; dump.ml labels it. That is the entire compiler surface. Nothing in typecheck, ownership, codegen, the .wob format or the VM changed, because the literal emits the same Str/InterpStr token a "..." string always did.

Proven: woc-test 554/0 with new token and AST goldens; oop-e2e 115/0 with a run fixture for the literal and a compile-fail fixture for WO-E004; just site 11/0 and just web-app 46/0 after wo-html's builders and the site's two chapter snippets migrated onto the form; a parity program comparing every migrated wo-html builder's old and new output on hostile input (<, >, &, ") — byte-identical in all eight. page() is the one deliberate byte change: four newlines now sit inside <head>, where whitespace is insignificant, and none inside <body>. Every .wo in the repo was re-lexed: no file gained a diagnostic.

The shop template migrated too — five view files, every render() body now one literal with real double-quoted attributes and no esc() calls — verified by building it against local file:// dep remotes and driving every route, since no just recipe gates it.

Landed 2026-08-25 — the component half

wo-html grew three things and no more: pub interface Component { fn render() -> Text }, pub fn render_all(cs: multi Component) -> Text, and pub class Layout { title, nav, content, footer } whose slots are pre-rendered Text. The MVC seam is stated in the library's own header and README, and the library still contains no query.

A finding that shaped the design: multi Component holds a heterogeneous list directly — no wrapper record. The framework's Mw/Aw wrappers had suggested otherwise; they are not a language requirement. That is what let a page component hold its children as multi Component and call render_all on them, which is where the interface actually earns its place — an interface nothing consumes as a TYPE would have been decoration.

Fork #4 settled by moving: ok_html now lives in framework/http/types.wo beside ok_text/ok_json, and both duplicate copies are deleted. No respond(c: Component) sugar — ok_html(c.render()) is already the whole thing, and a second spelling would earn nothing.

The site migrated: shell/shell_wide fill Layout (byte-identical — Layout.render() is page(title, nav .. content .. footer), exactly what the old functions built), and chapter_nav became a ChapterNav component whose query moved out into chapter_links(), called by the handlers. The homepage and every chapter page now render the SAME component with a different current — acceptance criterion 4, met by construction rather than by inspection.

The shop migrated further than planned, because its views already had render(): app_shell() became an AppShell component, and ProductListPage/OrdersPage now hold multi Component children instead of a concatenated Text blob, with the queries in the controllers. AppShell is deliberately NOT Layout — the template links a real stylesheet where Layout inlines tw_css(), and that difference is the point of it having its own shell.

Restructured 2026-08-25 (follow-on): the site sample was then laid out like the program template — types.wo for the model, a layout/ module for the chrome, home/ and chapter/ view modules, one *.controller.wo per feature in the root module, and a main.wo that is bootstrap and nothing else. main.wo went from 231 lines holding everything to 44 lines holding routes. Both samples now read the same way, which was the point: the template teaches a shape, and the site should not contradict it.

Proven: just site 11/0 (the gate, and the escape check still passes), just web-app 46/0 (the framework gained a function), woc-test and oop-e2e untouched and green. The shop was built against local file:// dep remotes and driven through every route — product grid (4 cards rendered as child components), product page with its form, buy, /orders (rows as child components), the empty-orders branch, 404, 409, and /assets/*.

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.