Docs/Skills and code/skill ecosystem-rater
.claude/skills/ecosystem-rater/scripts/lib.mjs
What the file says about itself
Shared helpers for the ecosystem rater: args, paths, read-only DB access, age parsing, robust statistics.
// Shared helpers for the ecosystem rater: args, paths, read-only DB access, age parsing, robust statistics.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import Database from "better-sqlite3";
import { normalizePublishedAt } from "../../../../src/analysis/temporal.js";
export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../..");
export const OUT_ROOT = path.join(ROOT, "ecosystem-ratings");
export const DAY_MS = 86400000;
export function parseArgs(argv) {
const out = { _: [] };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (!a.startsWith("--")) { out._.push(a); continue; }
const eq = a.indexOf("=");
const key = eq > 0 ? a.slice(2, eq) : a.slice(2);
out[key] = eq > 0 ? a.slice(eq + 1) : (argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : true);
}
return out;
}
/** "data/cricket-ecosystem.db" -> "cricket". */
export function ecosystemName(dbPath) {
return path.basename(dbPath).replace(/\.db$/, "").replace(/-ecosystem$/, "");
}
export function listDbs() {
const dir = path.join(ROOT, "data");
return fs.readdirSync(dir).filter(f => f.endsWith(".db")).sort().map(f => path.join(dir, f));
}
export function resolveDb(arg) {
if (!arg || arg === true) throw new Error("--db is required (e.g. --db data/cricket-ecosystem.db), or use --all");
const direct = path.resolve(ROOT, String(arg));
if (fs.existsSync(direct)) return direct;
const byName = listDbs().find(p => ecosystemName(p) === String(arg));
if (byName) return byName;
throw new Error(`database not found: ${arg}`);
}
export function openDb(dbPath) {
return new Database(dbPath, { readonly: true, fileMustExist: true });
}
export function tableExists(db, name) {
return !!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(name);
}
export function outDir(eco) {
const dir = path.join(OUT_ROOT, eco);
fs.mkdirSync(dir, { recursive: true });
return dir;
}
export function readJson(file, fallback = null) {
return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, "utf8")) : fallback;
}
/**
* Age in days at observation time. Uses an absolute publish date when present, otherwise the display text
* ("3 wk ago") anchored to the observation, so the result never depends on when the rater runs.
*/
export function ageAtObservation(publishedAt, publishedText, observedAt) {
const obs = new Date(observedAt).getTime();
if (!Number.isFinite(obs)) return null;
let pub = publishedAt ? new Date(publishedAt).getTime() : NaN;
if (!Number.isFinite(pub) && publishedText) {
const r = normalizePublishedAt(publishedText, observedAt);
pub = r.normalized ? new Date(r.normalized).getTime() : NaN;
}
if (!Number.isFinite(pub)) return null;
return Math.max(1, (obs - pub) / DAY_MS);
}
const finite = a => a.filter(Number.isFinite);
export const median = a => { const s = finite(a).sort((x, y) => x - y); if (!s.length) return null; const m = s.length >> 1; return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2; };
export const mean = a => { const s = finite(a); return s.length ? s.reduce((x, y) => x + y, 0) / s.length : null; };
export const round = (x, d = 3) => (x == null || !Number.isFinite(x) ? null : +x.toFixed(d));
Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.