--- enabled: true touches: [oracle/oracle.mjs, docs/dump-format.md, crates/harness/src/lib.rs, crates/layout/src/lib.rs, crates/harness/src/compare.rs, crates/harness/src/grammar.rs, crates/layout/src/bin/held_out_check.rs, crates/layout/src/overflow.rs, crates/layout/src/fragment.rs, crates/layout/src/generated/**, crates/harness/src/overflow_cases.rs, crates/harness/src/dump.rs, crates/layout/src/bin/fragment_check.rs, crates/dom/src/bin/style_check.rs, crates/generation/**, roadmap.md, docs/model-first-cost.md, corpus/**] depends-on: [] --- # `overflow-scrollbars`: a scrollbar-presence probe, then an `overflow` seam The 200-case grammar corpus (`crates/harness/src/grammar.rs::random`, seed `0xC0FFEE`) has discarded the same 132 of 200 cases across every measurement recorded in `roadmap.md` → "Proof of concept" claim 2 (9/68, 34/68, 42/68 — always +132 untrustworthy, 68+132=200). The discard is coarser than "the three engines disagree": `held_out_check`'s `score()` calls `harness::compare::compare()`, which discards outright — before any per-node comparison — the moment *either* dump's whole-document `overflowed` boolean is set (`compare.rs`, `CaseComparison::Discarded`). `overflowed` itself (`oracle/oracle.mjs::domDump`) is `documentElement.scrollWidth/scrollHeight` exceeding the fixed 800x600 viewport — one boolean for the whole page, not per node, and not contingent on the other two engines' agreement. `grammar.rs` already assigns `overflow: hidden` to 15% of generated boxes (line ~122), so the grammar corpus already exercises the property this spec gives layout meaning to; nothing in `crates/layout` resolves `overflow` today. **Scope.** In: a per-node scrollbar-presence signal in the oracle dump, a discard rule based on three-way agreement on that signal instead of any-one-engine's whole-document `overflowed`, and a generated seam giving `overflow: visible|hidden|scroll|auto` (CSS 2.1 §11.1.1) a used value the layout code acts on — specifically, whether a box whose content would otherwise overflow it gets a scrollbar, in the boolean sense (does one appear), not the exact pixel width one steals from descendants. Native scrollbar width is platform- and theme-dependent; if the three engines' dumps don't agree on a pixel figure for it, that disagreement is itself a valid, reportable finding for this spec to record, not a bug to paper over. Out: `overflow-x`/`overflow-y` as independent values, scroll position or interactivity, nested scroll containers, and painting a scrollbar in `paint.rs`. Keep to whether a scrollbar appears and whether the three engines agree it does. **Stage 1 — oracle and harness: measure before building anything in `crates/layout`.** Add per-node fields to `oracle/oracle.mjs`'s `domDump()` (inside `visit()`, alongside `bounding`/`rects`): enough to detect a rendered scrollbar per axis, e.g. `client_width`/`scroll_width` and `client_height`/`scroll_height` (a scrollbar is present on an axis when the client and scroll dimensions differ). This is additive per `docs/dump-format.md`'s own precedent for the trace, reflection and stacking probes (new optional fields / new optional operations, existing consumers unaffected) — update that doc's contract alongside the change, since it says explicitly that changing it means changing every consumer at once. Change `held_out_check`'s discard rule (`crates/harness/src/compare.rs::compare`, the `if left.overflowed || right.overflowed` check) to discard a grammar case only when the three engines disagree on scrollbar presence per node, using the existing three-way `vote()` machinery (`Consensus::{Unanimous, Majority, Split}`) rather than any single engine's document-level flag. A case where all three engines agree a scrollbar is present is no longer automatically untrustworthy — it's a case the engine currently can't model at all (nothing in `crates/layout` touches `overflow`), so it scores as a failure against the new signal rather than being discarded, which is the expected, correct outcome before Stage 2 exists. Re-run `cargo run --release --bin held_out_check -p layout -- --grammar` and record the new pass/discard/fail split in `roadmap.md` → "Proof of concept" claim 2, in the same form as its existing "Before"/"After" paragraphs. This number is the real measurement of how much the discard-rule change alone recovers, independent of Stage 2, and is a valid stopping point if Stage 2 turns out not to be worth its cost — record that as a Decision if so, rather than building `overflow.rs` speculatively. **Stage 2 — the `overflow` seam, same shape as the other generated seams, if Stage 1's numbers justify it.** - `crates/layout/src/overflow.rs` — `OverflowInputs` (the computed `overflow` value, whether content's own size exceeds the box's, content-box vs. border-box sizing), `OverflowOutputs` (does a scrollbar appear per axis), `type OverflowResolver`, `resolve_baseline`. - `crates/harness/src/overflow_cases.rs` — own `Category` enum (at least: `visible` with overflowing content, `hidden` with overflowing content, `auto`/`scroll` with and without overflowing content), `pub const ALL`, `Case`, `enumerated()`, `random(seed, count)`, built on `cases::{Elem, Source, px, RESET}`. Never add to `crates/harness/src/cases.rs`'s `RECIPES`. - `crates/layout/src/generated/overflow.rs` + `overflow.provenance.json`, `pub mod overflow;` in `generated/mod.rs`; `fragment.rs` wiring wherever a box's overflow status needs to feed later layout (keep the hunk small — other seams edit this file concurrently). - `crates/generation/src/overflow_runner.rs`, a `Seam::Overflow` variant, `prompt:: build_overflow`, `admit::record_for_overflow`, and the three `overflow_{ground_truth_cache,generation_loop,real_model_run}.rs` binaries, fixtures with a deliberately-wrong baseline and two seeded-wrong candidates the generation loop must reject. The model endpoint and keys are read from the environment only (`set -a; source .env; set +a`) — never print or commit any value from it. Run each binary as `cargo run --release -p generation --bin `, in the ground-truth-cache / generation-loop / real-model-run order the other seams use. Run `overflow_real_model_run` against the folknet model gateway, with `GENERA_PROVIDER` unset and `GENERA_MODELS=qwen3.8-flash-next:32:32,qwen3.8-27b-fast:32:32` set after sourcing `.env`, stopping after two flat batches or six, whichever comes first. Each entry is `model:count:concurrency[:timeout_secs]`; 32 in flight per model matches each model's four replicas at eight requests each. On the absolute seam, two mixed batches of this shape had `qwen3.8-27b-fast`'s best candidate at 82/108, tying `qwen3.8-flash-next`'s single-model best, and it alone passed `OverConstrainedRtl`, a category no `qwen3.8-flash-next` candidate passed (`docs/model-first-cost.md` → "Mixed batches"). Its replies land in 47–256 s against flash-next's 430–760 s, so its share adds candidates without lengthening the batch. A slow model may join with a small share and its own timeout, e.g. `deepseek-v4.1-flash:2:2:1800` (two requests in one wave finish inside flash-next's batch; four at two in flight would roughly double it). To record the winner's own model in provenance and print per-model counts, `overflow_real_model_run` adopts what `absolute_real_model_run` does: `score_share`, `conclude_batch`, `print_per_model_summary`, and the `model_of` lookup at admission; without that it records the joined label (`a+b`) and scores only after the last reply. On a build lane the `.env` copied there already points at the gateway (`docs/infrastructure.md` → "Reaching the model gateway"). Only if the gateway does not answer, fall back to `GENERA_PROVIDER=claude-code`, and record that fallback and what the gateway returned as a Decision. Never set `GENERA_PROVIDER=anthropic`. When a candidate is admitted, commit `generated/overflow.rs` byte-for-byte with `generated/width.rs`'s header-line convention, its provenance file, and default the relevant `fragment.rs` call site to it. Record the batch in `docs/model-first-cost.md` and update `roadmap.md` → "Proof of concept" claim 2 with the final held-out numbers Stage 2 reaches. A hand-written candidate is allowed only after every batch fails, recorded as an escalation. **Gates.** `cargo test --workspace` passes. `fragment_check --random` stays unchanged — this spec's corpus is the grammar corpus and its own new `overflow_cases.rs`, not the seeded-random recipe corpus. If Stage 2 is skipped per a Stage-1 Decision, the gate is Stage 1's alone: `held_out_check --grammar`'s new numbers are recorded and `docs/dump-format.md` accurately describes the additive fields.