# 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.