● Zion Boggan
repos/Docket/lint/neutrality.mjs
zionboggan.com ↗
97 lines · javascript
History for this file →
1
#!/usr/bin/env node
2
 
3
import fs from "node:fs";
4
import path from "node:path";
5
 
6
const ROOT = path.resolve(new URL("..", import.meta.url).pathname);
7
 
8
const BANNED = [
9
  "slashed", "gutted", "slammed", "blasted", "massive", "tiny", "huge",
10
  "sweeping", "radical", "extreme", "controversial", "popular", "unpopular",
11
  "landmark", "historic", "devastating", "crippling", "generous", "stingy",
12
  "bloated", "wasteful", "common-sense", "partisan", "crucial", "vital",
13
  "narrowly", "barely", "overwhelmingly", "finally", "surprisingly", "key",
14
];
15
 
16
const ALLOW_HOSTS = [
17
  "congress.gov", "api.congress.gov", "govinfo.gov", "usaspending.gov",
18
  "fiscaldata.treasury.gov", "cbo.gov", "gpo.gov", "clerk.house.gov",
19
  "senate.gov", "openstates.org", "house.gov", "whitehouse.gov",
20
];
21
 
22
const ALLOW_HOST_RE = /(^|\.)gov$|\.gov$/i;
23
 
24
const SCANS = [
25
  "lib/mock-data.ts",
26
  "app/register/page.tsx",
27
  "app/bill/[id]/page.tsx",
28
  "app/spending/page.tsx",
29
  "app/states/page.tsx",
30
  "app/how-we-pick/page.tsx",
31
  "app/who-pays/page.tsx",
32
  "components/site-footer.tsx",
33
];
34
 
35
let violations = 0;
36
 
37
function checkAllowed(url, file) {
38
  let host;
39
  try {
40
    host = new URL(url).host.toLowerCase().replace(/^www\./, "");
41
  } catch {
42
    violations++;
43
    console.log(`  โœ— ${file}: malformed sourceUrl "${url}"`);
44
    return;
45
  }
46
  const ok = ALLOW_HOST_RE.test(host) || ALLOW_HOSTS.includes(host);
47
  if (!ok) {
48
    violations++;
49
    console.log(`  โœ— ${file}: source host "${host}" not allowlisted`);
50
  }
51
}
52
 
53
console.log("Docket neutrality lint");
54
console.log("----------------------");
55
 
56
const bannedRe = new RegExp(`\\b(${BANNED.join("|")})\\b`, "gi");
57
console.log("\n#2 no adjectives");
58
for (const rel of SCANS) {
59
  const file = path.join(ROOT, rel);
60
  if (!fs.existsSync(file)) {
61
    console.log(`  - ${rel}: skipped (not found)`);
62
    continue;
63
  }
64
  const text = fs.readFileSync(file, "utf8");
65
  let m;
66
  while ((m = bannedRe.exec(text))) {
67
    if (/BANNED|banned/i.test(text.slice(Math.max(0, m.index - 60), m.index))) {
68
      continue;
69
    }
70
    violations++;
71
    const line = text.slice(0, m.index).split("\n").length;
72
    console.log(`  โœ— ${rel}:${line} banned word "${m[1]}"`);
73
  }
74
  console.log(`  - ${rel}: scanned`);
75
}
76
 
77
console.log("\n#1 government sources");
78
const dataFile = path.join(ROOT, "lib/mock-data.ts");
79
if (fs.existsSync(dataFile)) {
80
  const text = fs.readFileSync(dataFile, "utf8");
81
  const urlRe = /sourceUrl:\s*"([^"]+)"/g;
82
  let m;
83
  let count = 0;
84
  while ((m = urlRe.exec(text))) {
85
    checkAllowed(m[1], "lib/mock-data.ts");
86
    count++;
87
  }
88
  console.log(`  - checked ${count} sourceUrl values`);
89
}
90
 
91
console.log("\n----------------------");
92
if (violations === 0) {
93
  console.log("PASS - 0 neutrality violations");
94
  process.exit(0);
95
}
96
console.log(`FAIL - ${violations} neutrality violation(s)`);
97
process.exit(1);