← back to Approvals Viewer
TK-11685: approval-time freshness re-check guard (read-only, advisory)
c9cbd5fd1ce318cb70e9fe52608e8cf670b94bcd · 2026-09-14 01:12:05 -0700 · Steve Abrams
Closes the governance gap where a delete list could be approved against a
STALE enumeration: an approval fired at 18:26Z for a list a sibling lane had
already invalidated at 15:43Z, because nothing joined a fresh enumeration
against concurrent findings before the memo reached Steve.
freshness-guard.js joins a gated memo's enumerated target set (Shopify
variant/product ids, DW-SKUs, GMC offer-ids, /products/ handles) against every
newer concurrent finding — the executed-reversible ledger (already-done), memos
since filed to _done/_resolved/_superseded, other open memos, and decision
logs — and flags any target a finding newer than the memo's as-of has touched,
surfacing "N of M targets are now stale, do NOT execute blind". Precision
anchors + now-clamp (future expiry/scheduled dates never corrupt as-of or a
finding ts) + Drafted-date as-of + NOT_MEASURED-is-never-PASS. Verdicts in the
fleet-health PASS/WARN/FAIL vocabulary.
READ-ONLY + ADVISORY throughout: it annotates/warns and surfaces the memo's own
re-verify command; it NEVER approves, rejects, edits, moves, or executes any
memo, delete, or target action, and never runs the memo's re-verify command
itself.
- freshness-guard-cli.js: one memo / --all / --stale / --json
- server.js: read-only GET /api/freshness?file=<memo.md> (additive, guarded)
- test/freshness-guard.test.js: NEGATIVE TEST — injects the concurrent-
invalidation fault, proves STALE; proves FRESH on a still-fresh set; proves
the future-date clamp. 19/19 assertions pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: 963c1354-d10a-451f-8e5b-936663056ab7
Files touched
A FRESHNESS-GUARD.mdA freshness-guard-cli.jsA freshness-guard.jsM server.jsA test/freshness-guard.test.js
Diff
commit c9cbd5fd1ce318cb70e9fe52608e8cf670b94bcd
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Sep 14 01:12:05 2026 -0700
TK-11685: approval-time freshness re-check guard (read-only, advisory)
Closes the governance gap where a delete list could be approved against a
STALE enumeration: an approval fired at 18:26Z for a list a sibling lane had
already invalidated at 15:43Z, because nothing joined a fresh enumeration
against concurrent findings before the memo reached Steve.
freshness-guard.js joins a gated memo's enumerated target set (Shopify
variant/product ids, DW-SKUs, GMC offer-ids, /products/ handles) against every
newer concurrent finding — the executed-reversible ledger (already-done), memos
since filed to _done/_resolved/_superseded, other open memos, and decision
logs — and flags any target a finding newer than the memo's as-of has touched,
surfacing "N of M targets are now stale, do NOT execute blind". Precision
anchors + now-clamp (future expiry/scheduled dates never corrupt as-of or a
finding ts) + Drafted-date as-of + NOT_MEASURED-is-never-PASS. Verdicts in the
fleet-health PASS/WARN/FAIL vocabulary.
READ-ONLY + ADVISORY throughout: it annotates/warns and surfaces the memo's own
re-verify command; it NEVER approves, rejects, edits, moves, or executes any
memo, delete, or target action, and never runs the memo's re-verify command
itself.
- freshness-guard-cli.js: one memo / --all / --stale / --json
- server.js: read-only GET /api/freshness?file=<memo.md> (additive, guarded)
- test/freshness-guard.test.js: NEGATIVE TEST — injects the concurrent-
invalidation fault, proves STALE; proves FRESH on a still-fresh set; proves
the future-date clamp. 19/19 assertions pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: 963c1354-d10a-451f-8e5b-936663056ab7
---
FRESHNESS-GUARD.md | 64 ++++++
freshness-guard-cli.js | 81 ++++++++
freshness-guard.js | 450 +++++++++++++++++++++++++++++++++++++++++++
server.js | 10 +
test/freshness-guard.test.js | 211 ++++++++++++++++++++
5 files changed, 816 insertions(+)
diff --git a/FRESHNESS-GUARD.md b/FRESHNESS-GUARD.md
new file mode 100644
index 0000000..2c556e8
--- /dev/null
+++ b/FRESHNESS-GUARD.md
@@ -0,0 +1,64 @@
+# Approval-time freshness re-check guard (TK-11685)
+
+**Read-only. Advisory. Never approves, rejects, edits, moves, or executes anything.**
+
+## The incident it closes
+An approval was granted at **18:26Z** for a delete list that a **sibling lane had
+already invalidated at 15:43Z the same day**. Nothing joined a *fresh enumeration
+against concurrent findings* before the memo reached Steve — so a gated delete list
+could be approved against a **stale** target set: targets a newer lane had already
+deleted, superseded, or re-classified since the list was enumerated.
+
+## What it does
+Given a gated memo that carries an **enumerated target set** (Shopify variant/product
+ids, DW-SKUs, GMC offer-ids, `/products/<handle>` handles), it cross-references
+**every newer concurrent finding** and flags any target a finding *newer than the
+memo's own as-of* has since touched. Concurrent-finding surfaces:
+
+| surface | signal |
+|---|---|
+| `executed-reversible/ledger.jsonl` | target **already executed** (strongest) |
+| memos filed to `_done/_resolved/_superseded/_approved/_rejected/…` | target's memo **since resolved/superseded** |
+| other open memos in `pending-approval/` | another **open** memo also targets it |
+| `_decisions.jsonl` / `_review-decisions.jsonl` | a related memo **already decided** |
+
+Output answers one question: **"N of M targets are now stale — do NOT execute this
+list blind; re-enumerate first."** It also surfaces the memo's own re-verify command
+(it does **not** run it — that would be arbitrary code from a parsed file).
+
+## Correctness rails
+- **Precision anchors only** — long numeric ids, `DW*-####` SKUs, `shopify_XX_..`
+ offer ids, and `/products/`-context handles. Infra names (skill/canary names, the
+ store domain) are deliberately **not** anchors, so a shared anchor means "same
+ target," not a coincidence.
+- **now-clamp** — a future-dated string (a GMC `googleExpirationDate`, a scheduled
+ date) is never an enumeration/execution event. Timestamps in the future are
+ excluded from both the memo's as-of and from findings, so a future date can neither
+ push a memo falsely FRESH nor make a finding look newer than it is.
+- **as-of** = the memo's most recent *past* full timestamp, else its **Drafted/Written**
+ date — so findings the memo already accounted for do not fire.
+- **NOT_MEASURED is never PASS** — an unreadable memo or one with no parseable as-of
+ is WARN, never a false green.
+
+## Verdicts (fleet-health-rollup vocabulary)
+`FRESH → PASS` · `STALE → WARN` (advisory) · `NO_TARGETS → PASS` (not a delete list) ·
+`NOT_MEASURED → WARN`.
+
+## Usage
+```sh
+node freshness-guard-cli.js <memo.md> # one memo (path or queue basename)
+node freshness-guard-cli.js --all # every open memo
+node freshness-guard-cli.js --all --stale # only the flagged ones
+node freshness-guard-cli.js <memo> --json # machine-readable
+```
+Viewer endpoint (read-only): `GET /api/freshness?file=<memo.md>` returns the report
+JSON so a memo card can show its freshness badge before Steve decides.
+
+## Test
+```sh
+node test/freshness-guard.test.js
+```
+Ships a **negative test**: it injects a concurrent-invalidation fault (a sibling-lane
+execution landing after the memo's as-of — the exact incident) and proves the guard
+flips to STALE, then proves it stays FRESH when only older findings exist, and proves
+the future-date clamp holds.
diff --git a/freshness-guard-cli.js b/freshness-guard-cli.js
new file mode 100644
index 0000000..de20e5a
--- /dev/null
+++ b/freshness-guard-cli.js
@@ -0,0 +1,81 @@
+#!/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);
diff --git a/freshness-guard.js b/freshness-guard.js
new file mode 100644
index 0000000..ce875de
--- /dev/null
+++ b/freshness-guard.js
@@ -0,0 +1,450 @@
+'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,
+};
diff --git a/server.js b/server.js
index cfcbdaa..5340d9b 100644
--- a/server.js
+++ b/server.js
@@ -1,6 +1,8 @@
#!/usr/bin/env node
// Approvals yes/no swipe viewer — zero-dependency. Basic-auth admin/DW2024!.
const http=require('http'),fs=require('fs'),path=require('path');
+// TK-11685 approval-time freshness re-check (READ-ONLY, ADVISORY). Never mutates a memo.
+let freshnessGuard=null;try{freshnessGuard=require('./freshness-guard');}catch(e){}
const QUEUE=path.join(process.env.HOME,'.claude/yolo-queue/pending-approval');
const APPROVED=path.join(QUEUE,'_approved'),REJECTED=path.join(QUEUE,'_rejected');
const LOG=path.join(QUEUE,'_decisions.jsonl');
@@ -41,6 +43,14 @@ http.createServer((req,res)=>{
res.writeHead(401,{'WWW-Authenticate':'Basic realm="Approval Command Center"'});return res.end('auth required');}
if(req.url==='/'||req.url==='/index.html')return send(res,200,fs.readFileSync(path.join(__dirname,'public/index.html')),'text/html');
if(req.url.startsWith('/api/memos')){const u=new URL(req.url,'http://x');return send(res,200,JSON.stringify(listMemos(u.searchParams.get('sort')||'mtime')));}
+ // READ-ONLY advisory: approval-time freshness re-check for one memo. Surfaces
+ // "N of M targets are now stale" so Steve never approves a delete list blind.
+ // It only READS; it never approves/rejects/edits/moves/executes anything.
+ if(req.url.startsWith('/api/freshness')){const u=new URL(req.url,'http://x');const file=u.searchParams.get('file')||'';
+ if(!/^[\w.\-]+\.md$/.test(file))return send(res,400,'{"error":"bad file"}');
+ if(!freshnessGuard)return send(res,200,'{"verdict":"UNAVAILABLE","status":"WARN","reason":"freshness-guard module not loaded"}');
+ try{const rpt=freshnessGuard.checkMemo(path.join(QUEUE,file));return send(res,200,JSON.stringify(rpt));}
+ catch(e){return send(res,200,JSON.stringify({verdict:'NOT_MEASURED',status:'WARN',reason:String(e.message)}));}}
if(req.url==='/api/rate'&&req.method==='POST'){let d='';req.on('data',c=>d+=c);req.on('end',()=>{try{
const{file,rating,note}=JSON.parse(d);if(!/^[\w.\-]+\.md$/.test(file))throw new Error('bad file');
const r=loadRatings();r[file]={rating:Math.min(5,Math.max(0,parseInt(rating)||0)),note:String(note||'').slice(0,200),ts:new Date().toISOString()};saveRatings(r);
diff --git a/test/freshness-guard.test.js b/test/freshness-guard.test.js
new file mode 100644
index 0000000..c0cc49e
--- /dev/null
+++ b/test/freshness-guard.test.js
@@ -0,0 +1,211 @@
+'use strict';
+// NEGATIVE TEST for freshness-guard.js (TK-11685).
+//
+// Proves the guard (1) goes STALE on a fixture where the target set was invalidated
+// by a LATER concurrent finding — a faithful replay of the incident (a delete list
+// whose target a sibling lane invalidated after the memo's as-of but before
+// approval) — and (2) passes FRESH-clean when the same set has no newer finding.
+//
+// This is the "ship a check with a negative test proving it goes red on an injected
+// fault" rule: we inject the fault, watch it go red, and confirm the control (no
+// fault) stays green. Runs entirely against a throwaway temp fixture dir — it never
+// reads or writes the real ~/.claude/yolo-queue.
+
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const assert = require('assert');
+const G = require('../freshness-guard');
+
+let failures = 0;
+function check(name, cond, extra) {
+ if (cond) {
+ console.log(' ok - ' + name);
+ } else {
+ failures++;
+ console.log(' FAIL- ' + name + (extra ? ' :: ' + extra : ''));
+ }
+}
+
+// ---- build a throwaway fixture tree -------------------------------------------
+const root = fs.mkdtempSync(path.join(os.tmpdir(), 'freshguard-'));
+const queue = path.join(root, 'pending-approval');
+const done = path.join(queue, '_done');
+const execDir = path.join(root, 'executed-reversible');
+fs.mkdirSync(done, { recursive: true });
+fs.mkdirSync(execDir, { recursive: true });
+
+// The SUBJECT memo: a delete list enumerated at 10:12Z, targeting variant
+// 44453188108339 (DWRW-72230) and 44453188141107 (DWRW-72231). Mirrors TK-11657.
+const subject = path.join(queue, '2026-09-13-TK-11657-rebelwalls-delete.md');
+fs.writeFileSync(
+ subject,
+ [
+ '# TK-11657 — Delete 2 orphan variants (GATED)',
+ '',
+ '**Drafted:** 2026-09-13 · enumeration completed 2026-09-13T10:12:00Z',
+ '',
+ '| handle | delete variant |',
+ '|---|---|',
+ '| dwrw-72230 | `44453188108339` DWRW-72230-Sample |',
+ '| dwrw-72231 | `44453188141107` DWRW-72231-Sample |',
+ '',
+ 'Re-verify first (read-only): `node ~/.claude/skills/dw-x/check.mjs`',
+ ].join('\n'),
+);
+
+const cfg = {
+ queueDir: queue,
+ filedDirs: [done],
+ ledgerPaths: [path.join(execDir, 'ledger.jsonl')],
+ decisionLogs: [path.join(queue, '_decisions.jsonl')],
+ recurseDepth: 1,
+};
+
+// ---- CONTROL: no newer finding => FRESH (clean) --------------------------------
+{
+ // an OLDER ledger row (before as-of) that mentions the same id must NOT flag.
+ fs.writeFileSync(
+ cfg.ledgerPaths[0],
+ JSON.stringify({
+ ts: '2026-09-13T09:00:00-00:00',
+ agent: 'earlier-lane',
+ ticket: 'TK-99999',
+ action: 'unrelated earlier touch of variant 44453188108339',
+ blast_radius: 1,
+ }) + '\n',
+ );
+ const r = G.checkMemo(subject, cfg);
+ check('control: verdict is FRESH when only OLDER findings exist', r.verdict === 'FRESH', r.verdict);
+ check('control: status PASS', r.status === 'PASS', r.status);
+ // 4 distinct target IDENTIFIERS: two variant ids + two DW-SKUs (each row carries both forms)
+ check('control: 4 target identifiers detected', r.targetCount === 4, 'targetCount=' + r.targetCount);
+ check('control: reverify hint surfaced', !!r.reverifyHint, r.reverifyHint);
+}
+
+// ---- INJECT THE FAULT: a sibling lane invalidates a target AFTER as-of ----------
+// The incident exactly: enumeration at 10:12Z, a concurrent lane acts at 15:43Z,
+// approval would fire blind at 18:26Z. The guard must catch the 15:43Z finding.
+{
+ fs.appendFileSync(
+ cfg.ledgerPaths[0],
+ JSON.stringify({
+ ts: '2026-09-13T15:43:00-00:00',
+ agent: 'sibling-lane',
+ ticket: 'TK-11680',
+ action: 'productVariantsBulkDelete removed variant 44453188108339 (dwrw-72230) as part of the intra-product dup-SKU sweep',
+ blast_radius: 1,
+ undo_cmd: 'productVariantsBulkCreate ...',
+ }) + '\n',
+ );
+ const r = G.checkMemo(subject, cfg);
+ check('INJECTED FAULT: verdict flips to STALE', r.verdict === 'STALE', r.verdict);
+ check('INJECTED FAULT: status WARN (advisory, not a hard block)', r.status === 'WARN', r.status);
+ // the ledger row names both the variant id AND the SKU (dwrw-72230), so both
+ // identifiers of the deleted row light up — 2 of the 4 identifiers.
+ check('INJECTED FAULT: 2 of 4 identifiers stale (id + sku of the deleted row)', r.staleCount === 2 && r.targetCount === 4, `${r.staleCount}/${r.targetCount}`);
+ const st = (r.staleTargets || []).find((s) => s.target === '44453188108339');
+ check('INJECTED FAULT: the variant id 44453188108339 is flagged', !!st, JSON.stringify(r.staleTargets && r.staleTargets.map((s) => s.target)));
+ check('INJECTED FAULT: it is high severity (already executed)', st && st.worst === 'high', st && st.worst);
+ check('INJECTED FAULT: hit reason names the ledger execution',
+ st && st.hits[0].reason.includes('ALREADY EXECUTED'), st && st.hits[0] && st.hits[0].reason);
+ check('INJECTED FAULT: hit ts is the 15:43Z sibling finding',
+ st && st.hits[0].ts.startsWith('2026-09-13T15:43'), st && st.hits[0] && st.hits[0].ts);
+ check('INJECTED FAULT: headline says do NOT execute blind',
+ /DO NOT execute/i.test(r.headline || ''), r.headline);
+}
+
+// ---- second fault shape: a FILED memo (moved to _done) touches the other target -
+{
+ fs.writeFileSync(
+ path.join(done, '2026-09-13-TK-11682-rebelwalls-more.md'),
+ [
+ '# TK-11682 — 4 more Rebel Walls collisions (RESOLVED 2026-09-13T16:30:00Z)',
+ '',
+ 'Also cleaned `44453188141107` (dwrw-72231) under this ticket.',
+ ].join('\n'),
+ );
+ const r = G.checkMemo(subject, cfg);
+ // now all 4 identifiers (both rows, id+sku each) are touched by a newer finding
+ check('SECOND FAULT: all 4 identifiers now stale', r.staleCount === 4, 'staleCount=' + r.staleCount);
+ const other = (r.staleTargets || []).find((s) => s.target === '44453188141107');
+ check('SECOND FAULT: filed-memo finding attributed to TK-11682',
+ other && other.hits.some((h) => h.ticket === 'TK-11682' && h.kind === 'filed'),
+ other && JSON.stringify(other.hits));
+}
+
+// ---- NO_TARGETS: a prose-only memo is correctly N/A (not a false STALE) ---------
+{
+ const prose = path.join(queue, '2026-09-13-TK-00001-policy-note.md');
+ fs.writeFileSync(prose, '# TK-00001 — policy note\n\nWe should tidy things up on 2026-09-13. No ids here.');
+ const r = G.checkMemo(prose, cfg);
+ check('NO_TARGETS: prose memo => NO_TARGETS/PASS', r.verdict === 'NO_TARGETS' && r.status === 'PASS', r.verdict + '/' + r.status);
+}
+
+// ---- NOT_MEASURED: an enumerated memo with no parseable time => WARN, never PASS -
+{
+ const notime = path.join(queue, 'no-date-TK-00002-delete.md');
+ fs.writeFileSync(notime, '# delete\n\nDelete variant `55553188108339`.');
+ const r = G.checkMemo(notime, { ...cfg }); // filename has no date
+ check('NOT_MEASURED: enumerated memo w/ no as-of => WARN (never a false PASS)',
+ r.verdict === 'NOT_MEASURED' && r.status === 'WARN', r.verdict + '/' + r.status);
+}
+
+// ---- FUTURE-DATE CLAMP: a future expiry in the body must NOT cause a false FRESH -
+// and a future-dated finding must NOT flag. now is pinned so "future" is deterministic.
+{
+ const fdir = path.join(root, 'future');
+ const fq = path.join(fdir, 'pending-approval');
+ const fexec = path.join(fdir, 'executed-reversible');
+ fs.mkdirSync(fq, { recursive: true });
+ fs.mkdirSync(fexec, { recursive: true });
+ const fcfg = {
+ queueDir: fq,
+ filedDirs: [],
+ ledgerPaths: [path.join(fexec, 'ledger.jsonl')],
+ decisionLogs: [],
+ recurseDepth: 0,
+ now: '2026-09-20T00:00:00Z', // pin "now"
+ };
+ // subject: drafted 09-13, targets id 77771234567890, but body ALSO carries a
+ // future GMC expiry 2026-12-31 (the TK-11633 googleExpirationDate shape).
+ const fsubj = path.join(fq, '2026-09-13-TK-42000-delete.md');
+ fs.writeFileSync(
+ fsubj,
+ [
+ '# TK-42000 — delete (GATED)',
+ '**Drafted:** 2026-09-13',
+ 'Delete variant `77771234567890`.',
+ 'googleExpirationDate 2026-12-31T22:03:45Z (self-clears then).',
+ ].join('\n'),
+ );
+ // a REAL concurrent execution at 09-15 (after draft, before now) -> must flag
+ fs.appendFileSync(
+ fcfg.ledgerPaths[0],
+ JSON.stringify({ ts: '2026-09-15T12:00:00-00:00', agent: 'sib', ticket: 'TK-42001', action: 'deleted variant 77771234567890', blast_radius: 1 }) + '\n',
+ );
+ // a FUTURE-dated ledger row (after now) referencing the target -> must NOT flag
+ fs.appendFileSync(
+ fcfg.ledgerPaths[0],
+ JSON.stringify({ ts: '2026-10-05T12:00:00-00:00', agent: 'future', ticket: 'TK-42002', action: 'scheduled touch of variant 77771234567890', blast_radius: 1 }) + '\n',
+ );
+ const r = G.checkMemo(fsubj, fcfg);
+ check('FUTURE CLAMP: future expiry in body does NOT push as-of to the future',
+ r.asOf === '2026-09-13T00:00:00.000Z', 'asOf=' + r.asOf);
+ check('FUTURE CLAMP: real 09-15 execution still flags STALE (no false FRESH)',
+ r.verdict === 'STALE' && r.staleCount === 1, r.verdict + ' ' + r.staleCount);
+ check('FUTURE CLAMP: only the 09-15 finding counts, the 10-05 future row is ignored',
+ r.staleTargets && r.staleTargets[0].hits.length === 1 && r.staleTargets[0].hits[0].ts.startsWith('2026-09-15'),
+ r.staleTargets && JSON.stringify(r.staleTargets[0].hits.map((h) => h.ts)));
+}
+
+// ---- cleanup + report ----------------------------------------------------------
+try { fs.rmSync(root, { recursive: true, force: true }); } catch {}
+
+console.log('');
+if (failures) {
+ console.log(`NEGATIVE TEST FAILED — ${failures} assertion(s) failed.`);
+ process.exit(1);
+}
+console.log('NEGATIVE TEST PASSED — guard reddens on the injected concurrent-invalidation fault and stays green when the set is fresh.');
+process.exit(0);
← a3df63a creds-safe fetch guard: resolve relative fetch vs credential
·
back to Approvals Viewer
·
approvals-viewer: implement openAudit() — Audit All button w c9833a1 →