[object Object]

← 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

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 →