# Roadmap The design record: what the project is trying to prove, how, where it stands, and the decisions behind it. Detail lives in linked docs; history lives in git. ## Summary **Premise.** Generate a web rendering engine from the specifications, using three real browsers (Chromium, Firefox, WebKit) as the test oracle. A model writes each spec algorithm as a small Rust function; the browsers decide which attempts are right. If that works cheaply, anyone can have an engine, and keeping it current is a rerun rather than a rewrite. ```mermaid flowchart LR p0["Phase 0
feasibility probe"] --> p1["Phase 1
engine bootstrap"] p1 --> p2["Phase 2
generation pipeline"] p2 --> p3["Phase 3
breadth"] p3 --> p4["Phase 4
configurable builds"] ox["oracle expansion
(in parallel)"] -.-> p3 classDef done fill:#cfc,stroke:#393 classDef now fill:#ffc,stroke:#993 class p0,p1 done class p2,ox now ``` **Where it stands.** - The three-browser verification harness works and is measured ([`docs/geometry-verification.md`](docs/geometry-verification.md)). - DOM, style (via Stylo), box tree and fragment tree are hand-written and pass their oracles. - Nine CSS 2.1 layout rules sit behind small **generated functions**; eight were written by models and admitted against the browser vote, one keeps its hand-written rule ([`docs/generated-functions.md`](docs/generated-functions.md)). Together they pass the whole 227-case recipe corpus. - Every committed generated function carries a provenance record; the maintenance lifecycle that uses those records is designed but not yet run end to end. - One non-geometry feature (HTML `[Reflect]` attributes plus DOM mutation) was generated against a JavaScript-state oracle (`docs/feature-cost-experiment.md`). - A software rasterizer draws the fragment tree to a PNG ("Visual proof"). **The honest claim today.** Generated leaf functions inside a hand-built engine. The generation step is cheap; building the scaffolding for each generated function (its types, corpus, oracle and prompt) is the expensive hand work, and that cost — not tokens per candidate — decides the thesis. **Next.** Feature work run end to end by the automated track system with no hand intervention, starting with `specs/fixed-positioning.md`; then new layout surface in this order: atomic inlines, a text-metrics oracle, fixed-layout tables, then flexbox and grid. **Read next.** [`docs/generation-loop.md`](docs/generation-loop.md) for how a rule is generated, [`docs/geometry-verification.md`](docs/geometry-verification.md) for how it is checked, then "Proof of concept", "Decisions" and "Open questions" below. ## Proof of concept The thesis is proven when four claims each have a measured answer. | # | claim | result | | --- | --- | --- | | 1 | Generated functions compose into one engine | **227/227** on the recipe corpus, worst per-box error 0.0000 px | | 2 | The pass rate survives an exam the project did not write | WPT **11/11**; grammar corpus **61/164** (36 discarded) | | 3 | A model solves a layout rule nobody solved first, at a metered cost | absolute 82/108, floats 109/109, relative positioning 104/104, stacking 105/105, overflow 105/105 — no hand candidate first | | 4 | Admitted code is the engine, not a scratch file | every winner committed under `crates/layout/src/generated/` with its provenance | 1. **Composition.** `layout()` runs every admitted generated function together. On the hand-written fixtures it scored 196/227; with the model winners, 216/227; the last eleven losses were twelve-deep percentage nesting, closed by quantizing every used value onto the browsers' 1/64 px grid, truncating toward zero rather than rounding (`specs/device-pixel-rounding.md`). Rounding to nearest scored only 216/227. No loss was ever caused by two generated functions disagreeing. 2. **Held-out exam.** Two corpora the recipes did not produce: WPT `css/CSS2/normal-flow` pages with no text (11 of 1056 qualify) and a grammar-generated corpus (`crates/harness/src/grammar.rs`, 200 cases, seed `0xC0FFEE`). How the scores moved as fixes landed: | after | WPT | grammar | | --- | --- | --- | | baseline functions (`45c3932`) | 0/11 | 9/68, 132 discarded | | model-written functions assembled (`9e12a5c`) | 0/11 | 34/68 | | default body margin (`a95ef6d`) | 5/11 | 42/68 | | `style` attribute + floats (`486eb73`) | 11/11 | 42/68 | | per-node scrollbar discard rule (`a7bd8c4`) | 11/11 | 37/164, 36 discarded | | overflow function, first candidate | 11/11 | 42/164 | | overflow function, 105/105 candidate | 11/11 | 61/164 | The discard rule change moved 96 cases from "discarded" to "scored". The gap between 227/227 and 61/164 is the measure of what the recipes still miss. 3. **Model-first.** From absolute positioning on, the function's types, corpus and prompt were built by hand with tokens metered, and the model drew batches before any hand-written candidate existed. Costs and batch-by-batch results: `docs/model-first-cost.md` → "Batch scores and token totals". 4. **Committed winners.** A winner once lived only in a gitignored directory and was lost with its worktree; now `real_model_run` and its siblings write it into the source tree ("Decisions"). **Outside the proof and deferred until it is in:** a bigger model on the rig, the rig-fleet plan, the non-geometry cost experiment (dialog and popover), and automated repair iteration beyond best-of-N with a prompt edit between batches. ### Which resolver each generated function runs The real path is `crates/layout/src/fragment.rs` `layout()` — what `fragment_check`, `held_out_check` and `paint` call — plus `paint_order()` and `scrollbars()` in `crates/layout/src/lib.rs`. `layout()` passes every `crate::generated` resolver to `layout_with_resolvers()` with no flag or configuration. `layout_with_resolver()` and `layout_with_resolvers()` exist so the generation loop can score one candidate with the others held fixed; nothing on the real path calls them. | function | called from, on the real path | spec | | --- | --- | --- | | width | `layout_box()`, every in-flow block (a float's width is shrink-to-fit, CSS 2.1 10.3.5, not the generated function) | Phase 1 step 4 | | margin | `stack_children()` | Phase 1 step 4 | | percentage | `layout_box()`, `relative_shift()`, `layout_absolute_box()` | Phase 1 step 4 | | absolute | `layout_absolute_box()`, twice | `specs/absolute-batches.md` | | float | `stack_children()` | `specs/floats-bfc.md` | | relpos | `relative_shift()` | `specs/relative-positioning.md` | | height | `layout_box()`, `layout_absolute_box()` (hand-written incumbent) | `specs/block-height-equation.md` | | overflow | `scrollbars()`, which only `held_out_check` calls | `specs/overflow-scrollbars.md` | | containing block | `layout_box()` at the tree root, once per out-of-flow box | `specs/fixed-positioning.md` | | transform | `box_transform()`, once per box whose `transform` is not `none` | `specs/transforms2d.md` | | inline atomic | `layout_inline_children()`, once per container whose in-flow children are all inline-level | `specs/inline-atomic.md` | | stacking | nothing: `paint_order()` defaults to it but `paint` draws in tree order | `specs/stacking-order.md` | Follow-up: have `paint` draw in `paint_order()`'s order. Each generated function was checked to be live by laying out one small document with and without it swapped to its baseline; output differs for every one except height, whose baseline is the same correct rule. ## Visual proof Done: `crates/layout/src/bin/paint.rs` walks the fragment tree and writes a PNG per case under `target/visual-proof/` — flat rectangles for borders and backgrounds, no text, no images, no window (`specs/visual-proof.md`). Tests read the PNGs back: `--case blank` is 800×600 of canvas colour; `--case box` is a 200×100 border box at (0, 0), red inside a 10px blue border. Hand-written, not generated. ## Phases ### Phase 0 — Feasibility probe Does spec prose convert to Rust that passes against real browsers? Scope: block boxes with no text — sizes, margins, padding, borders, margin collapsing — on a DOM from `html5ever` and style from Stylo, no JavaScript. Exit: a measured first-shot rate, a rate after repair, and a failure taxonomy. Status: done. The harness agreed on 699 of 705 browser pairs across 235 documents (commit `3ca7323`). The generation half ran as Phase 1 step 4, which holds the rates and taxonomy. ### Phase 1 — Engine bootstrap ```mermaid flowchart LR s1["1. DOM + style
checked vs computed style"] --> s2["2. box tree
checked vs box existence"] s2 --> s3["3. fragment tree
fixed sizes only"] s3 --> s4["4. generated rules
one category at a time"] ``` Each step has its own oracle before the next starts. Steps 1–3 are hand-written; step 4 is generated. 1. **DOM and style.** An arena node tree with Stylo's traits adapted from Blitz. Checked against the browsers' `getComputedStyle` for properties that do not need layout (`display`, `position`, `box-sizing`, border widths, `overflow-*`, …). Measured: 2958/2958 values on the enumerated set; 31841/31841 with 200 random cases added; 6613/6613 with the SMT-sampled set (`style_check`). 2. **Box tree.** An element with no rectangles in the browser must generate no box. Measured: 223/223 nodes; 1900/1900 over 227 cases; 389/389 with the SMT set (`box_check`). 3. **Fragment tree, explicit sizes.** Gate: every `fixed-box` case passes. Measured: 3/3, and 25/25 over 227 cases (94/227 overall at the time, the rest needing step 4). 4. **Generated algorithms**, in order: width equation, margin collapsing, percentage resolution, then new generated functions model-first. Each lands when its category stops improving. Per-function results: [`docs/generated-functions.md`](docs/generated-functions.md). Latest: the aspect-ratio function (CSS Box Sizing 4 §4.1–§4.4), the first drawn from a sizing spec rather than CSS 2.1 and the first whose whole job is to transfer one size through a ratio when the width or the height was left `auto`. Until it existed `crates/dom/src/computed.rs` had no accessor for `aspect-ratio`, so a box with `aspect-ratio: 2 / 1` and `height: auto` took its height from its children like any other block. `fragment.rs` calls the function twice — once before the children are laid out, so a height-determined box's width is known in time for them, and once after, so §4.3's automatic minimum size can grow the box to the children's extent. Corpus of thirteen categories, runner, three binaries and two seeded-wrong fixtures, both rejected by the generation loop. One mixed model batch drew 82 candidates across four aliases and admitted `model_34` (`qwen3.8-27b-fast`) at 109/109, every category at 100%, committed to `crates/layout/src/generated/aspect_ratio.rs` (`specs/aspect-ratio.md`, `docs/model-first-cost.md` → "Aspect ratio"). A floor for it in `bench/seam-floor.sh` is a hand change after landing, since the landing script refuses a diff under `bench/`. Before it: the fixed-table-layout function (CSS 2.1 17.5.2.1 and 17.5.3), the first whose structural half had to be built from nothing — tables did not exist in the engine at all, so it needed new `display` values, the anonymous rows and cells 17.2.1 generates, and a third formatting context in `fragment.rs` beside stacking and line boxes. Two exported halves, split the way 17.5.2.1 splits the algorithm: the columns are resolved from the table's own width and its first row's cells alone, before any cell's content is laid out, and the rows from the heights that content then produced. No batch has been drawn: the committed candidate is the function's own deliberately wrong baseline, the way `overflow` and `inline_atomic` both started, because the build lane was unreachable for the whole session and both the browser oracle that fills the corpus and a model batch refuse to run anywhere else. Corpus of seventeen categories, runner, three binaries and two seeded-wrong fixtures all in place and compiling, none of them yet run (`specs/tables-fixed-layout.md`). What this function did measure is the term this step calls unmeasured: about 44 minutes of hand-written scaffolding against a model-run total of zero (`docs/model-first-cost.md` → "Fixed table layout"). Before it: the inline-atomic function (CSS 2.1 9.4.2/10.8/10.8.1), which gives the engine line boxes for the one kind of inline content that needs no font — a block container whose in-flow children are all explicitly sized `display: inline-block` boxes. Until it existed `boxtree.rs` gave every non-`none` `display` one block-level box and an `inline-block` simply stacked in normal flow. Its answer is a set of positions rather than a single value, and it is the first to place more than one box per call. Committed candidate `model_0` from `qwen3.8-flash-next` at 111/111 on its own corpus, admitted on the first batch, with one candidate from each of the four gateway models reaching 111/111. Corpus of 111 cases over fifteen categories, none dropped for engine disagreement (`specs/inline-atomic.md`). Before that: the transform function (CSS Transforms 1 §3/§4/§5), the first drawn from a spec other than CSS 2.1 and the first whose answer is a matrix. Until it existed `transform` was ignored outright and every box was reported where normal flow put it. Committed candidate `model_22` from `qwen3.8-27b-fast` at 110/110 on its own corpus, admitted on the first batch, with twelve of the twenty candidates that compiled tied at 110/110 (`specs/transforms2d.md`). Earlier still: the containing-block function (CSS 2.1 10.1), which is what `position: fixed` needed — a fixed box's containing block is the viewport, not the nearest positioned ancestor, and until this function existed `fragment.rs` decided the question itself and `fixed` was flattened to `static`. Committed candidate `model_0` from `qwen3.8-flash-next` at 106/106 on its own corpus, admitted on the first batch (`specs/fixed-positioning.md`). Failure taxonomy over the first three generated functions: no missing prerequisites; a few spec misreadings each fixed by one prompt sentence; one substrate gap (unrounded arithmetic, since fixed); one gate gap (a percentage-only mistake cannot regress anything until a first candidate is admitted). **What step 4 shows about the plan.** - *The generated part is the easy part, by construction.* The hand-written walk decides which rule applies and with what inputs; the model writes a leaf decision. This is finer than the planned one-algorithm-per-section granularity ("Decisions"). - *The model has not yet beaten a hand-written candidate* where one existed first, except width, by adding min/max clamping. Block height is the sharpest case: all 8 candidates tied the hand-written rule at 105/105. - *The cost of building each generated function's scaffolding is the unmeasured dominant term* (`docs/platform-economics.md` → "Per-feature cost"). - *The exam is the recipes.* A category at 100% means that recipe's shape is handled; the held-out corpora (claim 2) are the check on that. The batch loop now stops itself on a plateau or after six batches (`specs/automated-repair-iteration.md`), and can escalate once to the `claude` CLI (`specs/escalation-tier.md`, built, not yet run live). ### Oracle expansion — in parallel with Phase 1 Oracles for what the geometry vote cannot see (`docs/oracle-coverage.md`): - **wpt.fyi vote table** — per-subtest results across engines, joined to browser-compat-data. Done: `docs/wpt-votes.md`. - **Conformance suites** — canvas 82.6% usable, Web Audio 74.4%, WebGL 99.6% (`docs/conformance-oracles.md`). - **WebGPU CTS** — about 85% exact for validation, about 50% within ULP tolerance for numeric execution (`docs/webgpu-cts.md`). - **Trace oracle** — script-observable behaviour voted across engines. Done for hand-written probes (`docs/dump-format.md` → "Trace protocol"). - **Text metrics** — whether the three engines agree on text geometry closely enough for the 1/60 px vote to run once glyphs are involved. Every other corpus here is text-free by construction and the held-out WPT filter rejects any page with a text node, so this had never been measured; until it was, no text-bearing generated function could be scored. A text probe embeds a pinned font from the WPT checkout as a `data:` URL, refuses to measure a page that fell back to a system font, and reports each text node's line-fragment rects (`docs/dump-format.md` → "Text probe"). The result, per font and per category, is `docs/text-metrics.md`. ### Phase 2 — Generation pipeline Spec text in, Rust out, repaired first on compile errors and then on geometry differences. The loop itself: [`docs/generation-loop.md`](docs/generation-loop.md). **Provenance is a first-class artifact, not a comment.** Every generated file records the spec URL and section, a hash of the normalized spec text, a hash of every definition that text references, the spec level and revision, the model, prompt hash and seed, and the verification result that admitted it. The definition hash exists because a third of real spec changes alter meaning through a redefined term (`docs/spec-change-flow.md`). Spec steps are carried into the code as numbered comments, so a small spec edit can regenerate as a patch. Built in `crates/provenance` (`docs/provenance.md`): the record, the normalizer, the definition-closure hash, an impact index (`provenance_impact`), a live spec fetcher, and a webref-backed definition index. Not yet exercised end to end on a real spec change. ### Substrate rework — hand-built, in parallel with Phase 2 The hand-written substrate was shaped for scoring one generated function at a time against a fixed 800×600 corpus. Breadth, embedding, configurable builds and realtime use each need it reshaped first. This work is written interactively, one item at a time, not by feature jobs or background writers, because an error in the substrate invalidates every generated function above it (`## Layering`). Builds and tests still run on the build lanes (`AGENTS.md`). None of the costs below has been measured; each item's first step is a measurement where one is named. In order, each unblocking the next: 1. **Composition.** Every generated function reaches layout as its own positional argument: `fragment.rs` `layout_box` takes 17 of them, threaded through every call, and each new feature adds one, so every feature spec edits `fragment.rs`, the `layout*` signatures and the shared runners, and feature specs have to be chained with `depends-on`. Formatting contexts are chosen by a `match` inside `layout_box` (`layout_table_children`, `layout_inline_children`), so flex and grid have nowhere to plug in. Target: one table of generated functions with defaults, which a runner overrides by entry (the queued `seam-kit` item in `handoff.md`), and formatting contexts behind one interface that block, inline, table, and later flex and grid implement. Exit: every existing generated function reproduces its recorded score, and a new feature touches only its own files plus one table entry. 2. **Embedding.** `dom/src/style.rs` allows one `Document` per thread, found through a thread-local raw pointer (`CURRENT_DOCUMENT`); a second silently steals it. The viewport is the constant `VIEWPORT_WIDTH`/`VIEWPORT_HEIGHT`, and `UA_STYLESHEET` covers only the tags the recipe corpus produces. `layout` and `dom` list `harness` as a normal dependency, used only by their test binaries, so an embedder would link the test harness. Target: `Document` an ordinary value (several per thread, any viewport), the harness a dev-dependency or behind the binaries, a real default stylesheet, then a small stable API with a C interface and a WebAssembly build. 3. **Configurable builds (tree-shaking).** Nothing can be left out today: the engine crates have no cargo features, and `layout()` names every generated function, so all are linked. Target, built on the composition interface: one cargo feature per formatting context and per generated feature, a complete engine as the default build, and content using an excluded feature failing loudly. First measurement: release binary size, full build against a block-only build, with Stylo's share broken out (likely most of it, unmeasured). This is what Phase 4 and `docs/content-scoped-engine.md` build on. 4. **Realtime performance.** A styled document never changes: `Document::parse` styles once, and `dom/src/mutable.rs` is a separate tree Stylo never sees. Layout has no cache, though `## Layout invalidation` makes it a cache key. `dom/src/computed.rs` fetches the element's style again for every property read, and `display`, `is_border_box`, `is_rtl` and `overflow_x`/`overflow_y` compare freshly formatted strings. Target: incremental restyle through Stylo's invalidation, a layout cache keyed by constraint space (checked by the cold-against-warm rule in `## Layout invalidation`), style read once per box, and enum reads in place of strings. First measurement: a benchmark of parse, style and layout over the recipe corpus and one real page, run on a lane. ### Phase 3 — Breadth Expand the generated surface; module boundaries are decided here, not earlier. ### Phase 4 — Configurable engine builds Include or exclude web surface at build time: globals-attached APIs (WebRTC, WebGL, IndexedDB, Web Audio) by inclusion, formatting contexts behind a dispatch trait. **Content-scoped engines.** Analyse a body of content, find the features it uses, build only that engine (`docs/content-scoped-engine.md`). The first stage, a static analyser (`crates/content-analyser`), has landed: `mdn-docs` touches 11.65% of html.* and 2.80% of css.* compat keys, `danluu-blog` 6.92% and 0.93%. ## Decisions - **One whole algorithm per spec section is the unit of generation**, with spec steps as numbered comments. A single step has no oracle. (In practice generated functions came out finer; see "What step 4 shows about the plan".) - **A real browser, not WPT, is the primary verification loop.** A geometry diff gives per-box numeric error over unlimited input; WPT gives pass/fail over a fixed set. - **Box tree and fragment tree are separate.** One box can produce many fragments (an inline across three lines), and the split makes layout a pure function. Every production engine has migrated toward this, none away. - **Stylo provides style; the cascade is not generated early.** Generating cascade and layout together would make failures unattributable. - **Generated code is spec-literal and slow.** Easier to generate and verify, and later a reference for any optimized version. - **Layout is not designed for parallelism.** Style is where parallelism pays, and it comes with Stylo. - **Blitz contributes its Stylo glue only**, not its layout, which is physical-coordinate `f32` with no computed/used distinction. - **Text is excluded from Phase 0.** Inline layout is where engines are hardest and prose least algorithmic. - **Stylo is pinned exactly behind one boundary crate**, one document per process (`docs/stylo-risk.md`). - **WPT supplies inputs, never expected outputs.** Expected outputs come from live browsers; WPT assertions are only for scoring, and a held-out partition is never used for anything else. - **The goal is a generated core behind linked subsystems.** GPU, codecs, real-time media and the JavaScript engine stay linked. - **Logic programming is adopted narrowly**, as an oracle for intermediate layout facts: margin collapsing as Datalog agreed with the browsers on 1195/1195 offsets and caught a seeded bug the geometry vote missed entirely (`docs/logic-layer.md`, `docs/margin-collapse-experiment.md`). - **Admitted candidates are committed into the source tree with their provenance**, never left in a gitignored directory. Only regenerable output stays under `corpus/`. - **New generated functions are attempted by the model before any hand-written candidate exists.** A hand-written one is written only if every batch fails, and is recorded as an escalation. - **Candidates can come from the subscription-billed `claude` CLI** (`NEV_PROVIDER=claude-code`) as well as the per-token API, which once ran dry mid-batch. ## Open questions - **Does the economics work beyond layout?** For a subset: about 55–60% of engine-relevant compat entries have a cheap oracle and generatable prose; roughly 8–18% have no practical oracle. Per-feature *model* cost is small (234k tokens for the `[Reflect]` feature); the cost of building the scaffolding around each is what remains unmeasured (`docs/platform-economics.md`, `docs/oracle-coverage.md`). - **How risky is depending on Stylo?** Manageable with an exact pin and one adapter crate; fallback is a vendored copy (`docs/stylo-risk.md`). - **How is computed style stored once Stylo is replaced?** Grouped, shared and refcounted is the convergent answer. - **How wide should a feature's generated scope be?** When an existing generated function already does the maths (`position: fixed` reuses absolute positioning), a feature can add only hand-written wiring, or lift neighbouring hand-written logic into a new generated function so the model has real work. `specs/fixed-positioning.md` takes the second path. Later phases: - How much of WPT is reachable without JavaScript. - Where IDL codegen ends and prose-derived generation begins, per interface; only about 8–15% of API members are fully determined by IDL. ## Layering ```mermaid flowchart TB subgraph gen["generated from specs"] layout["layout algorithms"] dom["DOM semantics"] parser["HTML parser (later)"] end subgraph hand["hand-written once"] sub["box tree, fragment tree,
invalidation, event loop"] end subgraph linked["linked"] stylo["Stylo, html5ever
(generation target later)"] never["JS engine, HarfBuzz, ICU,
wgpu / vello (never generated)"] end gen --> hand hand --> linked ``` The hand-written substrate is not generated because an error there invalidates everything above it. Stylo (`servo/stylo` 0.21.0) has had 21 breaking releases in 18 months, so it is pinned exactly together with `web_atoms`, `html5ever`, `markup5ever` and `cssparser`, and only one crate names `style::` or `selectors::`. It needs Python 3 to build, and `:has()` is not parsed outside Gecko. Stylo is MPL-2.0: modifications to its own files must be published. ## Verification strategy A real browser is the training signal; WPT is the scorecard. Playwright drives Chromium, Firefox and WebKit; each dumps per-box geometry and computed style as JSON, the engine dumps the same, and the diff is numeric and per box. No rasterizer or JavaScript engine is needed in the engine under test, labelled input is unlimited, and the first wrong box names the wrong rule. Full explanation: [`docs/geometry-verification.md`](docs/geometry-verification.md). ## Geometry mapping What the fragment tree must produce to be comparable (CSSOM View): - `getClientRects()` is one border-box rect per box fragment; anonymous boxes produce none; `getBoundingClientRect()` is their union. - A fragment carries its border-box size, its offset from its parent fragment, and an optional originating node. Offsets are parent-relative, so layout stays a pure function. - Layout computes in logical coordinates; fragments store physical ones. - The harness compares sizes and parent-relative offsets, collects the full rect list (ready for inline layout), collects a computed-style subset (a second attribution axis), and treats a tree-shape mismatch as a parser failure. Precision: pass/fail at 1/60 px (`dump.rs` `TOLERANCE_PX`, Gecko's grid), with the raw delta kept as a reward signal. The engine quantizes every used value onto Blink's 1/64 px grid, truncating toward zero, through `crates/dom/src/px64.rs`, which made it bit-identical to the browsers on the recipe corpus. ## Oracles Three engines rather than two: two can only detect disagreement and discard the case; three give a majority, a named outlier, or a genuine split. Playwright drives all three through one API, so the third costs almost nothing. Measured at commit `3ca7323`: 699 of 705 engine pairs agreed across 235 documents. All six disagreements were the same case shape — twelve-deep percentage nesting — and the vote named Firefox the outlier on all sixteen affected measurements. Round numbers agreed to 0.0000 px; every disagreement involved `33.333%` or a percentage chain. The random corpus found all six; the enumerated set found none. **The Firefox finding.** Firefox's 1/60 px grid is a real web-compat problem on live sites (`docs/firefox-app-units.md` → "The ask"). **Confounds to eliminate**, before any of this means anything: the browsers' default stylesheets differ (test pages carry a reset); scrollbars steal width (fixed 800×600 viewport, per-node scrollbar check); device pixel ratio and zoom are pinned to 1. Beyond layout, other oracles matter more: conformance suites, fake-device hooks, reference libraries with exact answers, and script-observable traces (`docs/oracle-coverage.md`). ## Layout invalidation Layout is `fn(box, constraint_space) -> fragment`, so invalidation is a cache key, not an algorithm. The catch is completeness: every input a layout function reads must be in the constraint space (available inline size, block size, percentage basis, incoming collapsible margin, formatting-context membership, writing mode and direction), or the cache is silently wrong. The signature enforces this: style, constraint space, and a child-layout callback, nothing else. Out-of-flow boxes break the tree shape (their containing block is an ancestor, not the parent); as in LayoutNG, they are passed up to that ancestor and laid out in a later pass. Laying a document out cold and warm must give identical fragments — a free check for hidden inputs. No caching exists yet. ## Substrate contract The interface generated code is written against: 1. **Generated code is pure.** Inputs in, fragments out; no DOM walking, shared state or dirty bits. 2. **The substrate speaks spec vocabulary.** "Inline size" in the spec is `inline_size` in the code. 3. **Illegal states are unrepresentable.** Computed, used and actual values are distinct types; `auto` is a variant; a percentage needs an explicit basis. The compiler is the first oracle. 4. **Logical axes from day one.** Physical coordinates appear only at paint. Phase 0/1 scaffolding does not yet meet this contract itself: `fragment.rs` computes in physical coordinates with no computed/used distinction. It does quantize every used value onto the 1/64 px grid, except that `layout_absolute_box()` does not quantize the intermediate basis it hands its children (untested, not yet a measured problem). ## DOM layer, shaped by Stylo Stylo dictates the DOM side and nothing below computed style. From Blitz's trait impls: an arena of nodes keyed by id, with a pointer-sized handle (`crates/dom/src/style.rs` explains why); one handle type for node, element, document and shadow root; a Stylo-owned data slot per element; dirty-descendant bits where a set bit implies every ancestor is set; interned atoms; and the `Stylist`, lock, snapshot, device and traversal machinery. Stylo panics if run concurrently in one process (servo/stylo#361), so verification runs one document per process, with every worker thread entering `ThreadState::LAYOUT`. **Phase 0 economies:** no shadow DOM, HTML documents only, no quirks mode, no MathML or SVG, no animations, no incremental restyle, no presentational attributes. A minimal user-agent stylesheet (`UA_STYLESHEET`) is needed because Stylo has none; it includes `body { margin: 8px; }`, which only the held-out WPT pages exercise. The resolved value of `min-width: auto` is `0px` (`resolve_auto_to_zero`), and a border width with no border style is zero. ## Test input distribution The corpus is the reward function, and can mislead two ways: - **Matching Chrome is not implementing the spec.** Hence the three-engine vote, and WPT as an exam never trained against. - **Random values do not exercise an algorithm.** Hence recipes stratified by the spec's own case analysis — width-equation branches, min/max clamping, percentages against definite and auto parents, every form of margin collapsing, box-sizing, deep nesting — with fractional values (`33.333%`, 1/64 px steps) because round numbers hide rounding. Corpora and their roles: [`docs/geometry-verification.md`](docs/geometry-verification.md) → "Where the test pages come from". **Failure-weighted sampling, with a permanent regression set.** The prompt samples failing cases, and every case that has ever passed stays in the gate, so the loop cannot trade old correctness for new. Every case is seeded so any failure replays exactly. Not built yet: shrinking a failing page to a minimal one before it reaches the prompt. ## Browser compat data as a prior Of 17,663 compat notes, 238 notes and 64 `partial_implementation` flags survive for current, non-historical CSS. They predict where the three-way vote will split — a Phase 3 input, not Phase 0. About 62% of entries first shipped in 2025 were still in one engine by mid-2026, falling to about a third after two years, so breadth work follows shipping count. ## Model strategy Ordered by cost, each step only if the one before fails: 1. Context: spec section, substrate API, sibling implementations. 2. Retrieval over browser-specs, compat data and web-features. 3. Best-of-N, with the verifier choosing. 4. Escalate to a stronger model when every candidate fails — built, escalating to the `claude` CLI with the previous attempt's per-box error in the prompt (`specs/escalation-tier.md`); not yet run live. 5. Fine-tune on verified generations. 6. Reinforcement learning against the per-box geometry error — the one research-novel part: unlimited tasks, no human labelling, a dense reward. The verifier is the asset; the model is replaceable. Planned additions to the per-feature job (`docs/autonomous-loop.md`), each only after the basic job runs unattended: - **Performance runs.** Take several candidates already known to be correct and measure CPU time, wall-clock time and memory for each across many scenarios; the fastest correct candidate can win. - **Adversarial review.** One model reviews another model's winning candidate, and the reverse. - **A learning loop.** Review failures and winners over time, extract the patterns, and feed them into the prompt for new features and for regenerations. - **A swappable agent.** The runner executes the package a spec gives it; which model or agent writes candidates, verifies or reviews is a setting of that package. The job runs entirely on folknet, and a package may add a remote model (Kimi, Claude, Codex) for a whole stage or a percentage of candidates. Blocked on a person's decision: the standing rule allows only subscription-billed or self-hosted models, never the metered Anthropic API. ## Compute shape A GPU pool writes candidates; CPU servers build the ground-truth cache and score candidates (`docs/infrastructure.md`, `docs/rig-capacity.md`). Scoring is not the bottleneck — 272,930 cases/s at sixteen threads — and neither is the oracle, since ground truth is built once and cached. Writing candidates is. Candidates compile in batch (one crate per candidate costs about 24× more CPU at N=1000), and workers split a corpus by seed shard with no broker (`docs/infrastructure.md` → "Sharding without a broker"). The coding sessions that build generated functions run on Claude Code. A trial of OpenHands against the self-hosted `qwen3.8-27b-fast` completed one edit–test–commit loop unattended (`docs/writer-harness-spike.md`); one run shows it can work, not how often. ## Ongoing development lifecycle Once an engine exists, a spec change should reach a rebuilt, re-verified engine without a person reading the diff. Three triggers, each entering at a different point. ### A specification changes ```mermaid flowchart TD d["1. Detect
normalized section hash changed"] --> c["2. Classify
editorial / catching up / leading / blocked"] c --> l["3. Localize
provenance index finds the functions"] l --> r["4. Regenerate
only those, as a patch"] r --> v["5. Re-verify
regression set must not lose a case"] v --> e["6. Extend the corpus
if a new branch appeared"] e --> q{"7. green?"} q -- yes --> land["land"] q -- no --> quar["quarantine, keep old code"] ``` Evidence from three months of real changes: `docs/spec-change-flow.md`. 1. **Detect.** Watch browser-specs; hash each normalized section *and* the definitions it references. Debounce; never auto-regenerate draft sections; pin one canonical CSS level. 2. **Classify.** Editorial; spec catching up to browsers; spec leading browsers; blocked on the style layer; substrate. Cross-spec changes land together. 3. **Localize.** Substrate changes go to a person; style changes wait for Stylo. 4. **Regenerate** only affected functions, anchored on numbered steps. 5. **Re-verify.** Compile, determinism, then the cached corpus; the regression set may not lose a case. Exception: when the spec leads the browsers, affected cases are marked superseded and the linked WPT is the gate until two engines ship. 6. **Extend the corpus.** A new branch needs a new recipe — the one manual step, usually seeded from the linked WPT PR. 7. **Land or quarantine.** ### A browser updates Re-dump the corpus under the new versions and re-vote. Cases that newly split become untrustworthy; cases that newly agree become usable. Engine code does not change; a pass rate that moves anyway is a finding. ### WPT changes The exam is refreshed and rerun; WPT is never a training signal. A WPT change citing a spec issue triggers re-verification of the sections it touches and contributes its inputs to the corpus. ### Release A build selects a feature set; the provenance index is its bill of materials. ## Platform areas What builds and checks each platform area: `docs/platform-areas.md`. ## Inputs - browser-specs — https://github.com/w3c/browser-specs - web-features — https://github.com/web-platform-dx/web-features/ - browser-compat-data — https://github.com/mdn/browser-compat-data - WPT — https://web-platform-tests.org/ - wpt-gen — https://github.com/GoogleChromeLabs/wpt-gen ## Prior art to study - **Blink / LayoutNG** — constraint space in, immutable fragment out: the template for pure layout. - **Gecko** — continuations make layout non-reentrant: the cost of one tree. - **Servo** — the lasting output was crates with clean interfaces (`html5ever`, `selectors`, `cssparser`, Stylo, WebRender). - **Ladybird** — spec steps as numbered comments; spec prose is mechanical enough to transcribe. - **Blitz** — html5ever + Stylo + Taffy + Vello; the same style-to-layout boundary Nev needs, with the layout side hand-written. - **Style struct storage** — Gecko, Blink and Stylo each arrived at shared, copy-on-write style structs with an inherited/reset split.