← back to Approvals Viewer
freshness-guard.js
451 lines
'use strict';
// freshness-guard.js — READ-ONLY, ADVISORY approval-time freshness re-check.
//
// TK-11685 (governance): an approval was granted at 18:26Z for a delete list a
// sibling lane had already invalidated at 15:43Z the SAME day, because nothing
// joined a fresh enumeration against concurrent findings before the memo reached
// Steve. This module closes that gap.
//
// Given a gated memo that carries an ENUMERATED TARGET SET (Shopify variant/product
// ids, DW-SKUs, GMC offer-ids, /products/ handles), it cross-references EVERY newer
// concurrent finding — sibling & filed memos across the whole queue, the
// executed-reversible ledger (already-done), and the decision logs — and flags any
// target that a finding NEWER than the memo's own as-of has since touched
// (executed / superseded / re-classified). It answers ONE question:
//
// "N of M targets are now stale — do NOT execute this delete list blind."
//
// HARD RAILS (never violated by this file):
// * READ-ONLY. It only reads files. It NEVER writes, moves, edits, approves,
// rejects, or executes any memo, delete, or target action.
// * ADVISORY. It annotates/warns. The human still decides. A STALE verdict is a
// "re-enumerate before you fire" nudge, not an auto-reject.
// * No network, no shell-out, no eval. It does NOT run the memo's re-verify
// command (that would be arbitrary code from a parsed file) — it SURFACES the
// command for the human to run.
//
// Verdict vocabulary (fleet-health-rollup compatible): PASS / WARN / FAIL.
// FRESH -> PASS (0 targets touched by newer findings)
// STALE -> WARN (>=1 target touched by a newer finding — advisory, human decides)
// NO_TARGETS -> PASS (memo carries no enumerated target set; guard is N/A here)
// NOT_MEASURED-> WARN (memo unreadable / no parseable as-of time — never a false PASS,
// per the "an unmeasured input is never PASS" rule)
const fs = require('fs');
const path = require('path');
const HOME = process.env.HOME || require('os').homedir();
const QUEUE = path.join(HOME, '.claude/yolo-queue/pending-approval');
// Subdirs of the queue whose memos count as concurrent findings. A memo that has
// been FILED (moved out of pending-approval into one of the "resolved" dirs) is the
// strongest paper signal that its targets were dealt with.
const FILED_DIRS = ['_done', '_resolved', '_superseded', '_approved', '_rejected', '_held', '_parked'];
function defaultConfig() {
return {
// the memo under review lives here; siblings here are OPEN concurrent findings
queueDir: QUEUE,
// memos here are FILED concurrent findings (already dispositioned)
filedDirs: FILED_DIRS.map((d) => path.join(QUEUE, d)),
// the append-only ledger of already-executed reversible actions (already-done)
ledgerPaths: [path.join(HOME, '.claude/yolo-queue/executed-reversible/ledger.jsonl')],
// decision logs — a related memo already approved/rejected
decisionLogs: [
path.join(QUEUE, '_decisions.jsonl'),
path.join(QUEUE, '_review-decisions.jsonl'),
],
// recurse one level into filed dirs (they nest by date/batch sometimes)
recurseDepth: 2,
};
}
// ---------------------------------------------------------------------------
// Anchor extraction — the JOIN keys. High-precision identifiers only, so a shared
// anchor between a memo and a newer finding is meaningful, not a coincidence.
// ---------------------------------------------------------------------------
// Only UNAMBIGUOUS product-target identifiers count as anchors. Precision matters:
// a shared anchor between a memo and a newer finding must mean "same target", not a
// coincidental infra name. So we deliberately do NOT anchor on backticked handle-ish
// tokens (they catch skill/canary names — fleet-health-rollup, dw-backup-canary,
// git-filter-repo — and the store domain, which are prose, not delete targets).
const RE_LONG_ID = /\b\d{11,}\b/g; // Shopify variant/product ids, bare GMC offer ids
const RE_DWSKU = /\bDW[A-Z]{2,4}-\d{3,}\b/gi; // DW SKU tokens
const RE_OFFER = /\bshopify_[A-Za-z]{2}_\d+_\d+\b/g; // GMC structured offer ids
const RE_HANDLE_CTX = /(?:\/products\/|[?&]variant=|handle[=:]\s*)([a-z0-9][a-z0-9-]{3,})/g; // handle in a target context
// Handles that are infrastructure, not products — never a delete target.
const HANDLE_STOPWORDS = new Set([
'designer-laboratory-sandbox',
'fleet-health-rollup',
'dw-backup-canary',
'git-filter-repo',
]);
function extractAnchors(text) {
const anchors = new Map(); // normalizedAnchor -> type
const add = (raw, type) => {
if (!raw) return;
const key = String(raw).toUpperCase();
if (!anchors.has(key)) anchors.set(key, type);
};
let m;
RE_LONG_ID.lastIndex = 0;
while ((m = RE_LONG_ID.exec(text))) add(m[0], 'id');
RE_DWSKU.lastIndex = 0;
while ((m = RE_DWSKU.exec(text))) add(m[0], 'sku');
RE_OFFER.lastIndex = 0;
while ((m = RE_OFFER.exec(text))) add(m[0], 'offer');
RE_HANDLE_CTX.lastIndex = 0;
while ((m = RE_HANDLE_CTX.exec(text))) {
if (!HANDLE_STOPWORDS.has(m[1].toLowerCase())) add(m[1], 'handle');
}
return anchors;
}
// ---------------------------------------------------------------------------
// Timestamps. A memo's "as-of" is the most recent moment its content was authored
// or re-verified (an addendum re-verify counts). A finding invalidates only if it
// is NEWER than that — findings the memo already accounted for do not fire.
// ---------------------------------------------------------------------------
const RE_ISO = /\b(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)\b/g;
const RE_DATE_ONLY = /\b(\d{4}-\d{2}-\d{2})\b/g;
function parseIsoTimes(text) {
const out = [];
let m;
RE_ISO.lastIndex = 0;
while ((m = RE_ISO.exec(text))) {
const d = new Date(m[1]);
if (!isNaN(+d)) out.push(d);
}
return out;
}
const RE_DRAFTED = /(?:\*\*)?(?:Drafted|Written|Date|Authored|Created)(?:\*\*)?[:\s]+.*?(\d{4}-\d{2}-\d{2})/i;
// Returns { asOf: Date|null, precise: bool }. precise=false means we only had a
// date (no time) — the caller treats that as NOT fully measured.
//
// CLAMP TO now: a timestamp in the FUTURE is never an authoring/enumeration event —
// it is a scheduled/expiry reference (e.g. a GMC googleExpirationDate 2026-10-11).
// If we let a future date become the as-of, every real finding looks OLDER than the
// memo and the memo goes falsely FRESH. So we only consider timestamps <= now.
function memoAsOf(body, filenameDate, now) {
now = now || new Date();
const times = parseIsoTimes(body).filter((t) => t <= now);
if (times.length) {
return { asOf: new Date(Math.max(...times.map(Number))), precise: true };
}
// date-only: prefer the memo's own DRAFTED/WRITTEN date (its enumeration date),
// then the filename date, then the earliest date mentioned. Using the authored
// date (not the oldest context date) avoids flagging findings the memo already
// accounted for; the filename/earliest fallbacks still err toward flagging.
const dr = RE_DRAFTED.exec(body);
if (dr) {
const d = new Date(dr[1] + 'T00:00:00Z');
if (!isNaN(+d) && d <= now) return { asOf: d, precise: false };
}
if (filenameDate) {
const d = new Date(filenameDate + 'T00:00:00Z');
if (!isNaN(+d) && d <= now) return { asOf: d, precise: false };
}
const dm = [];
let m;
RE_DATE_ONLY.lastIndex = 0;
while ((m = RE_DATE_ONLY.exec(body))) {
const d = new Date(m[1] + 'T00:00:00Z');
if (!isNaN(+d) && d <= now) dm.push(d);
}
if (dm.length) return { asOf: new Date(Math.min(...dm.map(Number))), precise: false };
return { asOf: null, precise: false };
}
function filenameDate(name) {
const m = /(\d{4}-\d{2}-\d{2})/.exec(name);
return m ? m[1] : null;
}
function ticketOf(name, body) {
const m = /(TK-\d+)/.exec(name) || /(TK-\d+)/.exec(body || '');
return m ? m[1] : null;
}
// ---------------------------------------------------------------------------
// Gather concurrent findings from every configured surface.
// ---------------------------------------------------------------------------
function walkMemoFiles(dir, depth) {
const out = [];
let ents;
try {
ents = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return out;
}
for (const e of ents) {
const fp = path.join(dir, e.name);
if (e.isFile() && e.name.endsWith('.md')) out.push(fp);
else if (e.isDirectory() && depth > 0 && !e.name.startsWith('.')) {
out.push(...walkMemoFiles(fp, depth - 1));
}
}
return out;
}
function gatherFindings(cfg, selfPath, now) {
now = now || new Date();
const findings = [];
const selfReal = selfPath ? path.resolve(selfPath) : null;
const addMemo = (fp, kind) => {
if (selfReal && path.resolve(fp) === selfReal) return; // never self
let body;
try {
body = fs.readFileSync(fp, 'utf8');
} catch {
return;
}
const base = path.basename(fp);
const { asOf } = memoAsOf(body, filenameDate(base), now);
if (!asOf) return;
findings.push({
source: base,
path: fp,
kind, // 'sibling' (open) | 'filed'
ts: asOf,
anchors: extractAnchors(body),
ticket: ticketOf(base, body),
});
};
// open siblings
for (const fp of walkMemoFiles(cfg.queueDir, 0)) addMemo(fp, 'sibling');
// filed memos
for (const d of cfg.filedDirs) for (const fp of walkMemoFiles(d, cfg.recurseDepth)) addMemo(fp, 'filed');
// executed-reversible ledger (already-done — strongest invalidation)
for (const lp of cfg.ledgerPaths) {
let raw;
try {
raw = fs.readFileSync(lp, 'utf8');
} catch {
continue;
}
for (const line of raw.split('\n')) {
if (!line.trim()) continue;
let row;
try {
row = JSON.parse(line);
} catch {
continue;
}
const ts = new Date(row.ts);
if (isNaN(+ts) || ts > now) continue; // skip unparseable / future-dated
const text = [row.action, row.verify, row.undo_cmd, row.ticket].filter(Boolean).join(' ');
findings.push({
source: 'executed-reversible/ledger.jsonl',
path: lp,
kind: 'executed',
ts,
anchors: extractAnchors(text),
ticket: row.ticket || null,
agent: row.agent || null,
snippet: String(row.action || '').slice(0, 160),
});
}
}
// decision logs — a related memo already approved/rejected
for (const dl of cfg.decisionLogs) {
let raw;
try {
raw = fs.readFileSync(dl, 'utf8');
} catch {
continue;
}
for (const line of raw.split('\n')) {
if (!line.trim()) continue;
let row;
try {
row = JSON.parse(line);
} catch {
continue;
}
const ts = new Date(row.ts);
if (isNaN(+ts) || ts > now || !row.file) continue;
findings.push({
source: path.basename(dl),
path: dl,
kind: 'decision',
ts,
anchors: extractAnchors(row.file),
ticket: ticketOf(row.file, ''),
snippet: `${row.decision} ${row.file}`.slice(0, 160),
});
}
}
return findings;
}
// ---------------------------------------------------------------------------
// The check. Read a memo, extract its target set + as-of, join against newer
// findings, and return an advisory report.
// ---------------------------------------------------------------------------
const KIND_SEVERITY = { executed: 'high', filed: 'high', sibling: 'medium', decision: 'medium' };
const KIND_REASON = {
executed: 'ALREADY EXECUTED per executed-reversible ledger',
filed: 'target memo since FILED (resolved/superseded/decided)',
sibling: 'another OPEN memo also targets this',
decision: 'a related memo was already decided',
};
function findReverifyHint(body) {
// surface the memo's own re-enumeration/re-verify command as the recommended
// manual fresh-enumeration step (we never run it ourselves).
const lines = body.split('\n');
for (let i = 0; i < lines.length; i++) {
if (/re-?verify|re-?enumerate|re-?run the canary|check\.mjs/i.test(lines[i])) {
// prefer a fenced command near the hint
for (let j = i; j < Math.min(i + 4, lines.length); j++) {
const cm = /`([^`]*\b(?:node|check\.mjs)[^`]*)`/.exec(lines[j]);
if (cm) return cm[1].trim();
}
return lines[i].replace(/^[>#\s*-]+/, '').trim().slice(0, 200);
}
}
return null;
}
// input: memo file path OR { file, body }. cfg optional (defaults to real queue).
function checkMemo(input, cfg) {
cfg = Object.assign(defaultConfig(), cfg || {});
const now = cfg.now ? new Date(cfg.now) : new Date();
let file, body, fp;
if (typeof input === 'string') {
fp = input;
file = path.basename(input);
try {
body = fs.readFileSync(input, 'utf8');
} catch (e) {
return { file, verdict: 'NOT_MEASURED', status: 'WARN', reason: 'memo unreadable: ' + e.message };
}
} else {
file = input.file;
body = input.body;
fp = input.path || null;
}
const anchors = extractAnchors(body);
const { asOf, precise } = memoAsOf(body, filenameDate(file), now);
const ticket = ticketOf(file, body);
if (anchors.size === 0) {
return {
file,
ticket,
verdict: 'NO_TARGETS',
status: 'PASS',
targetCount: 0,
reason: 'memo carries no enumerated target set (no ids/skus/offer-ids/handles) — freshness join N/A',
};
}
if (!asOf) {
return {
file,
ticket,
verdict: 'NOT_MEASURED',
status: 'WARN',
targetCount: anchors.size,
reason: 'no parseable as-of timestamp in memo — cannot join against findings; re-enumerate manually before acting',
};
}
const findings = gatherFindings(cfg, fp, now);
// For each target anchor, collect newer findings that reference it.
const stale = new Map(); // anchor -> [ {source, ticket, ts, kind, severity, reason, snippet} ]
for (const f of findings) {
if (f.ts > now) continue; // never trust a future-dated finding
if (!(f.ts > asOf)) continue; // only findings NEWER than the memo's as-of
if (f.ticket && ticket && f.ticket === ticket && f.kind !== 'executed') {
// same ticket, non-ledger: this is the memo's own lane re-filing itself; skip
// (a ledger row for the same ticket IS an execution and DOES count).
continue;
}
for (const [anchor] of anchors) {
if (f.anchors.has(anchor)) {
if (!stale.has(anchor)) stale.set(anchor, []);
stale.get(anchor).push({
source: f.source,
ticket: f.ticket,
ts: f.ts.toISOString(),
kind: f.kind,
severity: KIND_SEVERITY[f.kind] || 'medium',
reason: KIND_REASON[f.kind] || 'referenced by a newer finding',
snippet: f.snippet || null,
});
}
}
}
const M = anchors.size;
const N = stale.size;
const reverifyHint = findReverifyHint(body);
if (N === 0) {
return {
file,
ticket,
verdict: 'FRESH',
status: precise ? 'PASS' : 'WARN',
targetCount: M,
staleCount: 0,
asOf: asOf.toISOString(),
precise,
reverifyHint,
headline: `0 of ${M} targets touched by newer findings — no concurrent invalidation detected.` +
(precise ? '' : ' (as-of was date-only — treat as advisory, re-verify recommended)'),
};
}
const staleTargets = [...stale.entries()].map(([anchor, hits]) => ({
target: anchor,
type: anchors.get(anchor),
hits: hits.sort((a, b) => (a.ts < b.ts ? 1 : -1)),
worst: hits.some((h) => h.severity === 'high') ? 'high' : 'medium',
}));
const anyHigh = staleTargets.some((s) => s.worst === 'high');
return {
file,
ticket,
verdict: 'STALE',
status: 'WARN', // advisory — never auto-FAIL a memo; the human decides
targetCount: M,
staleCount: N,
asOf: asOf.toISOString(),
precise,
reverifyHint,
highSeverity: anyHigh,
headline: `${N} of ${M} targets are now STALE — a newer concurrent finding has touched them since this memo's as-of (${asOf.toISOString()}). DO NOT execute this list blind; re-enumerate first.`,
staleTargets,
};
}
// convenience: check every open memo in the queue
function checkQueue(cfg) {
cfg = Object.assign(defaultConfig(), cfg || {});
const files = walkMemoFiles(cfg.queueDir, 0).filter((f) => !path.basename(f).startsWith('_'));
return files.map((fp) => checkMemo(fp, cfg));
}
module.exports = {
checkMemo,
checkQueue,
extractAnchors,
memoAsOf,
gatherFindings,
defaultConfig,
};