[object Object]

← back to Approval Verifier

auto-save: 2026-06-24T17:58:43 (2 files) — verify.mjs ledger.mjs

47d898658bd50eef89aabcd198449f61ef279911 · 2026-06-24 17:58:46 -0700 · Steve Abrams

Files touched

Diff

commit 47d898658bd50eef89aabcd198449f61ef279911
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jun 24 17:58:46 2026 -0700

    auto-save: 2026-06-24T17:58:43 (2 files) — verify.mjs ledger.mjs
---
 ledger.mjs | 135 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 verify.mjs |  26 +++++++++++-
 2 files changed, 159 insertions(+), 2 deletions(-)

diff --git a/ledger.mjs b/ledger.mjs
new file mode 100644
index 0000000..cd41273
--- /dev/null
+++ b/ledger.mjs
@@ -0,0 +1,135 @@
+#!/usr/bin/env node
+// approvals ledger — the single canonical source of truth for "following approvals".
+// DTD-A (2026-06-24, 3/3): one local approvals.json that every tool reads/writes;
+// CNCP renders a board from it; the yolo loop reads status; verify.mjs writes
+// landed/failed back. Collapses the 4 drifting state sources into one file.
+//
+// Usage:
+//   node ledger.mjs            # build/refresh approvals.json, then print the board
+//   node ledger.mjs --build    # build/refresh only
+//   node ledger.mjs --board    # print board from existing approvals.json (no rebuild)
+//   node ledger.mjs --json     # dump approvals.json to stdout
+//   node ledger.mjs --status drafted,stale   # board filtered to statuses
+
+import { readFileSync, writeFileSync, readdirSync, existsSync, statSync } from "node:fs";
+import { homedir } from "node:os";
+import { join, basename } from "node:path";
+
+const HOME = homedir();
+const QUEUE = join(HOME, ".claude/yolo-queue");
+const LEDGER = join(QUEUE, "approvals.json");
+const DAY = 86400000;
+const STALE_DAYS = 21;
+
+const args = process.argv.slice(2);
+const flag = (n) => args.includes(n);
+const opt = (n, d) => { const i = args.indexOf(n); return i >= 0 && args[i + 1] ? args[i + 1] : d; };
+
+// directory → base lifecycle status
+const DIRS = {
+  "pending-approval": "drafted",
+  "tasks": "approved",      // promoted into the run queue
+  "done": "executed",       // processed — needs verify to become 'landed'
+  "failed": "failed",
+};
+
+// --- classification heuristics (title + body) ---
+const CATS = [
+  ["fleet", /\bfleet|registry|watchdog|pm2|launchd|reconcile|keepalive|keep-alive|zombie|dark\b/i],
+  ["secrets", /\bsecret|credential|\bcred\b|rotate|token|password|\.env\b/i],
+  ["shopify", /\bshopify|theme|liquid|metafield|\bsku\b|product|variant|cadence|storefront\b/i],
+  ["dns", /\bdns|domain|cloudflare|nameserver|\bns\b|certbot|ssl\b/i],
+  ["email", /\bemail|purelymail|gmail|george|\bmx\b|dkim|dmarc|mailbox\b/i],
+  ["catalog", /\bcatalog|scrape|vendor|import|dedup|enrich\b/i],
+  ["cncp", /\bcncp|approval|dashboard\b/i],
+];
+// Explicit safety markers the memos use in their TITLES — strong low-risk signal.
+const SAFE = /\bREAD-?ONLY|DRAFTS? ONLY|propose|proposal|\baudit\b|dry.?run|report only|never (send|push|publish|delete)|SAMPLE-GATE|recommend\b/i;
+const HIGH = /\b(delete|deleting|\brm\b|\bdrop\b|rotate|\bprod\b|dw_unified|sudo|publish|push to (live|main)|send to|mailer|blast|nameserver|customer-facing|irreversible|\bforce\b)\b/i;
+const MED = /\b(install|launchctl|bootstrap|bootout|restart|pm2 (start|stop|delete|restart)|activate|enable|migrate|\bwire\b|stop\b|archive|retire)\b/i;
+const REVERSIBLE_BAD = /\b(delete|deleting|\brm\b|\bdrop\b|rotate|publish|push to (live|main)|send to|mailer|blast)\b/i;
+
+// classify on the TITLE (curated + concise) — body is too noisy and over-flags.
+function classify(title, body) {
+  const category = (CATS.find(([, re]) => re.test(title)) || ["other"])[0];
+  const safe = SAFE.test(title);
+  const risk = safe ? "low" : HIGH.test(title) ? "high" : MED.test(title) ? "med" : "low";
+  const reversible = safe || !REVERSIBLE_BAD.test(title);
+  const verifyM = body.match(/<!--\s*VERIFY:\s*([^]*?)-->/) || body.match(/```verify\s*([^]*?)```/);
+  const verify = verifyM ? verifyM[1].trim().split("\n")[0].trim() : null;
+  return { category, risk, reversible, verify };
+}
+
+function scanDir(dir, baseStatus) {
+  const p = join(QUEUE, dir);
+  if (!existsSync(p)) return [];
+  return readdirSync(p).filter(f => f.endsWith(".md")).map(f => {
+    const fp = join(p, f);
+    const body = readFileSync(fp, "utf8");
+    const title = (body.match(/^#[^#].*/m) || [f])[0].replace(/^#+\s*/, "").slice(0, 100);
+    const mtime = statSync(fp).mtime;
+    const age_days = Math.floor((Date.now() - mtime.getTime()) / DAY);
+    const { category, risk, reversible, verify } = classify(title, body);
+    let status = baseStatus;
+    if (status === "drafted" && age_days > STALE_DAYS) status = "stale";
+    return {
+      id: basename(f, ".md"), title, category, risk, reversible,
+      created: mtime.toISOString(), age_days, verify,
+      status, location: dir, last_checked: null, evidence: [],
+    };
+  });
+}
+
+function build() {
+  // preserve prior verify results (last_checked/evidence/landed) when refreshing
+  let prior = {};
+  if (existsSync(LEDGER)) { try { for (const e of JSON.parse(readFileSync(LEDGER, "utf8")).entries) prior[e.id] = e; } catch {} }
+  const entries = [];
+  for (const [dir, st] of Object.entries(DIRS)) {
+    for (const e of scanDir(dir, st)) {
+      const p = prior[e.id];
+      if (p && p.last_checked) { e.last_checked = p.last_checked; e.evidence = p.evidence;
+        if (["landed", "failed", "no-op"].includes(p.status) && e.status === "executed") e.status = p.status; }
+      entries.push(e);
+    }
+  }
+  const ledger = { generated: new Date().toISOString(), source: QUEUE.replace(HOME, "~"),
+    counts: entries.reduce((a, e) => (a[e.status] = (a[e.status] || 0) + 1, a), {}), entries };
+  writeFileSync(LEDGER, JSON.stringify(ledger, null, 2));
+  return ledger;
+}
+
+function board(ledger, statuses) {
+  const C = { drafted: "\x1b[37m", stale: "\x1b[90m", approved: "\x1b[36m", executed: "\x1b[36m",
+    landed: "\x1b[32m", failed: "\x1b[31m", "no-op": "\x1b[33m" };
+  const R = { high: "\x1b[31mHIGH\x1b[0m", med: "\x1b[33mmed \x1b[0m", low: "\x1b[32mlow \x1b[0m" };
+  const NC = "\x1b[0m";
+  // default view = the ACTIONABLE backlog (awaiting Steve), not the done/ history
+  const DEFAULT = ["drafted", "stale"];
+  const want = statuses?.length ? statuses : DEFAULT;
+  let rows = ledger.entries.filter(e => want.includes(e.status));
+  const order = { high: 0, med: 1, low: 2 };
+  rows.sort((a, b) => order[a.risk] - order[b.risk] || b.age_days - a.age_days);
+  console.log(`\n📋 approvals board — ${ledger.entries.length} items  (${ledger.generated.slice(0,16)})\n`);
+  console.log(`  ${"status".padEnd(9)} ${"risk".padEnd(4)} rev vfy ${"cat".padEnd(8)} age  title`);
+  console.log("  " + "─".repeat(86));
+  for (const e of rows.slice(0, 60)) {
+    console.log(`  ${C[e.status]||""}${e.status.padEnd(9)}${NC} ${R[e.risk]} ${e.reversible?" ✓ ":" ✗ "} ${e.verify?"✓":"·"}  ${e.category.padEnd(8)} ${String(e.age_days).padStart(3)}d ${e.title.slice(0,46)}`);
+  }
+  console.log("\n── by status ──");
+  for (const [k, v] of Object.entries(ledger.counts).sort()) console.log(`  ${C[k]||""}${k.padEnd(9)}${NC} ${v}`);
+  // the safe-to-batch bucket
+  const safe = ledger.entries.filter(e => ["drafted"].includes(e.status) && e.risk === "low" && e.reversible);
+  const verifiable = ledger.entries.filter(e => e.verify).length;
+  console.log(`\n  ✅ safe-batch (drafted · low-risk · reversible): ${safe.length}`);
+  console.log(`  🔍 machine-verifiable (has VERIFY line): ${verifiable}/${ledger.entries.length}  ← the rest need a VERIFY line or manual check`);
+  console.log(`\n  rev=reversible  vfy=has VERIFY line.  Approve the safe-batch bucket first; never "approve all".`);
+}
+
+// --- run ---
+let ledger;
+if (flag("--board") && existsSync(LEDGER)) ledger = JSON.parse(readFileSync(LEDGER, "utf8"));
+else ledger = build();
+if (flag("--json")) { console.log(JSON.stringify(ledger, null, 2)); process.exit(0); }
+if (flag("--build")) { console.log(`built ${LEDGER.replace(HOME, "~")} — ${ledger.entries.length} entries`); process.exit(0); }
+board(ledger, (opt("--status", "") || "").split(",").map(s => s.trim()).filter(Boolean));
diff --git a/verify.mjs b/verify.mjs
index 102ba93..534af75 100644
--- a/verify.mjs
+++ b/verify.mjs
@@ -28,7 +28,7 @@
 //   --done     also scan ~/.claude/yolo-queue/done
 // Writes a JSONL ledger to ~/.claude/yolo-queue/.approval-verify-ledger.jsonl
 
-import { readFileSync, readdirSync, existsSync, appendFileSync, statSync } from "node:fs";
+import { readFileSync, writeFileSync, readdirSync, existsSync, appendFileSync, statSync } from "node:fs";
 import { execSync } from "node:child_process";
 import { homedir } from "node:os";
 import { join, basename } from "node:path";
@@ -175,8 +175,30 @@ if (wantJson) {
   console.log(`UNVERIFIABLE/OBSERVED = needs a VERIFY block or manual check (add  <!-- VERIFY: <shell test> -->  to the memo).`);
 }
 
-// append ledger (skip in single/json ad-hoc mode)
+// append run ledger (skip in single/json ad-hoc mode)
 if (!single && !wantJson) {
   const ts = new Date().toISOString();
   try { appendFileSync(LEDGER, rows.map(r => JSON.stringify({ ts, name: r.name, status: r.status, conf: r.conf })).join("\n") + "\n"); } catch {}
 }
+
+// --write-ledger: merge verify results back into the canonical approvals.json (closes the loop).
+// Only flips POST-approval entries (executed/approved) — never a 'drafted' item.
+if (flag("--write-ledger")) {
+  const APPROVALS = join(QUEUE, "approvals.json");
+  const MAP = { LANDED: "landed", "LIKELY-LANDED": "landed", FAILED: "failed", "NO-OP": "no-op", "LIKELY-NOOP": "no-op" };
+  if (existsSync(APPROVALS)) {
+    try {
+      const L = JSON.parse(readFileSync(APPROVALS, "utf8"));
+      const byId = new Map(rows.map(r => [r.name, r]));
+      let n = 0;
+      for (const e of L.entries) {
+        const r = byId.get(e.id); if (!r) continue;
+        e.last_checked = new Date().toISOString(); e.evidence = r.evidence;
+        if (MAP[r.status] && ["executed", "approved"].includes(e.status)) { e.status = MAP[r.status]; n++; }
+      }
+      L.counts = L.entries.reduce((a, e) => (a[e.status] = (a[e.status] || 0) + 1, a), {});
+      writeFileSync(APPROVALS, JSON.stringify(L, null, 2));
+      console.log(`\n↪ wrote verify results into approvals.json (${n} status transitions)`);
+    } catch (e) { console.error("ledger write failed:", e.message); }
+  } else console.error("approvals.json not found — run `node ledger.mjs --build` first");
+}

← ce0abc9 approval-verifier: per-item end-to-end completion checker fo  ·  back to Approval Verifier  ·  (newest)