This repository has no description
bez tasks transforms2d.md
11 kB
Markdown
at main


enabled: false touches: [crates/layout/src/transform.rs, crates/layout/src/fragment.rs, crates/layout/src/lib.rs, crates/layout/src/generated/transform.rs, crates/layout/src/generated/transform.provenance.json, crates/layout/src/generated/mod.rs, crates/dom/src/computed.rs, crates/harness/src/transform_cases.rs, crates/harness/src/lib.rs, crates/generation/, docs/model-first-cost.md, roadmap.md, corpus/transform-ground-truth/, crates/provenance/src/transform.rs, crates/provenance/src/lib.rs, docs/generated-functions.md] depends-on: [fixed-positioning] #

transforms2d: 2D transform and transform-origin as a generated function #

transform is ignored. crates/dom/src/computed.rs has no accessor for it, and crates/layout/src/fragment.rs Fragment::walk_absolute reports every box's border box where normal flow put it. The browsers do not: getBoundingClientRect() and getClientRects() (what oracle/oracle.mjs domDump() records) return the bounding box of the transformed border box, for the element and for every descendant. The Web Features map (docs/web-features-map.json, feature transforms2d) lists it as none with 887 WPT test files under css/css-transforms, the largest layout-and-rendering feature after flexbox and grid. A transform never moves other boxes (CSS Transforms 1, "The Transform Rendering Model": it does not affect the flow), so the whole feature is one function from a box's transform list, origin and size to a matrix, plus hand-written bookkeeping that carries matrices down the tree.

The provisional rule for features whose math an existing generated function already covers (lift the adjacent hand-written logic into a new generated function; the roster files it as an open decision) does not come into play: no existing generated function computes a matrix.

Scope. In: transform with the 2D functions translate(), translateX(), translateY() (lengths and percentages of the border box), scale(), scaleX(), scaleY(), rotate(), skew(), skewX(), skewY() and matrix(), in lists of any length (composed left to right); transform-origin in lengths, percentages and keywords (two values; the third, z, stays 0); transforms on nested boxes, where a descendant's bounding box goes through every ancestor's matrix. The reference box is the border box (transform-box initial value). Out: 3D functions and perspective; transform-box; translate, rotate and scale as individual properties (CSS Transforms 2); a transformed box becoming the containing block for positioned descendants and establishing a stacking context (both real, but they change other generated functions' inputs, so the corpus has no positioned or z-index descendants of a transformed box); painting transformed boxes in crates/layout/src/bin/paint.rs; scrollable overflow from transforms. Say so in the new module's doc.

Build the same set the absolute-positioning function has:

  • crates/dom/src/computed.rs: transform() returning a list of plain-data TransformFunction values (lengths in px, percentages as fractions, angles in radians), and transform_origin(). Accessors only.
  • crates/layout/src/transform.rs — TransformInputs (the border-box width and height, the function list, the origin as length-or-percentage per axis), TransformOutputs (the affine matrix [a, b, c, d, e, f] taking a point in the box's own border-box coordinates to its parent's coordinates, relative to where normal flow put the box's border-box origin, with the origin already applied: translate by the origin, apply the list, translate back, as CSS Transforms 1 "The Transform Rendering Model" computes the transformation matrix), type TransformResolver, and a deliberately wrong resolve_baseline returning the identity, which is what the engine does today.
  • fragment.rs: Fragment carries the resolver's matrix, identity for a box with transform: none, which leaves every existing fragment exactly as it is. walk_absolute composes matrices from the root down and reports each box's bounding box of its transformed border box. That is hand-written: the four corners through the composed matrix, then min and max. Keep walk_absolute's signature, so fragment_check, held_out_check and paint need no change. One more resolver argument on the three layout* entry points.
  • crates/harness/src/transform_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. Categories at least: translate in px, translate in percentages, scale, rotate by a right angle, rotate by an arbitrary angle, skew, matrix(), a list whose order matters (rotate then translate against the reverse), a non-default transform-origin in each unit, a transformed parent with a transformed child, a transformed box between untransformed siblings (whose positions must not move). Never add to crates/harness/src/cases.rs RECIPES.
  • crates/generation/src/transform_runner.rs, a Seam::Transform variant (candidate_crate.rs), prompt::build_transform, admit::record_for_transform, and transform_ground_truth_cache, transform_generation_loop, transform_real_model_run under crates/generation/src/bin/, plus one baseline and two seeded-wrong fixtures under crates/generation/fixtures/transform-candidates/ (for example one that ignores transform-origin, one that composes the list right to left).

Rotations produce values off every browser's grid. docs/geometry-verification.md explains why the vote tolerance is 1/60 px, and Firefox rounds rects onto its own 1/60 px grid. Record the ground-truth cache's per-category drops for engine disagreement. If arbitrary-angle rotation drops most of its cases, say so and keep only the angles the engines agree on, rather than loosening the tolerance.

Shared files. crates/layout/src/fragment.rs, crates/layout/src/lib.rs and crates/generation/** are edited by every spec that adds a generated function; such specs have landed concurrently before, so keep each hunk small and additive. Fragment gains one field here; every other spec that builds fragments passes through the constructor this spec updates. crates/dom/src/computed.rs, crates/harness/src/lib.rs, crates/layout/src/generated/mod.rs, roadmap.md and docs/model-first-cost.md take additions only.

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

  1. transform_ground_truth_cache — fills corpus/transform-ground-truth/ from the three browsers via oracle/oracle.mjs; record the case count and drops for engine disagreement.
  2. transform_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. transform_real_model_run against the folknet model gateway, with NEV_PROVIDER unset and NEV_MODELS=qwen3.8-flash-next:16:16,qwen3.8-27b-fast:32:32,qwen3.8-27b-fast-2:32:16,deepseek-v4.1-flash:2:2:1800 set after sourcing .env. Each entry is model:count:concurrency[:timeout_secs], and the concurrencies match what each alias serves (docs/infrastructure.md → "Rig change log"). It stops on its own after two flat batches or six, whichever comes first. Score each model's share as it arrives and print per-model counts the way overflow_real_model_run does (score_share, conclude_batch, crate::orchestrate print_per_model_summary and best_per_model, and the model_of lookup at admission), so provenance records the winner's own model. Only if the gateway does not answer, fall back to NEV_PROVIDER=claude-code, and record that fallback and what the gateway returned as a Decision. Never set NEV_PROVIDER=anthropic.

When a candidate is admitted, commit it as crates/layout/src/generated/transform.rs byte for byte with the header line generated/width.rs uses, transform.provenance.json beside it, pub mod transform; in generated/mod.rs, and the three layout* entry points defaulting to it. Record every batch in docs/model-first-cost.md → "Batch scores and token totals", with per-model counts, and append the outcome to roadmap.md → "Phase 1 — Engine bootstrap" step 4 in the form the other generated functions use. 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.

A floor for this function in bench/seam-floor.sh is a hand change after landing: the landing script refuses a diff under bench/, and a score line in seam_floor_check with no cache rebuild behind it would fail every later landing. Say so in the track's handoff.

Files beyond the function's own. The spec text the function is generated against goes in a new provenance module whose file name contains transform (for example crates/provenance/src/css_transforms_transform.rs), so the touches glob crates/provenance/src/*transform*.rs matches it, with its one pub mod line in crates/provenance/src/lib.rs, the way css21_containing_block.rs was added. The function's row in the table and its "Per function" entry go in docs/generated-functions.md. Do not update docs/feature-map.json, docs/dashboard.json or README.md here: docs/dashboard.md asks every spec that lands a generated function to, but those files belong to tasks/dashboard-history-graph.md, and that instruction does not apply to an unattended track; the landing script refuses any file outside touches. In docs/*.md and roadmap.md prose, write "generated function", never the older word the root AGENTS.md forbids; crates/harness/tests/no_seam_word.rs fails cargo test --workspace otherwise (code identifiers and paths in backticks are exempt). The generated-function specs run one after another through depends-on, since each adds a resolver argument to the same layout* entry points and edits the same shared files.

Gates. cargo test --workspace passes. fragment_check --random stays 227/227 with every category unchanged: no recipe in cases.rs sets transform, so every matrix there is the identity. bench/seam-floor.sh's floors hold (floats 109, stacking 105, relative positioning 104, block height 105).