import { promises as dns } from "node:dns"; import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { join } from "node:path"; import YAML from "yaml"; import { coverageKey, coverageSectorSchema, type CoverageSector } from "@websensor/core"; /** * Coverage-universe validator — no database. * * node node_modules/tsx/dist/cli.mjs apps/engine/src/coverage-validate.ts [config/coverage/.yaml …] [--dns] [--quiet] * * Without arguments every file in config/coverage is checked. Reports schema issues, duplicate domains inside a * file, duplicate sector keys across files and (with --dns) domains that do not resolve. Exit code 1 on any error. */ const args = process.argv.slice(2); const flag = (n: string): boolean => args.includes(`--${n}`); const files = args.filter((a) => !a.startsWith("--")); export function loadCoverageFiles(dir = process.env.WS_COVERAGE_DIR ?? "./config/coverage"): { file: string; sector: CoverageSector | null; issues: string[] }[] { 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)) : []; return list.map((file) => { const issues: string[] = []; let sector: CoverageSector | null = null; try { const raw = YAML.parse(readFileSync(file, "utf8")); const parsed = coverageSectorSchema.safeParse(raw); if (!parsed.success) { for (const i of parsed.error.issues.slice(0, 40)) issues.push(`${i.path.join(".")}: ${i.message}`); if (parsed.error.issues.length > 40) issues.push(`… ${parsed.error.issues.length - 40} more issues`); } else sector = parsed.data; } catch (e) { issues.push(`YAML parse error: ${(e as Error).message}`); } if (sector) { const seen = new Map(); const keys = new Set(); for (const u of sector.universes) { if (keys.has(u.key)) issues.push(`duplicate universe key ${u.key}`); keys.add(u.key); if (!u.members.length) issues.push(`universe ${u.key} has no members`); for (const m of u.members) { if (!/^[a-z0-9.-]+\.[a-z]{2,}$/.test(m.domain) || m.domain.includes("..")) issues.push(`${u.key}: bad domain "${m.domain}" (${m.name})`); const k = coverageKey(m.domain); const prev = seen.get(k); if (prev && prev !== `${u.key}/${m.name}`) issues.push(`${u.key}: duplicate domain ${m.domain} (${m.name}) — already ${prev}`); seen.set(k, `${u.key}/${m.name}`); if (!m.country && !/^(INT|EU)$/.test(String(m.country))) issues.push(`${u.key}: ${m.name} has no country`); } } } return { file, sector, issues }; }); } async function resolves(domain: string): Promise { for (const h of [domain, `www.${domain}`]) { try { const r = await dns.lookup(h, { all: true }); if (r.length) return true; } catch { // try next } } return false; } async function main(): Promise { const loaded = loadCoverageFiles(); if (!loaded.length) { console.error("no coverage files found"); process.exitCode = 1; return; } let errors = 0; const sectors = new Map(); for (const l of loaded) { const members = l.sector?.universes.reduce((n, u) => n + u.members.length, 0) ?? 0; const warnIssues = l.issues.filter((i) => / has no country$/.test(i)); const hardIssues = l.issues.filter((i) => !/ has no country$/.test(i)); errors += hardIssues.length; // Several files may extend the same sector (e.g. corporate indices per region); universes are merged by the loader. if (l.sector && !sectors.has(l.sector.sector)) sectors.set(l.sector.sector, l.file); 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}` : ""}`); for (const i of hardIssues) console.log(` ${i}`); if (!flag("quiet")) for (const i of warnIssues.slice(0, 15)) console.log(` WARN ${i}`); if (flag("dns") && l.sector) { const domains = [...new Set(l.sector.universes.flatMap((u) => u.members.map((m) => m.domain)))]; let i = 0; const bad: string[] = []; await Promise.all( Array.from({ length: 16 }, async () => { while (i < domains.length) { const d = domains[i++]!; if (!(await resolves(d))) bad.push(d); } }), ); for (const d of bad.sort()) console.log(` DNS ${d} does not resolve`); if (bad.length) console.log(` ${bad.length}/${domains.length} domains do not resolve (fix or remove them)`); errors += bad.length; } } const total = loaded.reduce((n, l) => n + (l.sector?.universes.reduce((m, u) => m + u.members.length, 0) ?? 0), 0); console.log(`\n${loaded.length} files · ${sectors.size} sectors · ${total} members → ${errors ? `${errors} errors` : "all good"}`); if (errors) process.exitCode = 1; } if (process.argv[1] && /coverage-validate\.ts$/.test(process.argv[1])) void main();