This repository has no description
bez docs feature-to-function-design.md
20 kB
Markdown
at main

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 <passed> <run> 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/<feature>-triage.json (pages, pass or fail, implicated functions, unattributed pages, engine commit).
  • jobs/src/feature_rules.rs (about 100 lines): a verified evidence form <triage>.json#<key> 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/<feature>-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 <function> 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/<feature>-triage.json#<key> 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#<key> (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/<f>.json --out corpus/<f>-wpt-pages, measure with pages_ground_truth_cache --pages corpus/<set>-wpt-pages --list train|held-out --out corpus/<set>-wpt-<list>-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.