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 computedoverflowvalue, 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— ownCategoryenum (at least:visiblewith overflowing content,hiddenwith overflowing content,auto/scrollwith and without overflowing content),pub const ALL,Case,enumerated(),random(seed, count), built oncases::{Elem, Source, px, RESET}. Never add tocrates/harness/src/cases.rs'sRECIPES.crates/layout/src/generated/overflow.rs+overflow.provenance.json,pub mod overflow;ingenerated/mod.rs;fragment.rswiring 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, aSeam::Overflowvariant,prompt:: build_overflow,admit::record_for_overflow, and the threeoverflow_{ground_truth_cache,generation_loop,real_model_run}.rsbinaries, 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 <name>, 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.