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.rsstage_violationsallows one review stage;jobs/scripts/pi-review.shtakes one--typesand one--resolver.jobs/scripts/admit.pystore_mainadmits 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 (relposis 104 of 104).jobs/src/feature_rules.rsevidence_resolvescredits 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 #
physical-properties(partial). Verification-only forrelpos; generated cases for the 17absolute/fixedpages the filter rejects should yield exactly oneabsolutetask. Disproved if triage cannot separaterelposfailures fromabsoluteones, or credit needs a hand edit.box-sizing(91 pages). An input added to existing functions (border_boxexists only inaspect_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.min-max-width-height(6 pages, mostly generated cases). Width and height again. Disproved if its width task andbox-sizing's conflict and the lease does not order them.fit-content(32 pages). A new function (shrink-to-fit is hand-written today). Disproved if it needs other functions' types changed.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.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: afunctionslist (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 ofadd_feature_map_stage. About 400 lines.jobs/scripts/admit.pystore_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 theFUNCTIONStable. About 200 lines.jobs/src/feature_rules.rsevidence_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.pystore_maincompares only aggregate<passed> <run>counts and writes"regression_free": Truewithout a per-case check; a per-case rule needs the scorer to emit passed case ids (Report.passed_case_idsalready carries them).- Credit is already tied to a measurement:
docs/web-features-map.mdandweb-features-map-lib.mjsgivecompleteonly 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 reachcomplete. - Feature pages already reach the job functions' caches (
aspect_ratio_ground_truth_cache,box_generation_ground_truth_cache, throughcache_page_list). - Only
aspect_ratioandbox_generationare job-able (an*_engine.rs,*_types.rs,*_call_site.txtand a page runner). Makingrelposjob-able took about 570 lines on thephysical-properties-jobbranch. Every other function needs that first. border_box_sizingis already an input ofwidth,heightandabsolute, sobox-sizingis likely verification-only or a substrate fix.
Changes to the proposal:
- 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. - Triage writes one child manifest per function, each the unchanged pipeline, instead of
per-function stages in one manifest: rework rounds,
earlier_candidate,land_earlierand rule 23 assume one candidate per job. - Per-case scorer output before any change to the admission rule; every case keeps an
exclusion-with-reason path (as
features/*.jsonexclude), since one case browsers later disagree on would otherwise block every later admit. - A function-scoped lock held from
types-prompttoadmit, as a new mechanism:jobs/src/lease.rsis a per-model gateway semaphore tied to one process and stage. A task whose function was admitted by another task first reruns fromtypes-prompt. - 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). - A substrate task kind (hand-written
fragment.rsand 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 throughpage_scoringwithResolvers::generated(); for each failing page, the functions called on its failing boxes (call_recording); writescorpus/<feature>-triage.json(pages, pass or fail, implicated functions, unattributed pages, engine commit).jobs/src/feature_rules.rs(about 100 lines): averifiedevidence form<triage>.json#<key>that resolves only when every page tagged for the key passes, mirrored inweb-features-map-lib.mjskeyEntry.- Run by hand on
physical-properties,box-sizingandfit-content. It holds ifrelposis verified with no hand edit and the 17absolute/fixedpages implicateabsolute. Ifbox-sizingandfit-contentcome 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 writescorpus/<feature>-triage.json(tracked;.gitignorekeeps every other file undercorpus/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'sUnresolved: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 keycompletewhatever 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.rstriage_verifies,oracle/suites/feature-map-rules.mjstriageVerifies,web-features-map-lib.mjskeyEntry;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:
relposis verified with no hand edit: every one of the feature's eight keys is verified bycorpus/physical-properties-triage.json(top30 pages,right42,bottom42,left43, each.autokey 1), and a rule pointing at it makes the keycomplete. Crediting still takes a rule per key: today'stop/right/bottom/leftrules point atgenerated/absolute.rs.- The 17
absolute/fixedpages do not implicateabsolute, because they are notabsolutepages: the 13 usable ones pass, and in each the absolutely positioned box is a backdrop with no offset whiletopmoves a relatively positioned sibling. Triage does separate the two functions on them: withrelposswapped for its stand-in, 58 of the 170 pages fail, every one namesrelposand none namesabsolute; withabsoluteswapped, none fails. The proposal's "exactly oneabsolutetask" is zero tasks; the feature is verification-only. box-sizingandfit-contentcome out mostly unattributed: 13 of their 15 failing pages. All 10fit-contentfailures areblock-size: fit-content(8) orheight: fit-content(2) on an absolutely positioned box, which reachesabsoluteasUnresolved; 3box-sizingfailures arewidth: max-content, reachingwidththe same way. The 2 attributed pages (box-sizing-003.html,-005.html) areabsolutegetting aborder-boxbox with anautomargin wrong, and namecontaining_blockbeside 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
relposswapped out,widthis named beside it on 29 pages (it decides the left margin, so it decidesoffset_x),percentageon 10,margin_combineon 4. - It cannot see a property the engine never reads. In 4
fit-contentpages a child of the failing box also fails, because the engine ignoreswriting-mode; it nameswidthandheight, and is outvoted only because its parent fails too. - Evidence volume is set by the page filter, not by triage:
box-sizing.content-boxandwidth.fit-contenttag no page (8fit-contentpages setblock-size, which the key scan does not map toheight, and none setswidth: fit-content), and each.autokey ofphysical-propertiesrests on one page.
Left open:
- No rule in
docs/feature-map.jsonpoints at a triage record yet. Creditingphysical-propertiesmeans pointing its eight keys' rules atcorpus/physical-properties-triage.json#<key>(todaytop/right/bottom/leftpoint atgenerated/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 itsschemaVersionstays 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
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::IntrinsicKeywordandElementStyle::width_keyword,height_keyword,min_width_keyword,max_width_keywordread the keyword.size_to_lengthstill returnsUnresolvedfor it, soLengthPercentageOrAutoand every generated function's signature stay as landed.fragment.rscontent_widthcomputes 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 withclearends the run; at min-content each float counts alone.intrinsic_border_box_width_ofapplies the box's ownwidth,min-width,max-widthandbox-sizing.layout_box_styledturns a keywordwidth,min-widthormax-widthinto a length before the width function is called.fit-contentismin(max-content, max(min-content, available))(CSS Sizing 3 section 5.1). Awidth: autofloat uses the same rule, where it used to take max-content alone. A keywordheighton a box in normal flow isauto.layout_absolute_boxresolves a keywordwidththe same way, and giveswidth: autowithleftorrightauto the shrink-to-fit width (CSS 2.1 10.3.7). A keywordheightis laid out asauto, 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-widthtrack; its min-content (longest word) and max-content (whole line) contributions go intocontent_widthin 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.