enabled: true touches: [crates/layout/src/containing_block.rs, crates/layout/src/fragment.rs, crates/layout/src/boxtree.rs, crates/layout/src/lib.rs, crates/layout/src/stacking.rs, crates/layout/src/bin/fragment_check.rs, crates/layout/src/bin/held_out_check.rs, crates/layout/src/bin/paint.rs, crates/layout/src/generated/, crates/dom/src/computed.rs, crates/harness/src/containing_block_cases.rs, crates/harness/src/lib.rs, crates/generation/, docs/model-first-cost.md, roadmap.md, corpus/**, crates/provenance/src/css21_containing_block.rs, crates/provenance/src/lib.rs, docs/generated-functions.md] depends-on: [] #
fixed-positioning: position: fixed and a containing-block seam #
position: fixed does not exist in the engine. crates/dom/src/computed.rs
position() maps Fixed (and Sticky) to Position::Static, so a fixed box is laid
out in normal flow, and crates/layout/src/fragment.rs layout() takes only the
viewport's width, so nothing could place a box against the viewport's height. The Web
Features map (docs/web-features-map.json, feature fixed-positioning) lists it as
unknown with 17 WPT test files in css/CSS2/positioning and css/css-position.
A fixed box's size and offsets follow the same CSS 2.1 §10.3.7/§10.6.4 equations an
absolutely positioned box does, so the existing absolute seam
(crates/layout/src/absolute.rs AbsoluteResolver, generated in
crates/layout/src/generated/absolute.rs) solves them unchanged. What differs is which
box is the containing block (§10.1). Today that choice is hand-written into
fragment.rs (the is_containing_block test beside layout_absolute_box, where
pending absolutes resolve against the nearest positioned ancestor or the tree root).
This spec lifts that choice into a generated seam covering all four §10.1 rules and
adds fixed as the one new input it has to get right. That keeps the model-generated
part of the work real: a seam with its own corpus, drawn on the model pool, not only
wiring.
Scope. In: position: fixed on block-level, non-replaced boxes with no text; the
containing block of every box by CSS 2.1 §10.1 rules 1–4 (root → initial containing
block; static/relative → the content edge of the nearest block-container ancestor;
fixed → the viewport; absolute → the padding edge of the nearest ancestor whose
position is not static, else the initial containing block); a fixed box escaping
positioned ancestors, relative and absolute alike. The oracle never scrolls, so a
fixed box and an absolute box against the initial containing block land in the same
place at scroll 0. The corpus has to separate them by nesting fixed boxes inside
positioned ancestors and by using bottom/percentage heights against the 600 px
viewport height. Out: scrolling and scroll position, transform/filter/contain
making an ancestor the containing block for fixed descendants (CSS Transforms, not CSS
2.1), position: sticky (stays mapped to Static), paged media. Say so in the seam's
module doc.
Viewport height. Pass the viewport as (width, height) to layout(),
layout_with_resolver and layout_with_resolvers, the same shape
crates/layout/src/lib.rs scrollbars() already takes. That keeps it one argument
changed at each call site rather than a new one appended. Callers pass 800×600, the
oracle's VIEWPORT (oracle/oracle.mjs). Every caller listed in touches updates
together, and the change is mechanical: no score may move because of it alone. Check
this before building the seam: fragment_check --random stays 227/227 and
bench/seam-floor.sh's four floors hold.
Build the same set the absolute seam has:
crates/dom/src/computed.rs:Position::Fixed, mapped fromStyloPositionProperty::Fixed. Every existing== Position::Absolutetest inboxtree.rsandfragment.rsthat means "out of flow" becomes true forFixedtoo. Keep those hunks small, since other seams edit these files.crates/layout/src/containing_block.rs—ContainingBlockInputs(the box's ownPosition; its ancestors nearest-first, each withPosition, whether it is a block container, and its content-box and padding-box rects in viewport coordinates; the viewport rect),ContainingBlockOutputs(the chosen rect, plus which rule chose it, for reporting),type ContainingBlockResolver, and a deliberately wrongresolve_baselinethat reproduces today's behaviour (a fixed box treated as static).fragment.rs: call the resolver where the containing block is chosen now. Pending fixed boxes bubble past every positioned ancestor to the root and resolve there against the viewport rect, not the root box's padding box. One more resolver argument on the threelayout*entry points.crates/harness/src/containing_block_cases.rs— ownCategoryenum withpub const ALL,Case,enumerated(),random(seed, count), built oncases::{Elem, Source, px, RESET}likeabsolute_cases.rs. Categories at least: fixed at top level, fixed inside arelativeancestor, fixed inside anabsoluteancestor, fixed withbottom/rightonly, fixed with a percentageheight, absolute with no positioned ancestor, absolute inside nested positioned ancestors (nearest wins), static inside a padded block container. Never add tocrates/harness/src/cases.rsRECIPES.crates/generation/src/containing_block_runner.rs, aSeam::ContainingBlockvariant (candidate_crate.rs),prompt::build_containing_block,admit::record_for_containing_block, andcontaining_block_ground_truth_cache,containing_block_generation_loop,containing_block_real_model_runundercrates/generation/src/bin/, plus one baseline and two seeded-wrong fixtures undercrates/generation/fixtures/containing-block-candidates/(for example one that gives fixed boxes the nearest positioned ancestor, one that uses the content edge instead of the padding edge for absolutes).
The model endpoint and keys are read from the environment, and the binaries do not load
.env themselves. Before every run: set -a; source .env; set +a in the worktree (.env
is a symlink installed by tracks.json). Never print or commit any value from it.
Run, in order, each as cargo run --release -p generation --bin <name>:
containing_block_ground_truth_cache— fillscorpus/containing-block-ground-truth/from the three browsers viaoracle/oracle.mjs; record the case count and drops for engine disagreement.containing_block_generation_loop— the baseline seeds the regression set; both seeded-wrong fixtures must be rejected. If one is admitted, fix the loop or the corpus, not the fixture, and record what was wrong.containing_block_real_model_runagainst the folknet model gateway, withNEV_PROVIDERunset andNEV_MODELS=qwen3.8-flash-next:16:16,qwen3.8-27b-fast:32:32,qwen3.8-27b-fast-2:32:16,deepseek-v4.1-flash:2:2:1800set after sourcing.env. Each entry ismodel:count:concurrency[:timeout_secs], and the concurrencies match what each alias serves (docs/infrastructure.md→ "Rig change log"). It stops on its own after two flat batches or six, whichever comes first. Score each model's share as it arrives and print per-model counts the wayabsolute_real_model_rundoes (crate::orchestrateprint_per_model_summary,best_per_model, and themodel_oflookup at admission), so provenance records the winner's own model. Only if the gateway does not answer, fall back toNEV_PROVIDER=claude-code, and record that fallback and what the gateway returned as a Decision. Never setNEV_PROVIDER=anthropic.
When a candidate is admitted, commit it as crates/layout/src/generated/containing_block.rs
byte for byte with the header line generated/width.rs uses,
containing_block.provenance.json beside it, pub mod containing_block; in
generated/mod.rs, and the three layout* entry points defaulting to it. Record every
batch in docs/model-first-cost.md → "Batch scores and token totals", with per-model
counts, and append the outcome to roadmap.md → "Phase 1 — Engine bootstrap" step 4 in
the form the other seams use. A hand-written candidate is allowed only after every batch
has failed to admit one, and is recorded as an escalation with the batch count that
preceded it.
Gates. cargo test --workspace passes. fragment_check --random stays 227/227
with every category unchanged: no recipe in cases.rs uses position: fixed, and the
generated seam must reproduce today's containing block for every non-fixed box.
bench/seam-floor.sh's floors hold (floats 109, stacking 105, relative positioning
104, block height 105).