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.
flowchart LR
p0["Phase 0<br/>feasibility probe"] --> p1["Phase 1<br/>engine bootstrap"]
p1 --> p2["Phase 2<br/>generation pipeline"]
p2 --> p3["Phase 3<br/>breadth"]
p3 --> p4["Phase 4<br/>configurable builds"]
ox["oracle expansion<br/>(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). - 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). 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 for how a rule is
generated, 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 |
-
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. -
Held-out exam. Two corpora the recipes did not produce: WPT
css/CSS2/normal-flowpages with no text (11 of 1056 qualify) and a grammar-generated corpus (crates/harness/src/grammar.rs, 200 cases, seed0xC0FFEE). 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 styleattribute + 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.
-
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". -
Committed winners. A winner once lived only in a gitignored directory and was lost with its worktree; now
real_model_runand 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 #
flowchart LR
s1["1. DOM + style<br/>checked vs computed style"] --> s2["2. box tree<br/>checked vs box existence"]
s2 --> s3["3. fragment tree<br/>fixed sizes only"]
s3 --> s4["4. generated rules<br/>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.
-
DOM and style. An arena node tree with Stylo's traits adapted from Blitz. Checked against the browsers'
getComputedStylefor 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). -
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). -
Fragment tree, explicit sizes. Gate: every
fixed-boxcase passes. Measured: 3/3, and 25/25 over 227 cases (94/227 overall at the time, the rest needing step 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.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 existedcrates/dom/src/computed.rshad no accessor foraspect-ratio, so a box withaspect-ratio: 2 / 1andheight: autotook its height from its children like any other block.fragment.rscalls 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 admittedmodel_34(qwen3.8-27b-fast) at 109/109, every category at 100%, committed tocrates/layout/src/generated/aspect_ratio.rs(specs/aspect-ratio.md,docs/model-first-cost.md→ "Aspect ratio"). A floor for it inbench/seam-floor.shis a hand change after landing, since the landing script refuses a diff underbench/.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
displayvalues, the anonymous rows and cells 17.2.1 generates, and a third formatting context infragment.rsbeside 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 wayoverflowandinline_atomicboth 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-blockboxes. Until it existedboxtree.rsgave every non-nonedisplayone block-level box and aninline-blocksimply 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 candidatemodel_0fromqwen3.8-flash-nextat 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
transformwas ignored outright and every box was reported where normal flow put it. Committed candidatemodel_22fromqwen3.8-27b-fastat 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: fixedneeded — a fixed box's containing block is the viewport, not the nearest positioned ancestor, and until this function existedfragment.rsdecided the question itself andfixedwas flattened tostatic. Committed candidatemodel_0fromqwen3.8-flash-nextat 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 theclaudeCLI (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, isdocs/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.
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:
- Composition. Every generated function reaches layout as its own positional
argument:
fragment.rslayout_boxtakes 17 of them, threaded through every call, and each new feature adds one, so every feature spec editsfragment.rs, thelayout*signatures and the shared runners, and feature specs have to be chained withdepends-on. Formatting contexts are chosen by amatchinsidelayout_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 queuedseam-kititem inhandoff.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. - Embedding.
dom/src/style.rsallows oneDocumentper thread, found through a thread-local raw pointer (CURRENT_DOCUMENT); a second silently steals it. The viewport is the constantVIEWPORT_WIDTH/VIEWPORT_HEIGHT, andUA_STYLESHEETcovers only the tags the recipe corpus produces.layoutanddomlistharnessas a normal dependency, used only by their test binaries, so an embedder would link the test harness. Target:Documentan 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. - 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 anddocs/content-scoped-engine.mdbuild on. - Realtime performance. A styled document never changes:
Document::parsestyles once, anddom/src/mutable.rsis a separate tree Stylo never sees. Layout has no cache, though## Layout invalidationmakes it a cache key.dom/src/computed.rsfetches the element's style again for every property read, anddisplay,is_border_box,is_rtlandoverflow_x/overflow_ycompare 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
f32with 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
claudeCLI (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: fixedreuses 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.mdtakes 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 #
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,<br/>invalidation, event loop"]
end
subgraph linked["linked"]
stylo["Stylo, html5ever<br/>(generation target later)"]
never["JS engine, HarfBuzz, ICU,<br/>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.
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:
- Generated code is pure. Inputs in, fragments out; no DOM walking, shared state or dirty bits.
- The substrate speaks spec vocabulary. "Inline size" in the spec is
inline_sizein the code. - Illegal states are unrepresentable. Computed, used and actual values are distinct
types;
autois a variant; a percentage needs an explicit basis. The compiler is the first oracle. - 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
→ "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:
- Context: spec section, substrate API, sibling implementations.
- Retrieval over browser-specs, compat data and web-features.
- Best-of-N, with the verifier choosing.
- Escalate to a stronger model when every candidate fails — built, escalating to the
claudeCLI with the previous attempt's per-box error in the prompt (specs/escalation-tier.md); not yet run live. - Fine-tune on verified generations.
- 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 #
flowchart TD
d["1. Detect<br/>normalized section hash changed"] --> c["2. Classify<br/>editorial / catching up / leading / blocked"]
c --> l["3. Localize<br/>provenance index finds the functions"]
l --> r["4. Regenerate<br/>only those, as a patch"]
r --> v["5. Re-verify<br/>regression set must not lose a case"]
v --> e["6. Extend the corpus<br/>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.
- Detect. Watch browser-specs; hash each normalized section and the definitions it references. Debounce; never auto-regenerate draft sections; pin one canonical CSS level.
- Classify. Editorial; spec catching up to browsers; spec leading browsers; blocked on the style layer; substrate. Cross-spec changes land together.
- Localize. Substrate changes go to a person; style changes wait for Stylo.
- Regenerate only affected functions, anchored on numbered steps.
- 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.
- Extend the corpus. A new branch needs a new recipe — the one manual step, usually seeded from the linked WPT PR.
- 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.