Docs/Skills and code/skill ecosystem-rater

.claude/skills/ecosystem-rater/scripts/lib.mjs

Code81 lines3.3 KBSkill: ecosystem-rater

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.