diff --git a/package.json b/package.json
index 2f06ee5..6bc38f4 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,7 @@
"type": "module",
"private": true,
"scripts": {
+ "check:dashboard": "node scripts/check-dashboard-source.mjs",
"validate:data": "node scripts/validate-data.mjs",
"check:frontend": "node scripts/check-frontend.mjs"
},
diff --git a/public/index.html b/public/index.html
index 919273c..b791343 100644
--- a/public/index.html
+++ b/public/index.html
@@ -705,7 +705,7 @@
Bitcoin Quantum Readiness${comp.current_level}
-
${voiced.score} / 100 voiced urgency
(among developers who voiced a position)
${voiced.voices_count} of ${data.length} developers have voiced positions
+
${voiced.score} / 100 voiced urgency
(among developers who voiced a position)
${voiced.voices_count} of ${total} developers have voiced positions
${coverage.score}% coverage
${coverage.silent} of ${coverage.total} developers have said nothing
${comp.formula}
@@ -1200,10 +1200,11 @@
Ecosystem
// Ranked devs only (not notable)
const ranked = devs.filter(d => !d.notable && d.rank >= 1);
+ const rankMax = Math.max(...ranked.map(d => d.rank), 1);
bubbles = [];
ranked.forEach(dev => {
- const x = padL + ((dev.rank - 1) / (total - 1)) * plotW;
+ const x = padL + ((dev.rank - 1) / Math.max(rankMax - 1, 1)) * plotW;
const y = padT + plotH - ((dev.quantum_urgency_score - 1) / 4) * plotH;
const vol = dev.pq_work_volume || 0;
const r = Math.max(6, Math.min(18, 6 + vol * 2.5));
diff --git a/scripts/check-dashboard-source.mjs b/scripts/check-dashboard-source.mjs
new file mode 100644
index 0000000..843a438
--- /dev/null
+++ b/scripts/check-dashboard-source.mjs
@@ -0,0 +1,37 @@
+#!/usr/bin/env node
+import fs from "fs";
+
+const html = fs.readFileSync(new URL("../public/index.html", import.meta.url), "utf8");
+const errors = [];
+
+function assert(condition, message) {
+ if (!condition) errors.push(message);
+}
+
+assert(
+ !html.includes("${data.length} developers have voiced positions"),
+ "dashboard must not read data.length; the dataset is an object with metadata/developers",
+);
+
+assert(
+ html.includes("${voiced.voices_count} of ${total} developers have voiced positions"),
+ "voiced-position copy should use metadata.total_assessed",
+);
+
+assert(
+ html.includes("const rankMax = Math.max(...ranked.map(d => d.rank), 1);"),
+ "bubble chart should compute its influence scale from ranked entries",
+);
+
+assert(
+ html.includes("Math.max(rankMax - 1, 1)"),
+ "bubble chart should scale x positions against the max ranked position, not total assessed entries",
+);
+
+if (errors.length) {
+ console.error(`check-dashboard-source failed with ${errors.length} error(s):`);
+ for (const error of errors) console.error(`- ${error}`);
+ process.exit(1);
+}
+
+console.log("check-dashboard-source passed");