TypeScript 55.4%
Python 43.2%
SQL 1.2%
1import { promises as dns } from "node:dns";2import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";3import { join } from "node:path";4import YAML from "yaml";5import { coverageKey, coverageSectorSchema, type CoverageSector } from "@websensor/core";67/**8 * Coverage-universe validator — no database.9 *10 * node node_modules/tsx/dist/cli.mjs apps/engine/src/coverage-validate.ts [config/coverage/<file>.yaml …] [--dns] [--quiet]11 *12 * Without arguments every file in config/coverage is checked. Reports schema issues, duplicate domains inside a13 * file, duplicate sector keys across files and (with --dns) domains that do not resolve. Exit code 1 on any error.14 */15const args = process.argv.slice(2);16const flag = (n: string): boolean => args.includes(`--${n}`);17const files = args.filter((a) => !a.startsWith("--"));1819export function loadCoverageFiles(dir = process.env.WS_COVERAGE_DIR ?? "./config/coverage"): { file: string; sector: CoverageSector | null; issues: string[] }[] {20 const list = files.length ? files : existsSync(dir) && statSync(dir).isDirectory() ? readdirSync(dir).filter((f) => /\.ya?ml$/.test(f) && !f.startsWith("_")).sort().map((f) => join(dir, f)) : [];21 return list.map((file) => {22 const issues: string[] = [];23 let sector: CoverageSector | null = null;24 try {25 const raw = YAML.parse(readFileSync(file, "utf8"));26 const parsed = coverageSectorSchema.safeParse(raw);27 if (!parsed.success) {28 for (const i of parsed.error.issues.slice(0, 40)) issues.push(`${i.path.join(".")}: ${i.message}`);29 if (parsed.error.issues.length > 40) issues.push(`… ${parsed.error.issues.length - 40} more issues`);30 } else sector = parsed.data;31 } catch (e) {32 issues.push(`YAML parse error: ${(e as Error).message}`);33 }34 if (sector) {35 const seen = new Map<string, string>();36 const keys = new Set<string>();37 for (const u of sector.universes) {38 if (keys.has(u.key)) issues.push(`duplicate universe key ${u.key}`);39 keys.add(u.key);40 if (!u.members.length) issues.push(`universe ${u.key} has no members`);41 for (const m of u.members) {42 if (!/^[a-z0-9.-]+\.[a-z]{2,}$/.test(m.domain) || m.domain.includes("..")) issues.push(`${u.key}: bad domain "${m.domain}" (${m.name})`);43 const k = coverageKey(m.domain);44 const prev = seen.get(k);45 if (prev && prev !== `${u.key}/${m.name}`) issues.push(`${u.key}: duplicate domain ${m.domain} (${m.name}) — already ${prev}`);46 seen.set(k, `${u.key}/${m.name}`);47 if (!m.country && !/^(INT|EU)$/.test(String(m.country))) issues.push(`${u.key}: ${m.name} has no country`);48 }49 }50 }51 return { file, sector, issues };52 });53}5455async function resolves(domain: string): Promise<boolean> {56 for (const h of [domain, `www.${domain}`]) {57 try {58 const r = await dns.lookup(h, { all: true });59 if (r.length) return true;60 } catch {61 // try next62 }63 }64 return false;65}6667async function main(): Promise<void> {68 const loaded = loadCoverageFiles();69 if (!loaded.length) {70 console.error("no coverage files found");71 process.exitCode = 1;72 return;73 }74 let errors = 0;75 const sectors = new Map<string, string>();76 for (const l of loaded) {77 const members = l.sector?.universes.reduce((n, u) => n + u.members.length, 0) ?? 0;78 const warnIssues = l.issues.filter((i) => / has no country$/.test(i));79 const hardIssues = l.issues.filter((i) => !/ has no country$/.test(i));80 errors += hardIssues.length;81 // Several files may extend the same sector (e.g. corporate indices per region); universes are merged by the loader.82 if (l.sector && !sectors.has(l.sector.sector)) sectors.set(l.sector.sector, l.file);83 console.log(`${hardIssues.length ? "FAIL" : "OK "} ${l.file} sector=${l.sector?.sector ?? "-"} universes=${l.sector?.universes.length ?? 0} members=${members}${warnIssues.length ? ` warn=${warnIssues.length}` : ""}`);84 for (const i of hardIssues) console.log(` ${i}`);85 if (!flag("quiet")) for (const i of warnIssues.slice(0, 15)) console.log(` WARN ${i}`);86 if (flag("dns") && l.sector) {87 const domains = [...new Set(l.sector.universes.flatMap((u) => u.members.map((m) => m.domain)))];88 let i = 0;89 const bad: string[] = [];90 await Promise.all(91 Array.from({ length: 16 }, async () => {92 while (i < domains.length) {93 const d = domains[i++]!;94 if (!(await resolves(d))) bad.push(d);95 }96 }),97 );98 for (const d of bad.sort()) console.log(` DNS ${d} does not resolve`);99 if (bad.length) console.log(` ${bad.length}/${domains.length} domains do not resolve (fix or remove them)`);100 errors += bad.length;101 }102 }103 const total = loaded.reduce((n, l) => n + (l.sector?.universes.reduce((m, u) => m + u.members.length, 0) ?? 0), 0);104 console.log(`\n${loaded.length} files · ${sectors.size} sectors · ${total} members → ${errors ? `${errors} errors` : "all good"}`);105 if (errors) process.exitCode = 1;106}107108if (process.argv[1] && /coverage-validate\.ts$/.test(process.argv[1])) void main();109