this repo has no description
1import { readFileSync, readdirSync } from 'fs';
2import { join } from 'path';
3
4const DATA_DIR = new URL('.', import.meta.url).pathname;
5const HIST_DIR = join(DATA_DIR, 'historical');
6
7const webFeatures = JSON.parse(readFileSync('/Users/dietrich/misc/web-features/packages/web-features/data.json', 'utf8'));
8const manifestFile = readdirSync(DATA_DIR).find(f => f.startsWith('WEB_FEATURES_MANIFEST') && f.endsWith('.json'));
9const wptFeatureManifest = JSON.parse(readFileSync(join(DATA_DIR, manifestFile), 'utf8'));
10
11const FULL_THRESHOLD = 0.95;
12const PARTIAL_THRESHOLD = 0.20;
13
14// ============================================================
15// Helper: compute feature scores from a WPT summary
16// ============================================================
17function computeFeatureScores(wptSummary) {
18 const scores = {};
19 for (const [featureId, testPaths] of Object.entries(wptFeatureManifest.data)) {
20 let totalTests = 0, passingTests = 0, totalSubtests = 0, passedSubtests = 0;
21
22 for (const testPath of testPaths) {
23 const result = wptSummary[testPath];
24 if (!result) continue;
25 totalTests++;
26 if (result.c && result.c[1] > 0) {
27 passingTests += result.c[0] / result.c[1];
28 passedSubtests += result.c[0];
29 totalSubtests += result.c[1];
30 } else {
31 const pass = result.s === 'O' || result.s === 'P';
32 if (pass) passingTests += 1;
33 totalSubtests += 1;
34 passedSubtests += pass ? 1 : 0;
35 }
36 }
37
38 if (totalTests > 0) {
39 scores[featureId] = {
40 score: passingTests / totalTests,
41 subtestRate: totalSubtests > 0 ? passedSubtests / totalSubtests : 0,
42 matchedTests: totalTests
43 };
44 }
45 }
46 return scores;
47}
48
49// ============================================================
50// Helper: compute baseline readiness from feature scores
51// ============================================================
52function computeReadiness(featureScores) {
53 const result = {
54 high: { total: 0, full: 0, partial: 0, none: 0, noData: 0 },
55 low: { total: 0, full: 0, partial: 0, none: 0, noData: 0 },
56 all: { total: 0, full: 0, partial: 0, none: 0, noData: 0 }
57 };
58
59 for (const [featureName, feature] of Object.entries(webFeatures.features)) {
60 const baseline = feature.status?.baseline === "high" ? "high"
61 : feature.status?.baseline === "low" ? "low" : null;
62
63 const score = featureScores[featureName]?.score ?? null;
64
65 for (const bucket of [baseline, "all"].filter(Boolean)) {
66 result[bucket].total++;
67 if (score === null) result[bucket].noData++;
68 else if (score >= FULL_THRESHOLD) result[bucket].full++;
69 else if (score >= PARTIAL_THRESHOLD) result[bucket].partial++;
70 else result[bucket].none++;
71 }
72
73 // count non-baseline in "all" if not already
74 if (!baseline) {
75 result.all.total++;
76 if (score === null) result.all.noData++;
77 else if (score >= FULL_THRESHOLD) result.all.full++;
78 else if (score >= PARTIAL_THRESHOLD) result.all.partial++;
79 else result.all.none++;
80 }
81 }
82
83 return result;
84}
85
86// ============================================================
87// Load all snapshots
88// ============================================================
89const snapshots = [
90 { label: "2023-Q3", file: "2023-Q3.json" },
91 { label: "2024-Q1", file: "2024-Q1.json" },
92 { label: "2024-Q3", file: "2024-Q3.json" },
93 { label: "2025-Q1", file: "2025-Q1.json" },
94 { label: "2025-Q3", file: "2025-Q3.json" },
95 { label: "2026-Q1 (current)", file: "../servo-wpt-summary.json" },
96];
97
98console.log("=== PART 1: Feature-Level Velocity (WPT-based) ===\n");
99console.log("Thresholds: Full >= 95%, Partial >= 20%\n");
100
101const timeSeriesData = [];
102
103for (const snap of snapshots) {
104 const filePath = snap.file.startsWith("..") ? join(DATA_DIR, snap.file.replace("../", "")) : join(HIST_DIR, snap.file);
105 const wptSummary = JSON.parse(readFileSync(filePath, 'utf8'));
106 const featureScores = computeFeatureScores(wptSummary);
107 const readiness = computeReadiness(featureScores);
108
109 timeSeriesData.push({ label: snap.label, readiness, featureScores });
110
111 const h = readiness.high;
112 const hPct = (h.full / h.total * 100).toFixed(1);
113 const hPartialPct = ((h.full + h.partial) / h.total * 100).toFixed(1);
114 console.log(`${snap.label}:`);
115 console.log(` Widely Available: ${h.full}/${h.total} full (${hPct}%), ${h.full + h.partial}/${h.total} incl. partial (${hPartialPct}%)`);
116 console.log(` All features: ${readiness.all.full} full, ${readiness.all.partial} partial out of ${readiness.all.total}`);
117}
118
119// ============================================================
120// PART 2: Velocity calculation
121// ============================================================
122console.log("\n=== PART 2: Velocity (Features Gained Per Quarter) ===\n");
123
124console.log("Widely Available features fully supported (>= 95%):");
125for (let i = 1; i < timeSeriesData.length; i++) {
126 const prev = timeSeriesData[i - 1];
127 const curr = timeSeriesData[i];
128 const delta = curr.readiness.high.full - prev.readiness.high.full;
129 const deltaPartial = (curr.readiness.high.full + curr.readiness.high.partial) -
130 (prev.readiness.high.full + prev.readiness.high.partial);
131 console.log(` ${prev.label} -> ${curr.label}: +${delta} full, +${deltaPartial} incl. partial`);
132}
133
134// ============================================================
135// PART 3: Per-feature score changes (biggest movers)
136// ============================================================
137console.log("\n=== PART 3: Biggest Score Improvements (Widely Available, 2023-Q3 → current) ===\n");
138
139const first = timeSeriesData[0].featureScores;
140const last = timeSeriesData[timeSeriesData.length - 1].featureScores;
141
142const improvements = [];
143for (const [featureName, feature] of Object.entries(webFeatures.features)) {
144 if (feature.status?.baseline !== "high") continue;
145 const startScore = first[featureName]?.score ?? 0;
146 const endScore = last[featureName]?.score ?? 0;
147 const delta = endScore - startScore;
148 if (delta > 0) {
149 improvements.push({ name: featureName, start: startScore, end: endScore, delta });
150 }
151}
152
153improvements.sort((a, b) => b.delta - a.delta);
154console.log("Top 30 features by score improvement:");
155for (const f of improvements.slice(0, 30)) {
156 console.log(` ${f.name}: ${(f.start * 100).toFixed(1)}% -> ${(f.end * 100).toFixed(1)}% (+${(f.delta * 100).toFixed(1)}pp)`);
157}
158
159console.log(`\nTotal features improved: ${improvements.length}`);
160console.log(`Average improvement: ${(improvements.reduce((s, f) => s + f.delta, 0) / improvements.length * 100).toFixed(1)}pp`);
161
162// Features that crossed the full threshold
163const newlyFull = improvements.filter(f => f.start < FULL_THRESHOLD && f.end >= FULL_THRESHOLD);
164console.log(`\nFeatures that crossed 95% threshold: ${newlyFull.length}`);
165for (const f of newlyFull.slice(0, 20)) {
166 console.log(` ${f.name}: ${(f.start * 100).toFixed(1)}% -> ${(f.end * 100).toFixed(1)}%`);
167}
168
169// ============================================================
170// PART 4: Commit/contributor velocity context
171// ============================================================
172console.log("\n=== PART 4: Commit & Contributor Context ===\n");
173
174// Monthly data from git analysis (hardcoded from the git extraction)
175const monthlyData = [
176 { month: "2023-01", commits: 168, authors: 25 },
177 { month: "2023-02", commits: 187, authors: 22 },
178 { month: "2023-03", commits: 197, authors: 22 },
179 { month: "2023-04", commits: 166, authors: 21 },
180 { month: "2023-05", commits: 495, authors: 40 },
181 { month: "2023-06", commits: 279, authors: 27 },
182 { month: "2023-07", commits: 86, authors: 17 },
183 { month: "2023-08", commits: 235, authors: 25 },
184 { month: "2023-09", commits: 151, authors: 15 },
185 { month: "2023-10", commits: 140, authors: 14 },
186 { month: "2023-11", commits: 149, authors: 13 },
187 { month: "2023-12", commits: 110, authors: 12 },
188 { month: "2024-01", commits: 193, authors: 21 },
189 { month: "2024-02", commits: 166, authors: 25 },
190 { month: "2024-03", commits: 323, authors: 34 },
191 { month: "2024-04", commits: 186, authors: 29 },
192 { month: "2024-05", commits: 163, authors: 25 },
193 { month: "2024-06", commits: 157, authors: 24 },
194 { month: "2024-07", commits: 171, authors: 31 },
195 { month: "2024-08", commits: 269, authors: 32 },
196 { month: "2024-09", commits: 217, authors: 26 },
197 { month: "2024-10", commits: 332, authors: 40 },
198 { month: "2024-11", commits: 216, authors: 21 },
199 { month: "2024-12", commits: 216, authors: 29 },
200 { month: "2025-01", commits: 289, authors: 30 },
201 { month: "2025-02", commits: 305, authors: 40 },
202 { month: "2025-03", commits: 317, authors: 59 },
203 { month: "2025-04", commits: 314, authors: 50 },
204 { month: "2025-05", commits: 251, authors: 41 },
205 { month: "2025-06", commits: 350, authors: 45 },
206 { month: "2025-07", commits: 363, authors: 44 },
207 { month: "2025-08", commits: 439, authors: 48 },
208 { month: "2025-09", commits: 341, authors: 42 },
209 { month: "2025-10", commits: 438, authors: 50 },
210 { month: "2025-11", commits: 401, authors: 44 },
211 { month: "2025-12", commits: 340, authors: 52 },
212 { month: "2026-01", commits: 398, authors: 51 },
213 { month: "2026-02", commits: 197, authors: 39 },
214];
215
216// Quarterly aggregation
217const quarters = {};
218for (const m of monthlyData) {
219 const [y, mo] = m.month.split("-");
220 const q = `${y}-Q${Math.ceil(parseInt(mo) / 3)}`;
221 if (!quarters[q]) quarters[q] = { commits: 0, months: 0, authorsSet: new Set(), maxAuthors: 0 };
222 quarters[q].commits += m.commits;
223 quarters[q].months++;
224 quarters[q].maxAuthors = Math.max(quarters[q].maxAuthors, m.authors);
225}
226
227console.log("Quarterly summary:");
228console.log("Quarter | Commits | Peak Authors/Month | Avg Commits/Month");
229console.log("-----------|---------|--------------------|-----------------");
230for (const [q, d] of Object.entries(quarters).sort()) {
231 const avg = (d.commits / d.months).toFixed(0);
232 console.log(`${q.padEnd(10)} | ${String(d.commits).padStart(7)} | ${String(d.maxAuthors).padStart(18)} | ${String(avg).padStart(17)}`);
233}
234
235// ============================================================
236// PART 5: Velocity-based projections
237// ============================================================
238console.log("\n=== PART 5: Projections ===\n");
239
240// Calculate rate of "fully supported" Widely Available features gained per quarter
241const firstSnap = timeSeriesData[0];
242const lastSnap = timeSeriesData[timeSeriesData.length - 1];
243const quartersElapsed = (snapshots.length - 1) * 0.5 * 2; // roughly: 2023-Q3 to 2026-Q1 = ~10 quarters
244
245// More precise: 2023-Q3 (Jul 2023) to 2026-Q1 (Feb 2026) = ~2.58 years = ~10.3 quarters
246const yearsElapsed = 2.58;
247const quartersTotal = yearsElapsed * 4;
248
249const fullGained = lastSnap.readiness.high.full - firstSnap.readiness.high.full;
250const partialGained = (lastSnap.readiness.high.full + lastSnap.readiness.high.partial) -
251 (firstSnap.readiness.high.full + firstSnap.readiness.high.partial);
252
253const fullPerQuarter = fullGained / quartersTotal;
254const partialPerQuarter = partialGained / quartersTotal;
255
256console.log(`Over ${yearsElapsed} years (${quartersTotal.toFixed(1)} quarters):`);
257console.log(` Full features gained: ${fullGained} (${fullPerQuarter.toFixed(1)}/quarter)`);
258console.log(` Full+Partial gained: ${partialGained} (${partialPerQuarter.toFixed(1)}/quarter)`);
259
260// Recent velocity (last ~5 quarters, 2025-Q1 to 2026-Q1)
261const recentFirst = timeSeriesData[3]; // 2025-Q1
262const recentQuarters = 4; // ~4 quarters to current
263const recentFullGained = lastSnap.readiness.high.full - recentFirst.readiness.high.full;
264const recentPartialGained = (lastSnap.readiness.high.full + lastSnap.readiness.high.partial) -
265 (recentFirst.readiness.high.full + recentFirst.readiness.high.partial);
266const recentFullRate = recentFullGained / recentQuarters;
267const recentPartialRate = recentPartialGained / recentQuarters;
268
269console.log(`\nRecent velocity (2025-Q1 to 2026-Q1, ~${recentQuarters} quarters):`);
270console.log(` Full features gained: ${recentFullGained} (${recentFullRate.toFixed(1)}/quarter)`);
271console.log(` Full+Partial gained: ${recentPartialGained} (${recentPartialRate.toFixed(1)}/quarter)`);
272
273// Projections
274const currentFull = lastSnap.readiness.high.full;
275const totalWidelyAvailable = lastSnap.readiness.high.total;
276const remaining = totalWidelyAvailable - currentFull;
277
278console.log(`\nCurrent: ${currentFull}/${totalWidelyAvailable} Widely Available features fully supported`);
279console.log(`Remaining: ${remaining} features to reach 100%`);
280
281for (const [label, rate] of [["Overall average", fullPerQuarter], ["Recent rate", recentFullRate]]) {
282 if (rate <= 0) {
283 console.log(` At ${label} velocity: never (zero or negative rate)`);
284 continue;
285 }
286 const quartersNeeded = remaining / rate;
287 const yearsNeeded = quartersNeeded / 4;
288 const targetYear = 2026.1 + yearsNeeded; // roughly current date
289 console.log(` At ${label} velocity (${rate.toFixed(1)}/quarter): ${quartersNeeded.toFixed(0)} quarters (${yearsNeeded.toFixed(1)} years) → ~${Math.ceil(targetYear)}`);
290}
291
292// What about reaching specific thresholds?
293console.log("\nProjected dates for milestones:");
294for (const target of [0.25, 0.50, 0.75, 1.0]) {
295 const needed = Math.ceil(totalWidelyAvailable * target) - currentFull;
296 if (needed <= 0) {
297 console.log(` ${(target * 100).toFixed(0)}% (${Math.ceil(totalWidelyAvailable * target)} features): ALREADY REACHED`);
298 continue;
299 }
300 for (const [label, rate] of [["overall", fullPerQuarter], ["recent", recentFullRate]]) {
301 if (rate <= 0) continue;
302 const q = needed / rate;
303 const y = 2026.1 + q / 4;
304 console.log(` ${(target * 100).toFixed(0)}% (${Math.ceil(totalWidelyAvailable * target)} features): ${q.toFixed(0)} quarters at ${label} rate → ~${y.toFixed(1)}`);
305 }
306}
307
308// ============================================================
309// PART 6: Correlation between commits and WPT improvement
310// ============================================================
311console.log("\n=== PART 6: Commits vs. WPT Improvement Correlation ===\n");
312
313// WPT scores from scores.json data (hardcoded quarterly from the earlier analysis)
314const wptQuarterlyScores = [
315 { q: "2023-Q2", score: 30.35 },
316 { q: "2023-Q3", score: 32.04 },
317 { q: "2023-Q4", score: 33.24 },
318 { q: "2024-Q1", score: 34.32 },
319 { q: "2024-Q2", score: 38.38 },
320 { q: "2024-Q3", score: 39.83 },
321 { q: "2024-Q4", score: 42.06 },
322 { q: "2025-Q1", score: 47.36 },
323 { q: "2025-Q2", score: 51.34 },
324 { q: "2025-Q3", score: 55.51 },
325 { q: "2025-Q4", score: 57.03 },
326 { q: "2026-Q1", score: 62.38 },
327];
328
329console.log("Quarter | Commits | WPT Score | WPT Delta | Score/Commit");
330console.log("-----------|---------|-----------|-----------|------------");
331for (let i = 0; i < wptQuarterlyScores.length; i++) {
332 const wpt = wptQuarterlyScores[i];
333 const commitQ = quarters[wpt.q];
334 const prevScore = i > 0 ? wptQuarterlyScores[i - 1].score : null;
335 const delta = prevScore !== null ? (wpt.score - prevScore).toFixed(2) : " — ";
336 const commits = commitQ ? commitQ.commits : "?";
337 const perCommit = (commitQ && prevScore !== null) ? ((wpt.score - prevScore) / commitQ.commits).toFixed(4) : "—";
338 console.log(`${wpt.q.padEnd(10)} | ${String(commits).padStart(7)} | ${wpt.score.toFixed(2).padStart(9)} | ${String(delta).padStart(9)} | ${String(perCommit).padStart(12)}`);
339}