--- enabled: true model: claude-fable-5-1 touches: [crates/layout/src/stacking.rs, crates/layout/src/lib.rs, crates/layout/src/generated/**, crates/harness/src/stacking_cases.rs, crates/harness/src/lib.rs, crates/harness/src/trace.rs, crates/harness/src/dump.rs, crates/harness/src/bin/probe.rs, crates/dom/src/computed.rs, crates/generation/**, oracle/oracle.mjs, docs/dump-format.md, docs/platform-areas.md, docs/model-first-cost.md, roadmap.md, corpus/**] depends-on: [] --- # Stacking order: a paint-order seam with a hit-test oracle `docs/platform-areas.md` records painting as having no oracle at all, and every generated seam so far outputs geometry. Paint order (CSS 2.1 §9.9 and Appendix E: stacking contexts, `z-index`, the seven painting layers) is observable without pixels: `document.elementsFromPoint(x, y)` returns the boxes under a point in paint order, and all three engines implement it. This track adds that oracle kind and builds the first generated seam whose output is an ordering, not a rectangle, model-first the way the absolute seam was built (`tasks/absolute-batches.md`). Scope. In: block-level boxes in normal flow and `position: absolute` boxes (the two the engine lays out), `position` with `z-index: auto` and integer values, stacking contexts established by positioned boxes with non-`auto` `z-index` and by the root, the Appendix E order within one context (background of the context, negative z-index children, in-flow blocks, positioned descendants with `z-index: auto` or `0` in tree order, positive z-index children), and `opacity < 1` as a stacking-context trigger only. Out: transforms, `isolation`, `mix-blend-mode`, `will-change`, filters, floats, inline content, `pointer-events`, overflow clipping, `visibility`. Every box in the corpus is opaque, non-overflowing and untransformed, so `elementsFromPoint` reports paint order and nothing else. Say so in the seam's module doc. Oracle first, in this order: 1. `oracle/oracle.mjs`: a `stackingProbe` request that, for a document and a list of points, returns per point the ids of the elements `document.elementsFromPoint` lists, in order. Stdout keeps carrying only response JSON lines; diagnostics go to stderr. Document the request and response in `docs/dump-format.md` under a new "Stacking probe" heading, beside the trace protocol; the dump format itself is frozen and does not change. 2. `crates/harness/src/trace.rs` and `crates/harness/src/bin/probe.rs` gain the corresponding client and a three-engine vote over orderings: a point's ordering is ground truth only when all three engines agree exactly; otherwise the point is dropped and counted. `crates/harness/src/dump.rs` gets the record type for a probed point, nothing else. Then the seam, mirroring the absolute seam's set: - `crates/layout/src/stacking.rs` — `StackingInputs` (a flattened list of boxes with their id, tree order, parent, `position`, `z-index`, `opacity`, and rect), `StackingOutputs` (the ids in paint order, back to front), `type StackingResolver`, and a deliberately wrong `resolve_baseline` (for example tree order). The resolver reads the fragment tree `layout()` already produces; it does not change layout, and `crates/layout/src/fragment.rs` is not edited. - `crates/dom/src/computed.rs`: `z_index` and `opacity` accessors. - `crates/harness/src/stacking_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`; each case names the probe points (one per overlapping region). Categories at least: overlapping siblings in tree order, positioned over static, negative `z-index`, positive `z-index` ordering, `z-index` trapped inside a child stacking context, `opacity` creating a context, nested contexts three deep. Never add to `crates/harness/src/cases.rs` `RECIPES`. - `crates/generation/src/stacking_runner.rs` comparing orderings (not rects, so it does not reuse `compare::vote`), the `Seam` variant, `prompt::build_stacking_order`, `admit::record_for_stacking_order`, and `stacking_ground_truth_cache`, `stacking_generation_loop`, `stacking_real_model_run` under `crates/generation/src/bin/`, plus one baseline and two seeded-wrong fixtures under `crates/generation/fixtures/stacking-candidates/` (for example one that ignores stacking contexts and sorts globally by `z-index`, one that paints negative `z-index` above in-flow content). 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 `: 1. `stacking_ground_truth_cache` — fills `corpus/stacking-ground-truth/`; record the point count and the drops for engine disagreement. 2. `stacking_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. `GENERA_PROVIDER=claude-code stacking_real_model_run` — the loop stops on its own (two consecutive batches with no per-category improvement, or six batches). Never set `GENERA_PROVIDER=anthropic`: that path is metered and has run dry mid-batch. When a candidate is admitted, commit it as `crates/layout/src/generated/stacking.rs` byte for byte with the header line `generated/width.rs` uses, `stacking.provenance.json` beside it, and `pub mod stacking;` in `generated/mod.rs`; a `paint_order()` entry point in `crates/layout/src/lib.rs` defaults to it. Record every batch in `docs/model-first-cost.md` → "Batch scores and token totals" (wall-clock per batch in place of token counts), append the outcome to `roadmap.md` → "Phase 1 — Engine bootstrap" step 4 in the form the other seams use, and change the painting row of `docs/platform-areas.md` from no oracle to this one, with its agreement rate. 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. `cargo test --workspace` must pass. `fragment_check` on the seeded-random recipe corpus (`cargo run --release --bin fragment_check -p layout -- --random`) must still report 216/227 with every category unchanged: layout is not touched here.