# Feature jobs and generated functions: proposed design Status: proposal. Only the first piece (triage and triage-backed credit) is built; its results, at the end, call for rethinking the rest before any of it is built. ## The problem The pipeline from `types-prompt` through `admit` works on one generated function: one resolver, one types file, one score corpus (`generated_score_check.rs` `FUNCTIONS`), one floor, one provenance record. That part holds. What does not scale to 1000+ web features is the job being the same thing as the function: - `jobs/src/manifest.rs` `stage_violations` allows one review stage; `jobs/scripts/pi-review.sh` takes one `--types` and one `--resolver`. - `jobs/scripts/admit.py` `store_main` admits only a candidate that passes more cases than the committed function on that function's own corpus, so a job that extends a function that is already full on its corpus can never admit (`relpos` is 104 of 104). - `jobs/src/feature_rules.rs` `evidence_resolves` credits a key when its evidence file exists; no credit is tied to a measurement. - Cases reach a score corpus only through hand-wired cache binaries, so a feature's pages never become a floor for the functions that serve it. Most features will be already satisfied, a new input to an existing function, or cross-cutting (`calc()`, `writing-mode`); brand-new functions are the minority. Two jobs changing one function measure their landing bar against a function that has moved. Measured case: `physical-properties` (`top`/`left`/`right`/`bottom`). Of 200 WPT pages in `css/CSS2/positioning`, 158 are usable, all `position: relative` only; the committed `relpos` passes all of them (125 of 125 train, 32 of 32 held out), so a job could neither improve nor admit anything, and the map still shows the feature partial. ## Options - **One function per job; split features into sub-jobs.** Keeps admit as it is; the feature's credit has no single owner, every function needs a ~1000-line manifest, and sub-job order is managed by hand. - **Several functions admitted together.** Atomic, but one bad function blocks the rest and one review cannot cover several. - **Extension jobs.** Change a types file without breaking the committed function; blocked by the "passes more" rule. - **Proposed: triage, then independent function tasks, then credit.** - Triage runs the whole engine on the feature's pages before any model call. Pages that already pass are recorded and become floors. - Each failing page is attributed to a function (for example by swapping in that function's baseline resolver); each function with failures becomes one function task, which is the current pipeline. - Each task admits on its own, behind its own review and a per-function lease (`jobs/src/lease.rs`). - Score corpora accumulate: a feature's cases join every function they exercise. Admit requires no regression on the old corpus and no loss on the combined one, instead of strictly more on the old one. - Triage with nothing failing is the verification-only path: no model calls, and a credit backed by a measured record. ## Features that would test the proposal, in order 1. `physical-properties` (partial). Verification-only for `relpos`; generated cases for the 17 `absolute`/`fixed` pages the filter rejects should yield exactly one `absolute` task. Disproved if triage cannot separate `relpos` failures from `absolute` ones, or credit needs a hand edit. 2. `box-sizing` (91 pages). An input added to existing functions (`border_box` exists only in `aspect_ratio_types.rs`; width, height and absolute gain it). Disproved if the extended types file breaks the committed functions or admit cannot express the gain. 3. `min-max-width-height` (6 pages, mostly generated cases). Width and height again. Disproved if its width task and `box-sizing`'s conflict and the lease does not order them. 4. `fit-content` (32 pages). A new function (shrink-to-fit is hand-written today). Disproved if it needs other functions' types changed. 5. `calc` (82 pages). Cross-cutting over every length-percentage input. Disproved if it fans out to more than about 5 tasks or is better done by hand in the substrate, which would call for a substrate task kind. 6. `writing-mode` (1301 pages). Largest and most cross-cutting. Disproved if triage cost dominates or nearly every function must be rewritten at once. `css-sizing`, `css-values` and `css-writing-modes` are not yet in the local WPT checkout (`oracle/suites/suites.json` `wpt.sparse`). ## Changes it needs (rough sizes) - `jobs/src/manifest.rs`: a `functions` list (`module`, `resolver`, `types`, `cases`); per-function stages from a template; one review per function. About 300 lines. - `jobs/src/runner.rs`: rules 22 and 23 per function; triage and credit stages in the manner of `add_feature_map_stage`. About 400 lines. - `jobs/scripts/admit.py` `store_main`: the no-regression and combined-gain rule; append the job's cases to the function's corpus. About 150 lines. - `crates/generation/src/bin/generated_score_check.rs`: corpora from a per-function manifest in place of the `FUNCTIONS` table. About 200 lines. - `jobs/src/feature_rules.rs` `evidence_resolves`: evidence is a verification record (pages, pass counts, engine commit) checked against the floor. About 150 lines. - A triage binary: runs the engine on a feature's pages, attributes failures, writes the record. About 400 lines. - `jobs/src/lease.rs`: per-function leases. ## Review (2026-10-19): build with changes Corrections to the problem statement, checked against the code: - `admit.py` `store_main` compares only aggregate ` ` counts and writes `"regression_free": True` without a per-case check; a per-case rule needs the scorer to emit passed case ids (`Report.passed_case_ids` already carries them). - Credit is already tied to a measurement: `docs/web-features-map.md` and `web-features-map-lib.mjs` give `complete` only from a provenance record that passed every case. The gap is that the measurement is the function's own corpus, not the key's pages, and hand-written rules can never reach `complete`. - Feature pages already reach the job functions' caches (`aspect_ratio_ground_truth_cache`, `box_generation_ground_truth_cache`, through `cache_page_list`). - Only `aspect_ratio` and `box_generation` are job-able (an `*_engine.rs`, `*_types.rs`, `*_call_site.txt` and a page runner). Making `relpos` job-able took about 570 lines on the `physical-properties-job` branch. Every other function needs that first. - `border_box_sizing` is already an input of `width`, `height` and `absolute`, so `box-sizing` is likely verification-only or a substrate fix. Changes to the proposal: 1. Triage names the functions called on a failing box (`call_recording`), with an explicit unattributed bucket that routes to a substrate task. Swapping in baseline resolvers does not attribute: most baselines are the committed code or a stand-in that ignores its inputs, and compound or substrate failures come out attributed to nothing. 2. Triage writes one child manifest per function, each the unchanged pipeline, instead of per-function stages in one manifest: rework rounds, `earlier_candidate`, `land_earlier` and rule 23 assume one candidate per job. 3. Per-case scorer output before any change to the admission rule; every case keeps an exclusion-with-reason path (as `features/*.json` `exclude`), since one case browsers later disagree on would otherwise block every later admit. 4. A function-scoped lock held from `types-prompt` to `admit`, as a new mechanism: `jobs/src/lease.rs` is a per-model gateway semaphore tied to one process and stage. A task whose function was admitted by another task first reruns from `types-prompt`. 5. Credit per BCD key needs a key-to-pages mapping (a property and value scan of the page source); WPT pages map to features through `WEB_FEATURES.yml`, not to keys. The record names the engine commit and is rechecked after landings (`bench/after-land.sh`). 6. A substrate task kind (hand-written `fragment.rs` and the like), a function split or merge path, types-file versioning, and generated cases as triage input for a feature with no usable WPT pages. Order: `physical-properties` first; `box-sizing` after a cheap triage; keep `fit-content` as the new-function test; defer `calc` and `writing-mode` until a substrate task kind exists. Smallest first piece, about 450 lines and no runner changes: - `crates/generation/src/bin/feature_triage.rs` (about 250 lines): a feature file's measured pages through `page_scoring` with `Resolvers::generated()`; for each failing page, the functions called on its failing boxes (`call_recording`); writes `corpus/-triage.json` (pages, pass or fail, implicated functions, unattributed pages, engine commit). - `jobs/src/feature_rules.rs` (about 100 lines): a `verified` evidence form `.json#` that resolves only when every page tagged for the key passes, mirrored in `web-features-map-lib.mjs` `keyEntry`. - Run by hand on `physical-properties`, `box-sizing` and `fit-content`. It holds if `relpos` is verified with no hand edit and the 17 `absolute`/`fixed` pages implicate `absolute`. If `box-sizing` and `fit-content` come out mostly unattributed, the substrate is the main path and the larger plan is rethought. ## The first piece, built and run (2026-10-19) What was built: - `crates/generation/src/bin/feature_triage.rs`: lays every measured page of a feature out with every generated function as landed, scores it on its boxes (`page_scoring::page_outcome`), and writes `corpus/-triage.json` (tracked; `.gitignore` keeps every other file under `corpus/` out). A failing box implicates a function the engine asked about that box when the function decides one of the measurements the box fails (`decides`). Only the outermost failing boxes attribute a page. A measurement some function was handed an unrepresentable value for (the engine's `Unresolved`: `fit-content`, `max-content`, `calc()` mixing a length and a percentage) implicates nothing: the value was lost in hand-written code first. `--baseline ` swaps one function for its stand-in, to check the attribution on pages that otherwise all pass. - `crates/layout/src/generated_calls.rs`: the recording wrappers. Layout enters a box scope (`call_recording::BoxScope`) for each box it lays out and for each child it places, so a call is tied to a box although no function's inputs carry a node. Layout output is unchanged (`layout_with_the_recording_functions_is_layout`). - Triage-backed credit: evidence `corpus/-triage.json#` resolves, and makes the key `complete` whatever the rule's status, only when the record tags at least one page for the key and every tagged page passed (`jobs/src/feature_rules.rs` `triage_verifies`, `oracle/suites/feature-map-rules.mjs` `triageVerifies`, `web-features-map-lib.mjs` `keyEntry`; `docs/web-features-map.md` → "The status rule"). A page is tagged for a key by a scan of its source for the property, and the value when the key names one. Pages. `css/CSS2/positioning`, `css/css-sizing` and `css/css-ui` are in `wpt.sparse`. | Feature | Scanned | Usable | Measured | Pass | Fail | Unattributed | |---|---|---|---|---|---|---| | `physical-properties` (feature file as the job scopes it) | 200 | 158 | 157 | 157 | 0 | 0 | | its 17 `absolute`/`fixed` pages | 17 | 13 | 13 | 13 | 0 | 0 | | `box-sizing` (`css-sizing` and `css-ui`) | 48 | 8 | 8 | 3 | 5 | 3 | | `fit-content` | 16 | 12 | 11 | 1 | 10 | 10 | The `absolute`/`fixed` pages were selected with the feature file minus its `position: absolute|fixed` pattern and `pages` narrowed to the 17 (`corpus/physical-properties-absolute-triage.json`). `box-sizing`'s `css-ui` pages (`box-sizing-*.html`) were selected the same way, with `source` `css/css-ui/`; one feature file has one source folder. Most of `box-sizing`'s 91 pages are rejected before triage sees them: of 48 scanned, 11 hold text, 26 an image, canvas or svg, 3 a script. Results against the criteria: 1. `relpos` is verified with no hand edit: every one of the feature's eight keys is verified by `corpus/physical-properties-triage.json` (`top` 30 pages, `right` 42, `bottom` 42, `left` 43, each `.auto` key 1), and a rule pointing at it makes the key `complete`. Crediting still takes a rule per key: today's `top`/`right`/`bottom`/`left` rules point at `generated/absolute.rs`. 2. The 17 `absolute`/`fixed` pages do not implicate `absolute`, because they are not `absolute` pages: the 13 usable ones pass, and in each the absolutely positioned box is a backdrop with no offset while `top` moves a relatively positioned sibling. Triage does separate the two functions on them: with `relpos` swapped for its stand-in, 58 of the 170 pages fail, every one names `relpos` and none names `absolute`; with `absolute` swapped, none fails. The proposal's "exactly one `absolute` task" is zero tasks; the feature is verification-only. 3. `box-sizing` and `fit-content` come out mostly unattributed: 13 of their 15 failing pages. All 10 `fit-content` failures are `block-size: fit-content` (8) or `height: fit-content` (2) on an absolutely positioned box, which reaches `absolute` as `Unresolved`; 3 `box-sizing` failures are `width: max-content`, reaching `width` the same way. The 2 attributed pages (`box-sizing-003.html`, `-005.html`) are `absolute` getting a `border-box` box with an `auto` margin wrong, and name `containing_block` beside it. Finding: the features after the first land mostly in hand-written layout code, the values the engine cannot represent before any generated function is asked. The larger plan (per-function child manifests, accumulated score corpora, a function lock) is not started; a substrate task kind comes first, and the plan is rethought around it. Limits of the attribution, seen in these runs: - It names every function that could decide a failing measurement, not only the one at fault: with `relpos` swapped out, `width` is named beside it on 29 pages (it decides the left margin, so it decides `offset_x`), `percentage` on 10, `margin_combine` on 4. - It cannot see a property the engine never reads. In 4 `fit-content` pages a child of the failing box also fails, because the engine ignores `writing-mode`; it names `width` and `height`, and is outvoted only because its parent fails too. - Evidence volume is set by the page filter, not by triage: `box-sizing.content-box` and `width.fit-content` tag no page (8 `fit-content` pages set `block-size`, which the key scan does not map to `height`, and none sets `width: fit-content`), and each `.auto` key of `physical-properties` rests on one page. Left open: - No rule in `docs/feature-map.json` points at a triage record yet. Crediting `physical-properties` means pointing its eight keys' rules at `corpus/physical-properties-triage.json#` (today `top`/`right`/`bottom`/`left` point at `generated/absolute.rs`), a change to what the map credits. - The status rule now admits triage-backed `complete`, but the map snapshot is byte-identical while no rule uses the form, so its `schemaVersion` stays 1; it is bumped with the first rule that points at a triage record (`docs/web-features-map.md`). Reproducing the runs. Page sets and measured caches are under `corpus/` (gitignored; only `corpus/*-triage.json` is tracked). Select with `node oracle/suites/feature-pages.mjs --feature oracle/suites/features/.json --out corpus/-wpt-pages`, measure with `pages_ground_truth_cache --pages corpus/-wpt-pages --list train|held-out --out corpus/-wpt--ground-truth`, then `feature_triage --feature --cases ... [--out ] [--baseline ]`. Each record lists its `cases`; its `engine_commit` is the commit the binary was built from, so a rebase calls for a rerun. ## Intrinsic sizes in hand-written layout (2026-10-20) Of the 15 failing `box-sizing` and `fit-content` pages, 13 lost their value in hand-written code: `min-content`, `max-content` and `fit-content` reached generated functions as `Unresolved`. What changed, all in hand-written code: - `dom::computed::IntrinsicKeyword` and `ElementStyle::width_keyword`, `height_keyword`, `min_width_keyword`, `max_width_keyword` read the keyword. `size_to_length` still returns `Unresolved` for it, so `LengthPercentageOrAuto` and every generated function's signature stay as landed. - `fragment.rs` `content_width` computes a box's min-content and max-content content width from its in-flow children and floats: blocks stack (the widest margin box wins); at max-content a run of consecutive floats adds up and a block or a float with `clear` ends the run; at min-content each float counts alone. `intrinsic_border_box_width_of` applies the box's own `width`, `min-width`, `max-width` and `box-sizing`. - `layout_box_styled` turns a keyword `width`, `min-width` or `max-width` into a length before the width function is called. `fit-content` is `min(max-content, max(min-content, available))` (CSS Sizing 3 section 5.1). A `width: auto` float uses the same rule, where it used to take max-content alone. A keyword `height` on a box in normal flow is `auto`. - `layout_absolute_box` resolves a keyword `width` the same way, and gives `width: auto` with `left` or `right` auto the shrink-to-fit width (CSS 2.1 10.3.7). A keyword `height` is laid out as `auto`, and the absolute function is then handed the content height as a length. - A text run's contribution is not computed: the box tree holds no text in this build. Text arrives with the `text-width` track; its min-content (longest word) and max-content (whole line) contributions go into `content_width` in whichever of the two tracks lands second. Decision: the intrinsic-size computation stays hand-written, not a generated function. `tasks/min-max-content.md` proposed lifting `shrink_to_fit_content_width` and `intrinsic_border_box_width` into one generated function and left that rule open. It stays hand-written because the computation is a walk over the box tree, which a generated function's plain-data signature cannot see without a new input type, and because the three browsers agree on every page measured, so there is no disagreement a function would settle. That file's scope (floats at min- and max-content, keyword widths, `height` keywords as `auto`) is covered here; `tasks/min-max-content.md` is superseded by this change. Result (`feature_triage` rerun): | Feature | Pass before | Pass after | Unattributed before | Unattributed after | |---|---|---|---|---| | `box-sizing` (8 pages) | 3 | 5 | 3 | 0 | | `fit-content` (11 pages) | 1 | 4 | 10 | 0 | What still fails is now attributed. The four `orthogonal` `fit-content` pages set `writing-mode`, which the engine ignores. Three more (`div-fit-content-block-size`, `fit-content-block-size-abspos`, `-fixedpos`) are off in `offset_y` only and implicate `absolute`, `containing_block` and `percentage`: a box with `top`, `bottom`, a definite height and `margin: auto` with negative free space, a case for the absolute function. In `box-sizing`, `box-sizing-003` and `-005` implicate `absolute` and `containing_block`, and `border-box-and-max-content-002` implicates `width`.