SPB Git forge

spb/websensor

Public
33commits 1branches 0releases
3.4 MBsize
maindefault branch
10 days agolast push
TypeScript 55.4% Python 43.2% SQL 1.2%
5.1 KB · 109 lines typescript
Raw Blame History
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