This repository has no description
bez tasks overflow-scrollbars.md
8.8 kB
Markdown
at main


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 <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.