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-dataTransformFunctionvalues (lengths in px, percentages as fractions, angles in radians), andtransform_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 wrongresolve_baselinereturning the identity, which is what the engine does today.fragment.rs:Fragmentcarries the resolver's matrix, identity for a box withtransform: none, which leaves every existing fragment exactly as it is.walk_absolutecomposes 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. Keepwalk_absolute's signature, sofragment_check,held_out_checkandpaintneed no change. One more resolver argument on the threelayout*entry points.crates/harness/src/transform_cases.rs— ownCategoryenum withpub const ALL,Case,enumerated(),random(seed, count), built oncases::{Elem, Source, px, RESET}likeabsolute_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 (rotatethentranslateagainst the reverse), a non-defaulttransform-originin each unit, a transformed parent with a transformed child, a transformed box between untransformed siblings (whose positions must not move). Never add tocrates/harness/src/cases.rsRECIPES.crates/generation/src/transform_runner.rs, aSeam::Transformvariant (candidate_crate.rs),prompt::build_transform,admit::record_for_transform, andtransform_ground_truth_cache,transform_generation_loop,transform_real_model_runundercrates/generation/src/bin/, plus one baseline and two seeded-wrong fixtures undercrates/generation/fixtures/transform-candidates/(for example one that ignorestransform-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>:
transform_ground_truth_cache— fillscorpus/transform-ground-truth/from the three browsers viaoracle/oracle.mjs; record the case count and drops for engine disagreement.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.transform_real_model_runagainst the folknet model gateway, withNEV_PROVIDERunset andNEV_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:1800set after sourcing.env. Each entry ismodel: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 wayoverflow_real_model_rundoes (score_share,conclude_batch,crate::orchestrateprint_per_model_summaryandbest_per_model, and themodel_oflookup at admission), so provenance records the winner's own model. Only if the gateway does not answer, fall back toNEV_PROVIDER=claude-code, and record that fallback and what the gateway returned as a Decision. Never setNEV_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).