← back to Approvals Viewer

freshness-guard-cli.js

82 lines

#!/usr/bin/env node
'use strict';
// freshness-guard-cli.js — run the READ-ONLY approval-time freshness re-check.
//
//   node freshness-guard-cli.js <memo.md>        # one memo (path or queue basename)
//   node freshness-guard-cli.js --all            # every open memo in the queue
//   node freshness-guard-cli.js --all --stale    # only memos flagged STALE
//   node freshness-guard-cli.js <memo> --json    # machine-readable
//
// It PRINTS an advisory. It NEVER approves, rejects, edits, moves, or executes
// anything. A STALE line means "re-enumerate before you fire," not "blocked."

const fs = require('fs');
const path = require('path');
const G = require('./freshness-guard');

const args = process.argv.slice(2);
const asJson = args.includes('--json');
const staleOnly = args.includes('--stale');
const all = args.includes('--all');
const target = args.find((a) => !a.startsWith('--'));

function resolveMemo(t) {
  if (!t) return null;
  if (fs.existsSync(t)) return t;
  const inQueue = path.join(G.defaultConfig().queueDir, t);
  if (fs.existsSync(inQueue)) return inQueue;
  return t; // let checkMemo report NOT_MEASURED
}

const BADGE = { PASS: 'PASS', WARN: 'WARN', FAIL: 'FAIL' };
function line(r) {
  const parts = [`[${BADGE[r.status] || r.status}]`, r.verdict.padEnd(11), r.file];
  if (r.verdict === 'STALE') parts.push(`— ${r.staleCount}/${r.targetCount} stale`);
  else if (r.verdict === 'FRESH') parts.push(`— 0/${r.targetCount} touched`);
  return parts.join(' ');
}

function detail(r) {
  const out = [line(r)];
  if (r.headline) out.push('    ' + r.headline);
  if (r.reason) out.push('    ' + r.reason);
  if (r.staleTargets) {
    for (const s of r.staleTargets) {
      out.push(`    • ${s.type}:${s.target}  [${s.worst}]`);
      for (const h of s.hits.slice(0, 4)) {
        out.push(`        ↳ ${h.reason} — ${h.source}${h.ticket ? ' (' + h.ticket + ')' : ''} @ ${h.ts}`);
        if (h.snippet) out.push(`            "${h.snippet}"`);
      }
    }
  }
  if (r.reverifyHint) out.push('    RE-ENUMERATE (run this yourself, read-only): ' + r.reverifyHint);
  return out.join('\n');
}

let reports;
if (all) reports = G.checkQueue();
else if (target) reports = [G.checkMemo(resolveMemo(target))];
else {
  console.error('usage: freshness-guard-cli.js <memo.md> | --all [--stale] [--json]');
  process.exit(2);
}

if (staleOnly) reports = reports.filter((r) => r.verdict === 'STALE');

if (asJson) {
  console.log(JSON.stringify(all ? reports : reports[0], null, 2));
} else {
  const stale = reports.filter((r) => r.verdict === 'STALE');
  if (all) {
    for (const r of reports.sort((a, b) => (a.verdict === 'STALE' ? -1 : 1) - (b.verdict === 'STALE' ? -1 : 1))) {
      console.log(r.verdict === 'STALE' ? detail(r) : line(r));
    }
    console.log(`\n${reports.length} memos checked · ${stale.length} STALE (advisory — re-enumerate before firing those).`);
  } else {
    console.log(detail(reports[0]));
  }
}

// exit 0 always: advisory tool, a STALE finding is not a failure of the guard.
process.exit(0);