import { readFileSync, readdirSync } from 'fs'; import { join } from 'path'; const DATA_DIR = new URL('.', import.meta.url).pathname; const HIST_DIR = join(DATA_DIR, 'historical'); const webFeatures = JSON.parse(readFileSync('/Users/dietrich/misc/web-features/packages/web-features/data.json', 'utf8')); const manifestFile = readdirSync(DATA_DIR).find(f => f.startsWith('WEB_FEATURES_MANIFEST') && f.endsWith('.json')); const wptFeatureManifest = JSON.parse(readFileSync(join(DATA_DIR, manifestFile), 'utf8')); const FULL_THRESHOLD = 0.95; const PARTIAL_THRESHOLD = 0.20; // ============================================================ // Helper: compute feature scores from a WPT summary // ============================================================ function computeFeatureScores(wptSummary) { const scores = {}; for (const [featureId, testPaths] of Object.entries(wptFeatureManifest.data)) { let totalTests = 0, passingTests = 0, totalSubtests = 0, passedSubtests = 0; for (const testPath of testPaths) { const result = wptSummary[testPath]; if (!result) continue; totalTests++; if (result.c && result.c[1] > 0) { passingTests += result.c[0] / result.c[1]; passedSubtests += result.c[0]; totalSubtests += result.c[1]; } else { const pass = result.s === 'O' || result.s === 'P'; if (pass) passingTests += 1; totalSubtests += 1; passedSubtests += pass ? 1 : 0; } } if (totalTests > 0) { scores[featureId] = { score: passingTests / totalTests, subtestRate: totalSubtests > 0 ? passedSubtests / totalSubtests : 0, matchedTests: totalTests }; } } return scores; } // ============================================================ // Helper: compute baseline readiness from feature scores // ============================================================ function computeReadiness(featureScores) { const result = { high: { total: 0, full: 0, partial: 0, none: 0, noData: 0 }, low: { total: 0, full: 0, partial: 0, none: 0, noData: 0 }, all: { total: 0, full: 0, partial: 0, none: 0, noData: 0 } }; for (const [featureName, feature] of Object.entries(webFeatures.features)) { const baseline = feature.status?.baseline === "high" ? "high" : feature.status?.baseline === "low" ? "low" : null; const score = featureScores[featureName]?.score ?? null; for (const bucket of [baseline, "all"].filter(Boolean)) { result[bucket].total++; if (score === null) result[bucket].noData++; else if (score >= FULL_THRESHOLD) result[bucket].full++; else if (score >= PARTIAL_THRESHOLD) result[bucket].partial++; else result[bucket].none++; } // count non-baseline in "all" if not already if (!baseline) { result.all.total++; if (score === null) result.all.noData++; else if (score >= FULL_THRESHOLD) result.all.full++; else if (score >= PARTIAL_THRESHOLD) result.all.partial++; else result.all.none++; } } return result; } // ============================================================ // Load all snapshots // ============================================================ const snapshots = [ { label: "2023-Q3", file: "2023-Q3.json" }, { label: "2024-Q1", file: "2024-Q1.json" }, { label: "2024-Q3", file: "2024-Q3.json" }, { label: "2025-Q1", file: "2025-Q1.json" }, { label: "2025-Q3", file: "2025-Q3.json" }, { label: "2026-Q1 (current)", file: "../servo-wpt-summary.json" }, ]; console.log("=== PART 1: Feature-Level Velocity (WPT-based) ===\n"); console.log("Thresholds: Full >= 95%, Partial >= 20%\n"); const timeSeriesData = []; for (const snap of snapshots) { const filePath = snap.file.startsWith("..") ? join(DATA_DIR, snap.file.replace("../", "")) : join(HIST_DIR, snap.file); const wptSummary = JSON.parse(readFileSync(filePath, 'utf8')); const featureScores = computeFeatureScores(wptSummary); const readiness = computeReadiness(featureScores); timeSeriesData.push({ label: snap.label, readiness, featureScores }); const h = readiness.high; const hPct = (h.full / h.total * 100).toFixed(1); const hPartialPct = ((h.full + h.partial) / h.total * 100).toFixed(1); console.log(`${snap.label}:`); console.log(` Widely Available: ${h.full}/${h.total} full (${hPct}%), ${h.full + h.partial}/${h.total} incl. partial (${hPartialPct}%)`); console.log(` All features: ${readiness.all.full} full, ${readiness.all.partial} partial out of ${readiness.all.total}`); } // ============================================================ // PART 2: Velocity calculation // ============================================================ console.log("\n=== PART 2: Velocity (Features Gained Per Quarter) ===\n"); console.log("Widely Available features fully supported (>= 95%):"); for (let i = 1; i < timeSeriesData.length; i++) { const prev = timeSeriesData[i - 1]; const curr = timeSeriesData[i]; const delta = curr.readiness.high.full - prev.readiness.high.full; const deltaPartial = (curr.readiness.high.full + curr.readiness.high.partial) - (prev.readiness.high.full + prev.readiness.high.partial); console.log(` ${prev.label} -> ${curr.label}: +${delta} full, +${deltaPartial} incl. partial`); } // ============================================================ // PART 3: Per-feature score changes (biggest movers) // ============================================================ console.log("\n=== PART 3: Biggest Score Improvements (Widely Available, 2023-Q3 → current) ===\n"); const first = timeSeriesData[0].featureScores; const last = timeSeriesData[timeSeriesData.length - 1].featureScores; const improvements = []; for (const [featureName, feature] of Object.entries(webFeatures.features)) { if (feature.status?.baseline !== "high") continue; const startScore = first[featureName]?.score ?? 0; const endScore = last[featureName]?.score ?? 0; const delta = endScore - startScore; if (delta > 0) { improvements.push({ name: featureName, start: startScore, end: endScore, delta }); } } improvements.sort((a, b) => b.delta - a.delta); console.log("Top 30 features by score improvement:"); for (const f of improvements.slice(0, 30)) { console.log(` ${f.name}: ${(f.start * 100).toFixed(1)}% -> ${(f.end * 100).toFixed(1)}% (+${(f.delta * 100).toFixed(1)}pp)`); } console.log(`\nTotal features improved: ${improvements.length}`); console.log(`Average improvement: ${(improvements.reduce((s, f) => s + f.delta, 0) / improvements.length * 100).toFixed(1)}pp`); // Features that crossed the full threshold const newlyFull = improvements.filter(f => f.start < FULL_THRESHOLD && f.end >= FULL_THRESHOLD); console.log(`\nFeatures that crossed 95% threshold: ${newlyFull.length}`); for (const f of newlyFull.slice(0, 20)) { console.log(` ${f.name}: ${(f.start * 100).toFixed(1)}% -> ${(f.end * 100).toFixed(1)}%`); } // ============================================================ // PART 4: Commit/contributor velocity context // ============================================================ console.log("\n=== PART 4: Commit & Contributor Context ===\n"); // Monthly data from git analysis (hardcoded from the git extraction) const monthlyData = [ { month: "2023-01", commits: 168, authors: 25 }, { month: "2023-02", commits: 187, authors: 22 }, { month: "2023-03", commits: 197, authors: 22 }, { month: "2023-04", commits: 166, authors: 21 }, { month: "2023-05", commits: 495, authors: 40 }, { month: "2023-06", commits: 279, authors: 27 }, { month: "2023-07", commits: 86, authors: 17 }, { month: "2023-08", commits: 235, authors: 25 }, { month: "2023-09", commits: 151, authors: 15 }, { month: "2023-10", commits: 140, authors: 14 }, { month: "2023-11", commits: 149, authors: 13 }, { month: "2023-12", commits: 110, authors: 12 }, { month: "2024-01", commits: 193, authors: 21 }, { month: "2024-02", commits: 166, authors: 25 }, { month: "2024-03", commits: 323, authors: 34 }, { month: "2024-04", commits: 186, authors: 29 }, { month: "2024-05", commits: 163, authors: 25 }, { month: "2024-06", commits: 157, authors: 24 }, { month: "2024-07", commits: 171, authors: 31 }, { month: "2024-08", commits: 269, authors: 32 }, { month: "2024-09", commits: 217, authors: 26 }, { month: "2024-10", commits: 332, authors: 40 }, { month: "2024-11", commits: 216, authors: 21 }, { month: "2024-12", commits: 216, authors: 29 }, { month: "2025-01", commits: 289, authors: 30 }, { month: "2025-02", commits: 305, authors: 40 }, { month: "2025-03", commits: 317, authors: 59 }, { month: "2025-04", commits: 314, authors: 50 }, { month: "2025-05", commits: 251, authors: 41 }, { month: "2025-06", commits: 350, authors: 45 }, { month: "2025-07", commits: 363, authors: 44 }, { month: "2025-08", commits: 439, authors: 48 }, { month: "2025-09", commits: 341, authors: 42 }, { month: "2025-10", commits: 438, authors: 50 }, { month: "2025-11", commits: 401, authors: 44 }, { month: "2025-12", commits: 340, authors: 52 }, { month: "2026-01", commits: 398, authors: 51 }, { month: "2026-02", commits: 197, authors: 39 }, ]; // Quarterly aggregation const quarters = {}; for (const m of monthlyData) { const [y, mo] = m.month.split("-"); const q = `${y}-Q${Math.ceil(parseInt(mo) / 3)}`; if (!quarters[q]) quarters[q] = { commits: 0, months: 0, authorsSet: new Set(), maxAuthors: 0 }; quarters[q].commits += m.commits; quarters[q].months++; quarters[q].maxAuthors = Math.max(quarters[q].maxAuthors, m.authors); } console.log("Quarterly summary:"); console.log("Quarter | Commits | Peak Authors/Month | Avg Commits/Month"); console.log("-----------|---------|--------------------|-----------------"); for (const [q, d] of Object.entries(quarters).sort()) { const avg = (d.commits / d.months).toFixed(0); console.log(`${q.padEnd(10)} | ${String(d.commits).padStart(7)} | ${String(d.maxAuthors).padStart(18)} | ${String(avg).padStart(17)}`); } // ============================================================ // PART 5: Velocity-based projections // ============================================================ console.log("\n=== PART 5: Projections ===\n"); // Calculate rate of "fully supported" Widely Available features gained per quarter const firstSnap = timeSeriesData[0]; const lastSnap = timeSeriesData[timeSeriesData.length - 1]; const quartersElapsed = (snapshots.length - 1) * 0.5 * 2; // roughly: 2023-Q3 to 2026-Q1 = ~10 quarters // More precise: 2023-Q3 (Jul 2023) to 2026-Q1 (Feb 2026) = ~2.58 years = ~10.3 quarters const yearsElapsed = 2.58; const quartersTotal = yearsElapsed * 4; const fullGained = lastSnap.readiness.high.full - firstSnap.readiness.high.full; const partialGained = (lastSnap.readiness.high.full + lastSnap.readiness.high.partial) - (firstSnap.readiness.high.full + firstSnap.readiness.high.partial); const fullPerQuarter = fullGained / quartersTotal; const partialPerQuarter = partialGained / quartersTotal; console.log(`Over ${yearsElapsed} years (${quartersTotal.toFixed(1)} quarters):`); console.log(` Full features gained: ${fullGained} (${fullPerQuarter.toFixed(1)}/quarter)`); console.log(` Full+Partial gained: ${partialGained} (${partialPerQuarter.toFixed(1)}/quarter)`); // Recent velocity (last ~5 quarters, 2025-Q1 to 2026-Q1) const recentFirst = timeSeriesData[3]; // 2025-Q1 const recentQuarters = 4; // ~4 quarters to current const recentFullGained = lastSnap.readiness.high.full - recentFirst.readiness.high.full; const recentPartialGained = (lastSnap.readiness.high.full + lastSnap.readiness.high.partial) - (recentFirst.readiness.high.full + recentFirst.readiness.high.partial); const recentFullRate = recentFullGained / recentQuarters; const recentPartialRate = recentPartialGained / recentQuarters; console.log(`\nRecent velocity (2025-Q1 to 2026-Q1, ~${recentQuarters} quarters):`); console.log(` Full features gained: ${recentFullGained} (${recentFullRate.toFixed(1)}/quarter)`); console.log(` Full+Partial gained: ${recentPartialGained} (${recentPartialRate.toFixed(1)}/quarter)`); // Projections const currentFull = lastSnap.readiness.high.full; const totalWidelyAvailable = lastSnap.readiness.high.total; const remaining = totalWidelyAvailable - currentFull; console.log(`\nCurrent: ${currentFull}/${totalWidelyAvailable} Widely Available features fully supported`); console.log(`Remaining: ${remaining} features to reach 100%`); for (const [label, rate] of [["Overall average", fullPerQuarter], ["Recent rate", recentFullRate]]) { if (rate <= 0) { console.log(` At ${label} velocity: never (zero or negative rate)`); continue; } const quartersNeeded = remaining / rate; const yearsNeeded = quartersNeeded / 4; const targetYear = 2026.1 + yearsNeeded; // roughly current date console.log(` At ${label} velocity (${rate.toFixed(1)}/quarter): ${quartersNeeded.toFixed(0)} quarters (${yearsNeeded.toFixed(1)} years) → ~${Math.ceil(targetYear)}`); } // What about reaching specific thresholds? console.log("\nProjected dates for milestones:"); for (const target of [0.25, 0.50, 0.75, 1.0]) { const needed = Math.ceil(totalWidelyAvailable * target) - currentFull; if (needed <= 0) { console.log(` ${(target * 100).toFixed(0)}% (${Math.ceil(totalWidelyAvailable * target)} features): ALREADY REACHED`); continue; } for (const [label, rate] of [["overall", fullPerQuarter], ["recent", recentFullRate]]) { if (rate <= 0) continue; const q = needed / rate; const y = 2026.1 + q / 4; console.log(` ${(target * 100).toFixed(0)}% (${Math.ceil(totalWidelyAvailable * target)} features): ${q.toFixed(0)} quarters at ${label} rate → ~${y.toFixed(1)}`); } } // ============================================================ // PART 6: Correlation between commits and WPT improvement // ============================================================ console.log("\n=== PART 6: Commits vs. WPT Improvement Correlation ===\n"); // WPT scores from scores.json data (hardcoded quarterly from the earlier analysis) const wptQuarterlyScores = [ { q: "2023-Q2", score: 30.35 }, { q: "2023-Q3", score: 32.04 }, { q: "2023-Q4", score: 33.24 }, { q: "2024-Q1", score: 34.32 }, { q: "2024-Q2", score: 38.38 }, { q: "2024-Q3", score: 39.83 }, { q: "2024-Q4", score: 42.06 }, { q: "2025-Q1", score: 47.36 }, { q: "2025-Q2", score: 51.34 }, { q: "2025-Q3", score: 55.51 }, { q: "2025-Q4", score: 57.03 }, { q: "2026-Q1", score: 62.38 }, ]; console.log("Quarter | Commits | WPT Score | WPT Delta | Score/Commit"); console.log("-----------|---------|-----------|-----------|------------"); for (let i = 0; i < wptQuarterlyScores.length; i++) { const wpt = wptQuarterlyScores[i]; const commitQ = quarters[wpt.q]; const prevScore = i > 0 ? wptQuarterlyScores[i - 1].score : null; const delta = prevScore !== null ? (wpt.score - prevScore).toFixed(2) : " — "; const commits = commitQ ? commitQ.commits : "?"; const perCommit = (commitQ && prevScore !== null) ? ((wpt.score - prevScore) / commitQ.commits).toFixed(4) : "—"; console.log(`${wpt.q.padEnd(10)} | ${String(commits).padStart(7)} | ${wpt.score.toFixed(2).padStart(9)} | ${String(delta).padStart(9)} | ${String(perCommit).padStart(12)}`); }