this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 339 lines 15 kB view raw
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}