The web-features map #
One committed, versioned file, docs/web-features-map.json, says for every feature in
the web-features package and every browser-compat-data key it groups how much of it
this engine implements, and the evidence behind each status. A second committed file,
docs/web-features-events.jsonl, records what changed at each regeneration as JSON
lines. Both are data only: a widget, a dashboard or a bot reads them; nothing here
renders or announces anything.
Design: what already existed, and what was missing #
crates/wpt-votes(docs/wpt-votes.md).features.rsreads WPT's ownWEB_FEATURES.ymlfiles at a pinned WPT revision (WebFeaturesTree, a blobless clone undercorpus/wpt-votes/wpt-repo) and theweb-featurespackage'scompat_features(WebFeatures, fetched by URL at version 3.39.0).bin/join_features.rsrolls the wpt.fyi vote table up per browser-compat-data key (subtests, majority, frontier). What it provides is the WPT-to-feature join and the browsers' results; what it does not have is any result for this engine, which does not run WPT yet (docs/platform-areas.md-> "Tooling and embedding"). The vote table also needs about 2.4 GB of downloads and is built from whatever wpt.fyi reports as current, so it cannot be an input that regenerates on every merge.crates/content-analyser/src/bcd.rswalks browser-compat-data'shtmlandcsstrees for content analysis. It measures what pages use, not what the engine implements, so it is not an input here.oracle/suites/dashboard.mjswithdocs/feature-map.json(docs/dashboard.md) is the one place that records what the engine implements: an ordered rule map from browser-compat-data key patterns to generated / hand-written / linked / oracle only, each with an evidence pointer that must resolve. It counts leaf keys per area; it has no notion of aweb-featuresfeature, of "complete", or of history.oracle/suites/entry-coverage.mjsmeasures how much of an area's browser-compat-data a conformance suite could check (tests that mention an entry). It is about oracle reach, not implementation, and needs suite result files that are not committed, so it is not an input either.- Generated functions carry a committed
.provenance.jsonnext to their source with a verification record (cases_run,cases_passed,regression_free). That is the only machine-readable pass record for anything the engine implements today. docs/platform-areas.mdnames the areas; nothing mapped keys to them.specs/dashboard-history-graph.md(disabled) adds history for the dashboard'soverallpercentages. This map keeps its own history as events rather than percentages and leavesdocs/dashboard.jsonand the README untouched, so the two do not collide.
Missing, and built here: a per-feature and per-key status with a written rule for
complete; WPT evidence per feature that is reproducible from a pinned revision alone;
an area per feature; pinned package versions in a lockfile; a diff that emits events;
and regeneration on every merge.
Inputs, all pinned or committed #
| Input | Where it is pinned | Recorded in the snapshot as |
|---|---|---|
web-features 3.39.0 |
oracle/package.json (exact) and oracle/package-lock.json |
inputs.webFeatures |
@mdn/browser-compat-data 8.0.4 |
same | inputs.browserCompatData |
WPT revision bc93a431dd... |
docs/web-features-wpt.json |
inputs.wptRevision |
| The rule map | docs/feature-map.json (shared with the dashboard) |
per key: via, rule, evidence |
| Generated-function verification | crates/layout/src/generated/*.provenance.json |
per key: seam |
| Key-to-area rules | docs/web-features-areas.json |
per feature: area |
| Engine commit | the checkout the generator ran in (git rev-parse HEAD) |
inputs.engineCommit |
The web-features version matches WEB_FEATURES_PACKAGE_VERSION in
crates/wpt-votes/src/bin/join_features.rs; the browser-compat-data version is the one
the dashboard already used. Bumping either is a change to oracle/package.json and a
regeneration, and shows up as an input event.
WPT evidence. cargo run --release -p wpt-votes --bin feature_wpt_rollup <revision>
writes docs/web-features-wpt.json: per web-features id, how many WPT test files
WPT's WEB_FEATURES.yml assigns it, and the directories whose WEB_FEATURES.yml made
the assignment. It needs only the WPT tree at that revision (a blobless fetch, no file
contents beyond the rule files), so it reproduces exactly. "Test file" is
wpt_votes::testfiles::is_test_file, an approximation of WPT's manifest rules (markup
tests and .any.js / .window.js / .worker.js wrappers, outside support and reference
material); it counts files, not subtests. At bc93a431dd: 66,682 test files, 49,496 of
them under a web-features id, 858 ids. The engine's own WPT results would go in
wpt.enginePassed, which is null for every feature because no such run exists yet.
Areas. docs/web-features-areas.json is an ordered list of
{ "pattern", "area" } rules, matched like docs/feature-map.json's patterns (first
match wins; a pattern matches its key and everything under it; a segment may be * or
end in *). Every area must be a heading in docs/platform-areas.md; the generator
refuses to run otherwise. A feature's area is the one most of its keys map to (ties
broken by name), so a permission name or an allow token a feature also lists does not
move it; unmapped when none of its keys map. The rules are coarse by design: 66
features stay unmapped in the first snapshot (the Navigator grab bag, on-device AI APIs, and similar
that have no row in docs/platform-areas.md).
The status rule #
Computed only from the committed inputs above. Ambiguity resolves toward partial or
unknown, never complete.
Per browser-compat-data key, from the first docs/feature-map.json rule that matches it
(oracle/suites/web-features-map-lib.mjs keyEntry):
- none — no rule matches, or the rule is
oracle only: nothing claims an implementation. - unknown — an implementing rule (generated, hand-written, linked) matches only
because its pattern is a parent of the key or a wildcard. The parent is claimed; this
refinement of it (
css.properties.width.fit-contentunder acss.properties.widthrule) is not individually evidenced. - partial — an implementing rule names the key exactly, but no committed pass
record shows every case passing. Hand-written and linked rules carry no pass record
at all, so they are always
partial; a generated function whose own verification passed fewer cases than it ran ispartialtoo. - complete — a
generatedrule names the key exactly, its evidence is a.rsfile with a.provenance.jsonbeside it, and that record ran at least one case, passed every case it ran, and was regression-free.
Per feature (featureStatus), from the counts of its keys' statuses:
- complete — it lists at least one key and every key is complete.
- partial — otherwise, at least one key is complete or partial.
- none — every key is none.
- unknown — anything else: it lists no keys (21 features in 3.39.0), or its only
claimed keys are
unknownones. Flexbox is the example:display.flexsits under the hand-writtendisplayrule, which is not evidence that flexbox is started.
moved and split entries in web-features are redirects, not features, and are left
out. A key web-features lists that the pinned browser-compat-data does not have (141
in 3.39.0 against 8.0.4) is kept and marked inBcd: false.
Consequence today: no key is complete, because every generated function whose verification passed
every case (height, overflow, stacking, float, relpos) is not what docs/feature-map.json
names for its keys, and the ones it does name (width, margin, absolute, percentage)
passed fewer cases than they ran. The first rule that points an exact key at a fully
passing generated function makes that key complete; a feature follows once all its keys do.
Schema, version 1 #
docs/web-features-map.json, top-level fields in this order:
schemaVersion—1. Any change to a field's meaning or to the status rule bumps it.statusRule—docs/web-features-map.md#The status rule.inputs—webFeatures,browserCompatData,wptRevision,wptTestFileRule,engineCommit.totals:webShare— the one number:{ complete, measurable, percent }, complete features out of measurable ones (every feature notunknown), percent to one decimal.features—{ total, complete, partial, none, unknown }.keys—{ total, complete, partial, unknown, none }, over the distinct keys all features list.byBaseline—high,low,false(theweb-featuresBaseline status), each a feature breakdown as above.byArea— perdocs/platform-areas.mdarea (andunmapped), a feature breakdown.keysByDashboardStatus— the keys counted by the dashboard's own five classes, so the two views can be reconciled.
features— one line per feature id, sorted:{ name, group[], baseline, status, counts: { keys, complete, partial, unknown, none }, area, wpt: { testFiles, directories[], enginePassed }, keys[] }.keys— one line per browser-compat-data key, sorted:{ status, via, rule?, evidence?, seam?: { provenance, casesRun, casesPassed, regressionFree }, inBcd? }.viais the dashboard's classification of the key (unreachedwhen no rule matches), computed by the same code (oracle/suites/feature-map-rules.mjs), so a key'sviahere and its class indocs/dashboard.jsonnever disagree.
One line per feature and per key makes every status change a one-line diff. The file is about 2.3 MB.
Where consumers read it #
- A dashboard or widget reads
totals.webSharefor the headline,totals.featuresandtotals.byAreafor the breakdown, andfeatures.<id>for a per-feature row. The README's coverage table (oracle/suites/dashboard.mjs) still counts browser-compat-data leaf keys over the whole of browser-compat-data rather than per feature; its classification code is the same module the map uses, andtotals.keysByDashboardStatusis the bridge between the two. Rendering a line fromtotals.webShareinto the README is a change todashboard.mjs's README block, whichspecs/dashboard-history-graph.mdalso edits, so it is left for after that spec. - A bot reads
docs/web-features-events.jsonl, below. - The landing announcement reads the events each landing appends and posts to the community Discord when a feature moves forward ("Announcements", below).
Events #
node oracle/suites/web-features-diff.mjs BEFORE AFTER prints one JSON object per line.
BEFORE may be empty or missing (no earlier snapshot: every feature is reported as new).
Every event has format (1), type, and at (the engineCommit of AFTER). Order:
input changes, then totals, then features by id.
type |
Fields | When |
|---|---|---|
input |
input, from, to |
schemaVersion, webFeatures, browserCompatData, wptRevision or wptTestFileRule differs |
totals |
from, to: each { webShare, features, keys } |
any of those moved |
feature-status |
feature, name, from, to, counts |
a feature's status changed; from is null for a new feature, to and counts are null for a removed one |
feature-counts |
feature, name, status, from, to |
its key counts moved but its status did not |
A feature becoming complete is feature-status with to: "complete"; becoming partial
is to: "partial"; losing completeness is from: "complete". Fixtures that exercise
every type: oracle/suites/fixtures/web-features-map/ (before.json, after.json,
expected-events.jsonl, rebuilt by make-fixtures.mjs).
docs/web-features-events.jsonl is the committed log: every regeneration that changed
the snapshot appends its events. It starts with the events from no snapshot to the first
one, so replaying it from the top reconstructs every feature's status.
Example: a bot that keeps a running count and announces newly complete features.
import { readFileSync, writeFileSync } from 'node:fs';
const log = readFileSync('docs/web-features-events.jsonl', 'utf8').trim().split('\n');
const state = JSON.parse(readFileSync('bot-state.json', 'utf8')); // { seen: 0, complete: 0 }
for (const line of log.slice(state.seen)) {
const e = JSON.parse(line);
if (e.type !== 'feature-status') continue;
if (e.to === 'complete') { state.complete++; announce(`${e.name} is complete (${state.complete} features so far, at ${e.at.slice(0, 10)})`); }
if (e.from === 'complete') state.complete--;
}
state.seen = log.length;
writeFileSync('bot-state.json', JSON.stringify(state));
It remembers how many lines it has read, so each run handles only the lines appended
since; the count comes from the events alone and agrees with totals.features.complete
in the snapshot at the same commit.
Keeping it current #
One entry point: bench/web-features-update.sh [--no-commit], run in the checkout to
update. It regenerates the snapshot; if the content changed (anything except
engineCommit), it appends the diff's events to the log and commits exactly those two
files as web-features map: regenerate at <sha> (<n> events). Unchanged content writes
and commits nothing, so a merge that touches no input adds no commit. On any failure it
puts both files back and exits 1 with the reason on stderr.
- Unattended landings.
bench/land.shruns it in the primary checkout after every merge, after the roster step, throughbench/after-land.sh(MAP_ON_LAND, infinish_landing), which runsbench/dashboard-update.sh(the README's coverage table,docs/dashboard.md) before it and the announcement below after it. It is tried once; a failure of either update is named on the one output line (owed: web-features map or dashboard not updated), never undoes the merge and does not change the exit status. While the primary is dirty or off base it is owed like the roster step and a rerun tries it.bench/land-test.shw1covers the stand-in being called once per landing and never under--check, the real script committing a regenerated snapshot and its events after a merge that changesdocs/feature-map.json, and a failing regeneration leaving the merge and a clean primary. - Hand landings.
tracks.jsondeclaresbench/after-land.shas itsafterLandcommand, so the/landcommand (which lives outside this repository) runs the map update and then the announcement once in the primary checkout after every merge. A failed map update is reported in the landing report and leaves the merge in place; a failed announcement is one line and never fails the step. - By hand.
node oracle/suites/web-features-map.mjsrewrites the snapshot;--checkexits 1 when the committed one is stale against its pinned inputs.
Announcements #
Every landing that moves a web feature forward to partial or complete posts one message
to the community Discord's #nev channel. bench/after-land.sh (both landing paths, above)
notes how long the event log is, runs bench/web-features-update.sh, and hands only the
lines that run appended to bench/feature-announce.py. A feature-status event counts
when it moves a feature forward: from none, unknown or new to partial or complete, or
from partial to complete. A regression (complete to partial) is not announced. No such
event, no post.
Rules come with the job. A feature moves only through an exact-key rule (above), so a
job that implements one lists the rules in its manifest: feature_rules in
specs/<name>.job.json, each an exact browser-compat-data key with its status and
evidence in this file's rule shape, beside expected_features (the web-features ids
that must end partial or complete). The job's spec names docs/feature-map.json in its
touches, or jobs/scripts/submit.sh refuses the job and bench/land.sh would refuse its
landing. job-run adds a feature-map stage before review:
job-check feature-map writes the rules into docs/feature-map.json (through
job-check feature-rules, each directly above the first rule that would otherwise match
its key), commits them, runs this map's update as a dry run, and fails the job when a
listed key or expected feature does not end partial or complete. A missing or wrong rule
therefore fails the job, not the announcement. job-check feature-rules specs/<name>.job.json run by hand on main writes and commits the same rules, for a job
that landed without the stage. After the announcement, bench/after-land.sh runs
bench/expected-features-check.py on the landed job's result.json: a declared feature
that no event moved and that is not partial or complete gets one NOT ANNOUNCED line
naming the fix; one already partial or complete gets an "extended ... nothing to announce"
line.
The post names the features (linked to their webstatus.dev pages) and adds what the work
behind them took, from the landed track's ledger (below): per model the candidates drawn
and compiled, the best score and the completion tokens; the wall time from the track's
first ledger entry or first commit, whichever is earlier, to the landing; lane
CPU-hours; GPU-hours (each model's draw seconds times the cards serving its pool); and an
energy estimate (GPU-hours times card watts plus CPU-hours times a per-CPU figure). Card
counts and watts come from the gitignored rig inventory bench/rig-inventory.local.json
(model_pools, lane_watts_per_cpu; shape in bench/rig-inventory.example.json), since
hardware counts stay out of the repository; a model it does not list is left out of GPU
time, and without it there is no energy figure.
The ledger. Every model batch appends one JSON line to ledger/<track>.jsonl on the
lanes' shared NAS directory (bench/batch-lane.sh batch_measure_stop runs
bench/ledger.py): start and end time, lane CPU-seconds, and per model the counts,
tokens, draw seconds and best score, parsed from the batch binary's own output. No host
names or addresses. The announcement reads it from NEV_LEDGER_DIR, or
$NEV_LANE_SHARED/ledger where that is mounted, or else asks a build lane over ssh. The
track is the landed worktree's name: bench/land.sh passes it; a hand landing finds the
one track/* branch that the merge brought in, and without one the post names the
features only.
Posting. POST <NEV_ANNOUNCE_URL>/announce with Authorization: Bearer <NEV_ANNOUNCE_TOKEN>, both from the environment or the gitignored .env; the bot
(repository phenomena, announce.js) checks the body against Discord's embed limits
and posts it. With either unset or empty the post is skipped with one line. A failed post
is one line naming the HTTP status or that the service could not be reached; it never
fails or undoes the landing, and neither the address nor the token is printed.
Trying it. bench/feature-announce.py --dry-run --events <file> --track <name>
prints the JSON body instead of posting; bench/after-land.sh --dry-run does the same
after a real map update. bench/feature-announce-test.sh checks the ledger line and the
body against bench/fixtures/feature-announce/ and posts to a local stand-in server;
bench/land-test.sh a1 covers a landing that posts, one that does not, and a refused
post that leaves the landing in place.
Existing files changed #
oracle/package.json,oracle/package-lock.json—web-features3.39.0, exact.oracle/suites/dashboard.mjs— its rule loading and matching moved, unchanged, intooracle/suites/feature-map-rules.mjs, which it now imports.crates/wpt-votes/src/features.rs—list_tree_filesandWebFeaturesTree::deciding_dir, with a test;crates/wpt-votes/src/lib.rs— the newtestfilesmodule.bench/land.sh— the map step infinish_landing;bench/land-test.sh— a stand-in map script in the harness and scenariow1.docs/dashboard.md— a pointer to this map.
How to verify #
node --test oracle/suites/web-features-map.test.mjs— the key and feature status rules, the area choice, the snapshot's shape, the diff tool against the committed fixtures, and that the committed snapshot reproduces from its pinned inputs.node oracle/suites/web-features-map.mjs --check— the last of those on its own.cargo run --release -p wpt-votes --bin feature_wpt_rollup bc93a431ddd2e301dece2c6346eb23858aa487a8— rewritesdocs/web-features-wpt.jsonbyte-identically.bench/land-test.sh w1— the landing step.cargo test --workspace— includeswpt_votes::testfilesanddeciding_dir.