This repository has no description
bez specs fixed-positioning.md
8.8 kB


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 from StyloPositionProperty::Fixed. Every existing == Position::Absolute test in boxtree.rs and fragment.rs that means "out of flow" becomes true for Fixed too. Keep those hunks small, since other seams edit these files.
  • crates/layout/src/containing_block.rs — ContainingBlockInputs (the box's own Position; its ancestors nearest-first, each with Position, 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 wrong resolve_baseline that 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 three layout* entry points.
  • crates/harness/src/containing_block_cases.rs — own Category enum with pub const ALL, Case, enumerated(), random(seed, count), built on cases::{Elem, Source, px, RESET} like absolute_cases.rs. Categories at least: fixed at top level, fixed inside a relative ancestor, fixed inside an absolute ancestor, fixed with bottom/right only, fixed with a percentage height, absolute with no positioned ancestor, absolute inside nested positioned ancestors (nearest wins), static inside a padded block container. Never add to crates/harness/src/cases.rs RECIPES.
  • crates/generation/src/containing_block_runner.rs, a Seam::ContainingBlock variant (candidate_crate.rs), prompt::build_containing_block, admit::record_for_containing_block, and containing_block_ground_truth_cache, containing_block_generation_loop, containing_block_real_model_run under crates/generation/src/bin/, plus one baseline and two seeded-wrong fixtures under crates/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>:

  1. containing_block_ground_truth_cache — fills corpus/containing-block-ground-truth/ from the three browsers via oracle/oracle.mjs; record the case count and drops for engine disagreement.
  2. 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.
  3. containing_block_real_model_run against the folknet model gateway, with NEV_PROVIDER unset and NEV_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:1800 set after sourcing .env. Each entry is model: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 way absolute_real_model_run does (crate::orchestrate print_per_model_summary, best_per_model, and the model_of lookup at admission), so provenance records the winner's own model. Only if the gateway does not answer, fall back to NEV_PROVIDER=claude-code, and record that fallback and what the gateway returned as a Decision. Never set NEV_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).