From bec868d0355727e842ad8affecb6e405c2bdb532 Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Fri, 24 Apr 2026 19:22:15 +0200 Subject: [PATCH 1/7] Refactor cumulative shipping days calculation to be exportable --- scripts/unmapped-compat-keys.ts | 112 ++++++++++++++++++-------------- 1 file changed, 64 insertions(+), 48 deletions(-) diff --git a/scripts/unmapped-compat-keys.ts b/scripts/unmapped-compat-keys.ts index 1afd4b774cf..998c5bdb09f 100644 --- a/scripts/unmapped-compat-keys.ts +++ b/scripts/unmapped-compat-keys.ts @@ -1,39 +1,20 @@ import { Temporal } from "@js-temporal/polyfill"; import { coreBrowserSet } from "compute-baseline"; import { Compat, Feature } from "compute-baseline/browser-compat-data"; +import { fileURLToPath } from "node:url"; import winston from "winston"; import yargs from "yargs"; import { features } from "../index.js"; import { support } from "../packages/compute-baseline/dist/baseline/support.js"; import { isOrdinaryFeatureData } from "../type-guards.js"; -const compat = new Compat(); -const browsers = coreBrowserSet.map((b) => compat.browser(b)); +const defaultLogLevel = "warn"; const today = Temporal.Now.plainDateISO(); -const argv = yargs(process.argv.slice(2)) - .scriptName("unmapped-compat-keys") - .usage( - "$0", - "Print keys from mdn/browser-compat-data not assigned to a feature", - ) - .option("format", { - choices: ["json", "yaml"], - default: "yaml", - describe: - "Choose the output format. JSON has more detail, while YAML is suited to pasting into feature files.", - }) - .option("verbose", { - alias: "v", - describe: "Show more information", - type: "count", - default: 0, - defaultDescription: "warn", - }) - .parseSync(); +const compat = new Compat(); const logger = winston.createLogger({ - level: argv.verbose > 0 ? "debug" : "warn", + level: defaultLogLevel, format: winston.format.combine( winston.format.colorize(), winston.format.simple(), @@ -52,7 +33,10 @@ const mappedCompatKeys = (() => { ); })(); -const compatFeatures: Map = (() => { +/** + * Get a map of each compat key to the sum of days that key has been shipping. + */ +export function compatFeaturesToCumulativeDaysShipped(): Map { const map = new Map(); for (const f of compat.walk()) { if (f.id.startsWith("webextensions")) { @@ -66,30 +50,6 @@ const compatFeatures: Map = (() => { map.set(f, cumulativeDaysShipped(f)); } return map; -})(); - -const byAge = [...compatFeatures.entries()].sort( - ([, aDays], [, bDays]) => aDays - bDays, -); - -if (argv.format === "yaml") { - for (const [f] of byAge) { - console.log(` - ${f.id}`); - } -} - -if (argv.format === "json") { - console.log( - JSON.stringify( - byAge.map(([f, days]) => ({ - key: f.id, - cumulativeDaysShipped: days, - deprecated: f.deprecated, - })), - undefined, - 2, - ), - ); } /** @@ -103,6 +63,7 @@ if (argv.format === "json") { * @return {number} an integer */ function cumulativeDaysShipped(feature: Feature) { + const browsers = coreBrowserSet.map((b) => compat.browser(b)); const results = support(feature, browsers); return [...results.values()] .filter((r) => r !== undefined) @@ -115,3 +76,58 @@ function cumulativeDaysShipped(feature: Feature) { ) .reduce((prev, curr) => prev + curr, 0); } + +function main() { + const argv = yargs(process.argv.slice(2)) + .scriptName("unmapped-compat-keys") + .usage( + "$0", + "Print keys from mdn/browser-compat-data not assigned to a feature", + ) + .option("format", { + choices: ["json", "yaml"], + default: "yaml", + describe: + "Choose the output format. JSON has more detail, while YAML is suited to pasting into feature files.", + }) + .option("verbose", { + alias: "v", + describe: "Show more information", + type: "count", + default: 0, + defaultDescription: "warn", + }) + .parseSync(); + + logger.transports[0].level = argv.verbose > 0 ? "debug" : "warn"; + + const byAge = [...compatFeaturesToCumulativeDaysShipped().entries()].sort( + ([, aDays], [, bDays]) => aDays - bDays, + ); + + if (argv.format === "yaml") { + for (const [f] of byAge) { + console.log(` - ${f.id}`); + } + } + + if (argv.format === "json") { + console.log( + JSON.stringify( + byAge.map(([f, days]) => ({ + key: f.id, + cumulativeDaysShipped: days, + deprecated: f.deprecated, + })), + undefined, + 2, + ), + ); + } +} + +if (import.meta.url.startsWith("file:")) { + if (process.argv[1] === fileURLToPath(import.meta.url)) { + main(); + } +} From dd3938fb0179dca70c3161da792e26cdab5148ca Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Wed, 29 Apr 2026 18:09:27 +0200 Subject: [PATCH 2/7] Calculate new project stats --- scripts/stats.ts | 140 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 98 insertions(+), 42 deletions(-) diff --git a/scripts/stats.ts b/scripts/stats.ts index b106e5040ab..d1ce4749b95 100644 --- a/scripts/stats.ts +++ b/scripts/stats.ts @@ -1,70 +1,126 @@ import { Compat } from "compute-baseline/browser-compat-data"; +import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import yargs from "yargs"; -import { features } from "../index.js"; +import { features, groups } from "../index.js"; import { isOrdinaryFeatureData } from "../type-guards.js"; +import { caniuseToWebFeaturesId } from "./caniuse.js"; +import { compatFeaturesToCumulativeDaysShipped } from "./unmapped-compat-keys.js"; -yargs(process.argv.slice(2)) +interface Result { + featureCount: number; + groupCount: number; + compatKeys: number; + unmappedCompatKeys: number; + unmappedCompatKeysNormal: number; + unmappedCompatKeysDiscourageable: number; + unmappedCompatKeysCumulativeShippingDays: number; + unmappedCompatKeysCumulativeShippingDaysNormal: number; + unmappedCompatKeysCumulativeShippingDaysDiscourageable: number; + caniuseIds: number; + unmappedCaniuseIds: number; + change?: Change; +} + +type ResultKey = keyof Result; +type ChangeKey = `${ResultKey}Change`; +type Change = Record; + +const argv = yargs(process.argv.slice(2)) .scriptName("stats") - .usage("$0", "Generate statistics").argv; + .option("previous", { + alias: "p", + type: "string", + description: "Path to a JSON file", + coerce: (filePath) => { + const raw = readFileSync(filePath, "utf-8"); + return JSON.parse(raw); + }, + }) + .usage("$0", "Generate statistics") + .parseSync(); -export function stats() { +export function stats(previous: Partial): Result { const featureCount = Object.values(features).filter( isOrdinaryFeatureData, ).length; + const groupCount = Object.values(groups).length; - const keys = []; - const doneKeys = Array.from( - new Set( - Object.values(features).flatMap((f) => { - if (isOrdinaryFeatureData(f)) { - return f.compat_features ?? []; - } - return []; - }), - ), + const mappedCompatKeys = new Set( + Object.values(features).flatMap((f) => { + if (isOrdinaryFeatureData(f)) { + return f.compat_features ?? []; + } + return []; + }), ); + let compatKeys = 0; + let unmappedCompatKeys = 0; + let unmappedCompatKeysNormal = 0; + let unmappedCompatKeysDiscourageable = 0; for (const f of new Compat().walk()) { - if (!f.id.startsWith("webextensions")) { - keys.push(f.id); + if (!f.id.startsWith("webextensions.")) { + compatKeys += 1; + unmappedCompatKeys += mappedCompatKeys.has(f.id) ? 0 : 1; + if (f.deprecated || !f.standard_track) { + unmappedCompatKeysNormal += 1; + } else { + unmappedCompatKeysDiscourageable += 1; + } } } - const featureSizes = Object.values(features) - .filter(isOrdinaryFeatureData) - .map((feature) => (feature.compat_features ?? []).length) - .sort((a, b) => a - b); + const featuresToDays = compatFeaturesToCumulativeDaysShipped(); + const unmappedCompatKeysCumulativeShippingDaysDiscourageable = [ + ...featuresToDays.values(), + ].reduce((prev, curr) => prev + curr, 0); + const unmappedCompatKeysCumulativeShippingDaysNormal = (() => { + let sum = 0; + for (const [feature, days] of featuresToDays.entries()) { + if (!feature.deprecated && feature.standard_track) { + sum += days; + } + } + return sum; + })(); + const unmappedCompatKeysCumulativeShippingDays = + unmappedCompatKeysCumulativeShippingDaysDiscourageable + + unmappedCompatKeysCumulativeShippingDaysNormal; + + const caniuseIds = [...caniuseToWebFeaturesId.keys()].length; + const unmappedCaniuseIds = [...caniuseToWebFeaturesId.values()].filter( + (v) => v === null, + ).length; const result = { - features: featureCount, - compatKeys: doneKeys.length, - compatKeysUnmapped: keys.length - doneKeys.length, - compatCoverage: doneKeys.length / keys.length, - compatKeysPerFeatureMean: doneKeys.length / featureCount, - compatKeysPerFeatureMedian: (() => { - const sizes = featureSizes; - const middle = Math.floor(sizes.length / 2); - return sizes.length % 2 - ? sizes[middle] - : (sizes[middle - 1] + sizes[middle]) / 2; - })(), - compatKeysPerFeatureMode: (() => { - const frequencyMap = new Map(); - for (const size of featureSizes) { - frequencyMap.set(size, (frequencyMap.get(size) ?? 0) + 1); - } - return [...frequencyMap.entries()] - .sort(([, frequencyA], [, frequencyB]) => frequencyA - frequencyB) - .pop()[0]; - })(), + featureCount, + groupCount, + compatKeys, + unmappedCompatKeys, + unmappedCompatKeysNormal, + unmappedCompatKeysDiscourageable, + unmappedCompatKeysCumulativeShippingDays, + unmappedCompatKeysCumulativeShippingDaysNormal, + unmappedCompatKeysCumulativeShippingDaysDiscourageable, + caniuseIds, + unmappedCaniuseIds, }; + if (previous) { + const change = Object.fromEntries( + Object.keys(previous).map((key) => [ + `${key}Change`, + result[key] - previous[key], + ]), + ) as Change; + return { ...result, change }; + } return result; } if (import.meta.url.startsWith("file:")) { if (process.argv[1] === fileURLToPath(import.meta.url)) { - console.log(JSON.stringify(stats(), undefined, 2)); + console.log(JSON.stringify(stats(argv.previous), undefined, 2)); } } From 7ea382d159c7d8c4c62ecb07af8c02ae7f866e96 Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Tue, 26 May 2026 14:12:18 +0200 Subject: [PATCH 3/7] Calculate unmapped keys and shipping days by sets --- scripts/stats.ts | 60 ++++++++++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/scripts/stats.ts b/scripts/stats.ts index 72425129a36..0e9fb0fae32 100644 --- a/scripts/stats.ts +++ b/scripts/stats.ts @@ -55,35 +55,51 @@ export function stats(previous: Partial): Result { }), ); - let compatKeys = 0; - let unmappedCompatKeys = 0; - let unmappedCompatKeysNormal = 0; - let unmappedCompatKeysDiscourageable = 0; + const inScopeCompatKeys = new Set(); + const deprecatedCompatKeys = new Set(); + const nonstandardCompatKeys = new Set(); for (const f of new Compat().walk()) { if (!f.id.startsWith("webextensions.")) { - compatKeys += 1; - unmappedCompatKeys += mappedCompatKeys.has(f.id) ? 0 : 1; - if (f.deprecated || !f.standard_track) { - unmappedCompatKeysNormal += 1; - } else { - unmappedCompatKeysDiscourageable += 1; + inScopeCompatKeys.add(f.id); + if (f.deprecated) { + deprecatedCompatKeys.add(f.id); + } + if (!f.standard_track) { + nonstandardCompatKeys.add(f.id); } } } + const discourageableCompatKeys = deprecatedCompatKeys.union( + nonstandardCompatKeys, + ); + const normalCompatKeys = inScopeCompatKeys.difference( + discourageableCompatKeys, + ); + const mappableKeys = inScopeCompatKeys.difference(mappedCompatKeys); + + let compatKeys = inScopeCompatKeys.size; + let unmappedCompatKeys = mappableKeys.size; + let unmappedCompatKeysNormal = mappableKeys.difference( + discourageableCompatKeys, + ).size; + let unmappedCompatKeysDiscourageable = + mappableKeys.difference(normalCompatKeys).size; + const featuresToDays = compatFeaturesToCumulativeDaysShipped(); - const unmappedCompatKeysCumulativeShippingDaysDiscourageable = [ - ...featuresToDays.values(), - ].reduce((prev, curr) => prev + curr, 0); - const unmappedCompatKeysCumulativeShippingDaysNormal = (() => { - let sum = 0; - for (const [feature, days] of featuresToDays.entries()) { - if (!feature.deprecated && feature.standard_track) { - sum += days; - } - } - return sum; - })(); + const unmappedCompatKeysCumulativeShippingDaysDiscourageable = Array.from( + featuresToDays.entries(), + ) + .filter(([f, days]) => discourageableCompatKeys.has(f.id)) + .map(([f, days]) => days) + .reduce((prev, curr) => prev + curr, 0); + const unmappedCompatKeysCumulativeShippingDaysNormal = Array.from( + featuresToDays.entries(), + ) + .filter(([f, days]) => normalCompatKeys.has(f.id)) + .map(([f, days]) => days) + .reduce((prev, curr) => prev + curr, 0); + const unmappedCompatKeysCumulativeShippingDays = unmappedCompatKeysCumulativeShippingDaysDiscourageable + unmappedCompatKeysCumulativeShippingDaysNormal; From 7bcedf76a04694a97335b17ec4e3d246d4b778c3 Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Thu, 28 May 2026 20:00:57 +0200 Subject: [PATCH 4/7] Fix linter errors --- scripts/stats.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/stats.ts b/scripts/stats.ts index 0e9fb0fae32..5bfa9ddde86 100644 --- a/scripts/stats.ts +++ b/scripts/stats.ts @@ -90,14 +90,14 @@ export function stats(previous: Partial): Result { const unmappedCompatKeysCumulativeShippingDaysDiscourageable = Array.from( featuresToDays.entries(), ) - .filter(([f, days]) => discourageableCompatKeys.has(f.id)) - .map(([f, days]) => days) + .filter(([f]) => discourageableCompatKeys.has(f.id)) + .map(([, days]) => days) .reduce((prev, curr) => prev + curr, 0); const unmappedCompatKeysCumulativeShippingDaysNormal = Array.from( featuresToDays.entries(), ) - .filter(([f, days]) => normalCompatKeys.has(f.id)) - .map(([f, days]) => days) + .filter(([f]) => normalCompatKeys.has(f.id)) + .map(([, days]) => days) .reduce((prev, curr) => prev + curr, 0); const unmappedCompatKeysCumulativeShippingDays = From 59a24beee100e7d2c48adf5c5152b0c7bb6f69f5 Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Tue, 4 Aug 2026 18:41:46 +0200 Subject: [PATCH 5/7] Improve stat field names, descriptions, and completeness --- EXAMPLE.json | 23 ++++++++ scripts/stats.ts | 135 ++++++++++++++++++++++++++++++++++------------- 2 files changed, 122 insertions(+), 36 deletions(-) create mode 100644 EXAMPLE.json diff --git a/EXAMPLE.json b/EXAMPLE.json new file mode 100644 index 00000000000..66d8aaf4f18 --- /dev/null +++ b/EXAMPLE.json @@ -0,0 +1,23 @@ +{ + "featuresCount": 1168, + "groupsCount": 103, + "compatKeysCount": 17734, + "normalCompatKeysCount": 16042, + "discourageableCompatKeysCount": 1692, + "mappedCompatKeysCount": 15060, + "unmappedCompatKeysCount": 2674, + "mappedNormalCompatKeysCount": 13301, + "unmappedNormalCompatKeysCount": 1759, + "mappedDiscourageableCompatKeysCount": 777, + "unmappedDiscourageableCompatKeysCount": 915, + "mappedCompatKeysRatio": 0.8492161948798918, + "unmappedCompatKeysRatio": 0.15078380512010828, + "unmappedNormalCompatKeysRatio": 0.10964966961725471, + "unmappedDiscourageableCompatKeysRatio": 0.5407801418439716, + "caniuseIdsCount": 583, + "unmappedCaniuseIdsCount": 236, + "unmappedCaniuseIdsRatio": 0.40480274442538594, + "unmappedCompatKeysCumulativeShippingDays": 31138557, + "unmappedNormalCompatKeysCumulativeShippingDays": 12107641, + "unmappedDiscourageableCompatKeysCumulativeShippingDays": 19030916 +} diff --git a/scripts/stats.ts b/scripts/stats.ts index 5bfa9ddde86..344e3a175c8 100644 --- a/scripts/stats.ts +++ b/scripts/stats.ts @@ -7,18 +7,49 @@ import { isOrdinaryFeatureData } from "../type-guards.ts"; import { caniuseToWebFeaturesId } from "./caniuse.ts"; import { compatFeaturesToCumulativeDaysShipped } from "./unmapped-compat-keys.ts"; -interface Result { - featureCount: number; - groupCount: number; - compatKeys: number; - unmappedCompatKeys: number; - unmappedCompatKeysNormal: number; - unmappedCompatKeysDiscourageable: number; +interface ResultBase { + featuresCount: number; // `kind: feature` ID count (ignores other `kind` values) + groupsCount: number; // group ID count + compatKeysCount: number; // Count of in-scope BCD keys (i.e., excluding webextensions) + normalCompatKeysCount: number; // Count of in-scope BCD keys that are "normal" — standard and not deprecated + discourageableCompatKeysCount: number; // Count of in-scope BCD keys that are not "normal" — non-standard or deprecated + + // Mapped keys are mapped to one or more web-features entries (higher is better). + // Unmapped keys are not mapped to any web-features entry (lower is better). + mappedCompatKeysCount: number; + unmappedCompatKeysCount: number; + + mappedNormalCompatKeysCount: number; + unmappedNormalCompatKeysCount: number; + + mappedDiscourageableCompatKeysCount: number; + unmappedDiscourageableCompatKeysCount: number; + + mappedCompatKeysRatio: number; // mappedCompatKeysCount : compatKeysCount + unmappedCompatKeysRatio: number; // unmappedCompatKeysCount : compatKeysCount + unmappedNormalCompatKeysRatio: number; // unmappedNormalCompatKeysCount : normalCompatKeysCount + unmappedDiscourageableCompatKeysRatio: number; // unmappedDiscourageableCompatKeysCount : discourageableCompatKeysCount + + caniuseIdsCount: number; // caniuse IDs + unmappedCaniuseIdsCount: number; // caniuseIDs that lack a corresponding web-features entry + unmappedCaniuseIdsRatio: number; // unmappedCaniuseIdsCont : caniuseIDs + + // The "cumulative shipping days" metrics are the sum of days since each BCD + // key has shipped in each browser in the core browser set. A smaller number + // is better. This number increases every day for every key that has shipped + // but has not been mapped. This metric encourages us to map keys that are + // more likely to affect real-world developers (i.e., features implemented in + // multiple browsers). Widely-implemented but not mapped keys increase this + // metric more (up to +7 per calendar day per key), while other keys increase + // this metric less or not at all. Keys which have not yet shipped in any + // stable release count as 0. Like the Baseline calculation, partials and + // prefixes do not count as shipping. unmappedCompatKeysCumulativeShippingDays: number; - unmappedCompatKeysCumulativeShippingDaysNormal: number; - unmappedCompatKeysCumulativeShippingDaysDiscourageable: number; - caniuseIds: number; - unmappedCaniuseIds: number; + unmappedNormalCompatKeysCumulativeShippingDays: number; + unmappedDiscourageableCompatKeysCumulativeShippingDays: number; +} + +interface Result extends ResultBase { change?: Change; } @@ -41,10 +72,10 @@ const argv = yargs(process.argv.slice(2)) .parseSync(); export function stats(previous: Partial): Result { - const featureCount = Object.values(features).filter( + const featuresCount = Object.values(features).filter( isOrdinaryFeatureData, ).length; - const groupCount = Object.values(groups).length; + const groupsCount = Object.values(groups).length; const mappedCompatKeys = new Set( Object.values(features).flatMap((f) => { @@ -76,24 +107,32 @@ export function stats(previous: Partial): Result { const normalCompatKeys = inScopeCompatKeys.difference( discourageableCompatKeys, ); - const mappableKeys = inScopeCompatKeys.difference(mappedCompatKeys); + const unmappedKeys = inScopeCompatKeys.difference(mappedCompatKeys); - let compatKeys = inScopeCompatKeys.size; - let unmappedCompatKeys = mappableKeys.size; - let unmappedCompatKeysNormal = mappableKeys.difference( + const compatKeysCount = inScopeCompatKeys.size; + const normalCompatKeysCount = normalCompatKeys.size; + const discourageableCompatKeysCount = compatKeysCount - normalCompatKeysCount; + + const mappedCompatKeysCount = mappedCompatKeys.size; + const unmappedCompatKeysCount = unmappedKeys.size; + const unmappedNormalCompatKeysCount = unmappedKeys.difference( discourageableCompatKeys, ).size; - let unmappedCompatKeysDiscourageable = - mappableKeys.difference(normalCompatKeys).size; + const mappedNormalCompatKeysCount = + mappedCompatKeysCount - unmappedNormalCompatKeysCount; + const unmappedDiscourageableCompatKeysCount = + unmappedKeys.difference(normalCompatKeys).size; + const mappedDiscourageableCompatKeysCount = + discourageableCompatKeysCount - unmappedDiscourageableCompatKeysCount; const featuresToDays = compatFeaturesToCumulativeDaysShipped(); - const unmappedCompatKeysCumulativeShippingDaysDiscourageable = Array.from( + const unmappedDiscourageableCompatKeysCumulativeShippingDays = Array.from( featuresToDays.entries(), ) .filter(([f]) => discourageableCompatKeys.has(f.id)) .map(([, days]) => days) .reduce((prev, curr) => prev + curr, 0); - const unmappedCompatKeysCumulativeShippingDaysNormal = Array.from( + const unmappedNormalCompatKeysCumulativeShippingDays = Array.from( featuresToDays.entries(), ) .filter(([f]) => normalCompatKeys.has(f.id)) @@ -101,26 +140,50 @@ export function stats(previous: Partial): Result { .reduce((prev, curr) => prev + curr, 0); const unmappedCompatKeysCumulativeShippingDays = - unmappedCompatKeysCumulativeShippingDaysDiscourageable + - unmappedCompatKeysCumulativeShippingDaysNormal; + unmappedDiscourageableCompatKeysCumulativeShippingDays + + unmappedNormalCompatKeysCumulativeShippingDays; - const caniuseIds = [...caniuseToWebFeaturesId.keys()].length; - const unmappedCaniuseIds = [...caniuseToWebFeaturesId.values()].filter( + const caniuseIdsCount = [...caniuseToWebFeaturesId.keys()].length; + const unmappedCaniuseIdsCount = [...caniuseToWebFeaturesId.values()].filter( (v) => v === null, ).length; + const unmappedCaniuseIdsRatio = unmappedCaniuseIdsCount / caniuseIdsCount; + + const mappedCompatKeysRatio = mappedCompatKeysCount / compatKeysCount; + const unmappedCompatKeysRatio = unmappedCompatKeysCount / compatKeysCount; + const unmappedNormalCompatKeysRatio = + unmappedNormalCompatKeysCount / normalCompatKeysCount; + const unmappedDiscourageableCompatKeysRatio = + unmappedDiscourageableCompatKeysCount / discourageableCompatKeysCount; + + const result: ResultBase = { + featuresCount, + groupsCount, + compatKeysCount, + normalCompatKeysCount, + discourageableCompatKeysCount, + + mappedCompatKeysCount, + unmappedCompatKeysCount, + + mappedNormalCompatKeysCount, + unmappedNormalCompatKeysCount, + + mappedDiscourageableCompatKeysCount, + unmappedDiscourageableCompatKeysCount, + + mappedCompatKeysRatio, + unmappedCompatKeysRatio, + unmappedNormalCompatKeysRatio, + unmappedDiscourageableCompatKeysRatio, + + caniuseIdsCount, + unmappedCaniuseIdsCount, + unmappedCaniuseIdsRatio, - const result = { - featureCount, - groupCount, - compatKeys, - unmappedCompatKeys, - unmappedCompatKeysNormal, - unmappedCompatKeysDiscourageable, unmappedCompatKeysCumulativeShippingDays, - unmappedCompatKeysCumulativeShippingDaysNormal, - unmappedCompatKeysCumulativeShippingDaysDiscourageable, - caniuseIds, - unmappedCaniuseIds, + unmappedNormalCompatKeysCumulativeShippingDays, + unmappedDiscourageableCompatKeysCumulativeShippingDays, }; if (previous) { From e8ca693f0be6e0293780a256af2beef13d94f590 Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Wed, 5 Aug 2026 16:23:21 +0200 Subject: [PATCH 6/7] Fix stats clock to BCD releases --- EXAMPLE.json | 23 ----------------------- scripts/unmapped-compat-keys.ts | 12 ++++++++++-- 2 files changed, 10 insertions(+), 25 deletions(-) delete mode 100644 EXAMPLE.json diff --git a/EXAMPLE.json b/EXAMPLE.json deleted file mode 100644 index 66d8aaf4f18..00000000000 --- a/EXAMPLE.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "featuresCount": 1168, - "groupsCount": 103, - "compatKeysCount": 17734, - "normalCompatKeysCount": 16042, - "discourageableCompatKeysCount": 1692, - "mappedCompatKeysCount": 15060, - "unmappedCompatKeysCount": 2674, - "mappedNormalCompatKeysCount": 13301, - "unmappedNormalCompatKeysCount": 1759, - "mappedDiscourageableCompatKeysCount": 777, - "unmappedDiscourageableCompatKeysCount": 915, - "mappedCompatKeysRatio": 0.8492161948798918, - "unmappedCompatKeysRatio": 0.15078380512010828, - "unmappedNormalCompatKeysRatio": 0.10964966961725471, - "unmappedDiscourageableCompatKeysRatio": 0.5407801418439716, - "caniuseIdsCount": 583, - "unmappedCaniuseIdsCount": 236, - "unmappedCaniuseIdsRatio": 0.40480274442538594, - "unmappedCompatKeysCumulativeShippingDays": 31138557, - "unmappedNormalCompatKeysCumulativeShippingDays": 12107641, - "unmappedDiscourageableCompatKeysCumulativeShippingDays": 19030916 -} diff --git a/scripts/unmapped-compat-keys.ts b/scripts/unmapped-compat-keys.ts index c63464f6fd0..1f949f09f24 100644 --- a/scripts/unmapped-compat-keys.ts +++ b/scripts/unmapped-compat-keys.ts @@ -9,10 +9,18 @@ import { support } from "../packages/compute-baseline/dist/baseline/support.js"; import { isOrdinaryFeatureData } from "../type-guards.ts"; const defaultLogLevel = "warn"; -const today = Temporal.Now.plainDateISO(); const compat = new Compat(); +// The reference date is a date that approximates "now" but doesn't advance past +// the moment in time that a BCD release represents. This makes generated stats +// consistent between runs where the underlying data hasn't changed but the +// wall-clock time has. It also permits running stats retrospectively. +const bcdTimestamp: string = (compat.data as any).__meta.timestamp; +const referenceDate = Temporal.Instant.from(bcdTimestamp) + .toZonedDateTimeISO("UTC") + .toPlainDate(); + const logger = winston.createLogger({ level: defaultLogLevel, format: winston.format.combine( @@ -69,7 +77,7 @@ function cumulativeDaysShipped(feature: Feature) { .filter((r) => r !== undefined) .map( (r) => - r.release.date.until(today, { + r.release.date.until(referenceDate, { largestUnit: "days", smallestUnit: "days", }).days, From 368dc11351c2a900be8149d684099c70a3b788ac Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Mon, 17 Aug 2026 14:01:59 +0200 Subject: [PATCH 7/7] Add stats script to `package.json` commands --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 321bfa3642a..3613f8cbce5 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "test:types": "npm run --workspaces test:types && tsc", "test": "npm run test:caniuse -- --quiet && npm run test:schematypes && npm run test:specs && npm run test:types && npm run test:format && npm run test:index && npm run test:dist && npm test --workspaces && npm run test:lint", "update-drafts": "tsx scripts/update-drafts.ts", - "remove-tagged-compat-features": "tsx scripts/remove-tagged-compat-features.ts && npm run format" + "remove-tagged-compat-features": "tsx scripts/remove-tagged-compat-features.ts && npm run format", + "stats": "tsx ./scripts/stats.ts" }, "devDependencies": { "@eslint/js": "^10.0.1",