- Board renamed docs/plan/00-kanban.md -> docs/00-status.md and rebuilt: ▶ NEXT PLAN pointer (iteration 4 — emitter, corpus, `woc build`) then six buckets — stories, in progress, done, pending, discarded, learnings. It covered only the Rust runtime before, so the whole OOP track was invisible. All 16 inbound refs repointed; `Kanban:` banners renamed to `Status:`. - New discarded.md (settled rejections with reasons: inheritance, `abstract`, Money/SKU/Float, Dynamic/cast/macro/extern, AOT-to-C, Menhir, shared engine state) and learnings.md (plumbed≠enforced, vacuous goldens, exit-0-wrong- output, malloc-path ASan trick, deferred checks that never reach the VM). - RECOVERED docs/plan/exploration/blue-green-vm/00-vision.md — gone from disk, never committed (gitignored path), cited by five docs incl. principle 12. Root cause was broader: all seven forward-roadmap plans in docs/superpowers/plans/ were untracked and ignored, on one disk only. Dropped the docs ignore rules with a do-not-re-add note; added __pycache__/*.pyc. - Repaired broken links across docs/, 270 -> 36: fixes a regression from the earlier reference/ -> .dev/reference/ move (relative paths at ../../ and deeper were skipped), plus depth and reorg drift. The 36 residual point at content that does not exist and need decisions, not paths. - New spec docs/superpowers/specs/2026-08-10-logwatcher-gap-closure-design.md, applied: `and`/`or` verdict row; Part 3 gains `env` (six modules), swaps time.mono for iso/local, adds 22 bare core builtins; throw/time.mono/is cut (0 uses in the sample). Plan 8: Task 2 gains and/or, Task 5 drops throw, abstract+`is` task deleted, 8/9 renumber to 7/8. Plan 9 gains core builtins. Plan 10 gains the 307 -> 0 diagnostic gate. WO-E205 re-filed unreachable-by- design. types.ml header drops its false satisfaction-set claim. 00-code- review.md reduced to a stub — its rival Phase 1-4 roadmap retired.
7.6 KiB
01 — .htmlx format spec
Context sources: ./00-overview.md §§ ".htmlx with live subscriptions — target format" (L127–166), "Design decisions" (L28–37), reference/crates/wo-htmlx/ (the v1 template engine that 90% of this phase ports), templates/article.htmlx and templates/home.htmlx (v1 concrete usage), docs/examples/ecommerce/shared/components/order-row.htmlx (the live-binding workload this format must serve).
Goal
Lock the exact .htmlx grammar — every v1 Mustache construct unchanged plus two new constructs: a <wo:live source=… key=…>…</wo:live> subscription subtree and a wo:bind="field" field-level live attribute — and define the JSON manifest emitted in a <script data-wo-manifest> so the client runtime (phase 03) knows which DOM nodes map to which subscription rows and fields.
Design decisions (locked)
- Mustache constructs carry through unchanged.
{{path}},{{#each xs as y}}…{{/each}},{{#if cond}}…{{/if}},{{#when cond}}…{{/when}},{{> partial arg=val}}. The v1 parser already handles all of these; the new parser inherits them verbatim. Seereference/crates/wo-htmlx/src/parser.rs(173 LOC) and the AST inast.rs(18 LOC). <wo:live>is a parsed structured node, not HTML passthrough. The parser recognises the<wo:prefix, captures attributes (source,key, optionalsort,filter), and recursively parses the body as a normal.htmlxsubtree. No nesting in this phase — error at parse if a<wo:live>contains another<wo:live>.wo:bind="field"is an HTML attribute, parsed but emitted verbatim. SSR writes the attribute through; the consumer is the client runtime. The parser records each(element, field)pair into the manifest; nothing else changes about element rendering.- Helpers are a closed Rust enum. v1 invocation forms (
{{relative ts}},{{#if (eq for "ops")}},{{> money amount=x}}) carry through. The registered set is fixed for this phase:relative,eq,markdown,code,money,tag-chips,pill,image,stock-badge,list. No author extensibility. - Manifest is one JSON object per page. Emitted as
<script type="application/json" data-wo-manifest>{ … }</script>and consumed only by phase 03's client runtime. Schema below;version: 1is a constant for this phase.
Scope
New files inside crates/ui/src/htmlx/
| File | Responsibility | Port source |
|---|---|---|
mod.rs |
Re-exports Template, Manifest, LiveSubscription, BindSite, ParseError, RenderError |
reference/crates/wo-htmlx/src/lib.rs (11 LOC) |
ast.rs |
Adds Node::Live { attrs, body } and wo_bind: Option<String> on element nodes |
reference/crates/wo-htmlx/src/ast.rs (18 LOC) — extend by ~50 LOC |
parser.rs |
Adds <wo: prefix recognition + attribute capture; rest unchanged |
reference/crates/wo-htmlx/src/parser.rs (173 LOC) — extend by ~90 LOC |
value.rs |
Path resolution against a context Value | reference/crates/wo-htmlx/src/value.rs (122 LOC) — copied verbatim |
registry.rs |
Closed helper-fn registry | reference/crates/wo-htmlx/src/registry.rs (121 LOC) — extend by ~60 LOC for new helpers |
render.rs |
Emits HTML; wraps <wo:live> body in <div data-wo-subscription="…"> for the runtime |
reference/crates/wo-htmlx/src/render.rs (140 LOC) — extend by ~70 LOC |
manifest.rs |
Walks the AST, collects subscriptions + bind sites, serialises JSON | new (~150 LOC) |
Total: ~835 LOC (585 ported + ~250 new).
Cargo.toml change
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
(Both already in crates/rt; crates/ui adopts them rather than hand-rolling JSON for the manifest at this phase.)
Manifest schema
{
"version": 1,
"subscriptions": [
{
"id": "orders-live-0",
"source": "Order{ status != Cancelled }",
"key": "id",
"sort": "placed_at desc",
"filter": null,
"root_selector": "[data-wo-subscription=\"orders-live-0\"]"
}
],
"bind_sites": [
{ "subscription_id": "orders-live-0", "key": "id", "field": "status" },
{ "subscription_id": "orders-live-0", "key": "id", "field": "total" }
]
}
API shape (target)
use ui::htmlx::{Template, Manifest, HelperRegistry};
let tmpl = Template::parse(src)?; // Result<Template, ParseError>
let html = tmpl.render(&ctx, ®istry)?; // Result<String, RenderError>
let mani = tmpl.manifest(); // Manifest
// SSR pattern: page = head + html + "<script data-wo-manifest>" + mani.to_json() + "</script>"
Exit criteria
cargo build -p uigreen; no new top-level dependencies beyondserde/serde_json.- Golden parse + render for every
.htmlxfile underdocs/examples/blog/ui/components/anddocs/examples/ecommerce/shared/components/— output is HTML and parses back into an isomorphic AST. - Manifest emission for
<wo:live source="Order{ status != Cancelled }" key="id" sort="placed_at desc">…</wo:live>produces aLiveSubscriptionwith the source string preserved verbatim and the body wrapped underdata-wo-subscription="orders-live-0". - Bind-site collection for
<td wo:bind="status">{{status}}</td>inside<wo:live>records(subscription_id, key="id", field="status")once and only once. - v1 regression:
cargo run --bin wo -- run docs/examples/blogcontinues to start without parser errors. The blog sample has no<wo:live>orwo:bindtoday; nothing should regress. - All 14 existing
crates/rttests pass.
Non-scope
- No SSR. Phase 02 emits templates; phase 03 ships the runtime; serving them is a downstream concern that the per-app binary in phase 05 wires together.
- No nested
<wo:live>. Parse error in this phase. Author can compose live subtrees by partial inclusion ({{> child}}). - No author-extensible helpers. The closed enum is the contract for this phase.
- No streaming render. Templates are rendered to a single
String. - No manifest version negotiation. Wire format is
version: 1always.
Verification
cargo build -p ui
cargo test -p ui --test golden_v1 # blog/ecommerce templates byte-identical
cargo test -p ui --test golden_extensions # <wo:live> + wo:bind cases
cargo test -p ui --test manifest # manifest emission
# v1 regression
cargo run --bin wo -- run docs/examples/blog &
PID=$!; sleep 1; curl -fsS http://127.0.0.1:8080/ >/dev/null; kill $PID
cd reference/crates && cargo build && cargo test
After this phase
Phase 02 (02-ui-compiler.md) consumes the Template + Manifest types defined here as its emission target — every ##ui block compiles down to an .htmlx file that parses cleanly under this phase's parser. Phase 03 (03-client-runtime.md) consumes the manifest JSON schema as its wire input.