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:
oracle/oracle.mjs: astackingProberequest that, for a document and a list of points, returns per point the ids of the elementsdocument.elementsFromPointlists, in order. Stdout keeps carrying only response JSON lines; diagnostics go to stderr. Document the request and response indocs/dump-format.mdunder a new "Stacking probe" heading, beside the trace protocol; the dump format itself is frozen and does not change.crates/harness/src/trace.rsandcrates/harness/src/bin/probe.rsgain 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.rsgets 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 wrongresolve_baseline(for example tree order). The resolver reads the fragment treelayout()already produces; it does not change layout, andcrates/layout/src/fragment.rsis not edited.crates/dom/src/computed.rs:z_indexandopacityaccessors.crates/harness/src/stacking_cases.rs— ownCategoryenum withpub const ALL,Case,enumerated(),random(seed, count), built oncases::{Elem, Source, px, RESET}likeabsolute_cases.rs; each case names the probe points (one per overlapping region). Categories at least: overlapping siblings in tree order, positioned over static, negativez-index, positivez-indexordering,z-indextrapped inside a child stacking context,opacitycreating a context, nested contexts three deep. Never add tocrates/harness/src/cases.rsRECIPES.crates/generation/src/stacking_runner.rscomparing orderings (not rects, so it does not reusecompare::vote), theSeamvariant,prompt::build_stacking_order,admit::record_for_stacking_order, andstacking_ground_truth_cache,stacking_generation_loop,stacking_real_model_runundercrates/generation/src/bin/, plus one baseline and two seeded-wrong fixtures undercrates/generation/fixtures/stacking-candidates/(for example one that ignores stacking contexts and sorts globally byz-index, one that paints negativez-indexabove 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 <name>:
stacking_ground_truth_cache— fillscorpus/stacking-ground-truth/; record the point count and the drops for engine disagreement.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.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 setGENERA_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.