[object Object]

← back to Sample Followup Sweep

TK-11409: anti-dup filter moves to the send gate only (DTD 5/5 verdict C)

55c81102b5631c8420295ce9a8f82861c8b2cb93 · 2026-09-10 11:15:26 -0700 · Steve Abrams

DRAFT now always includes recently-emailed vendors, flagged for review; SEND keeps
the rolling window strictly enforced. A draft sends nothing and leaves no mailbox,
so suppressing it prevented no harm and only hid work -- which is how 33 overdue
memos across 10 vendors stayed invisible until Steve asked. The --include-suppressed
opt-in is why: a safety step that must be REMEMBERED is not a control, and nobody
ever typed it. Flag retained as a no-op so launchd/callers keep working.

Also killed the stale '8/15' label. There was never an 8/15 cutoff -- the rule is a
rolling RECENT_DAYS (14) window recomputed every run, and the hardcoded reason string
misled everyone, including me, into reading it as a one-time event.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Files touched

Diff

commit 55c81102b5631c8420295ce9a8f82861c8b2cb93
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 10 11:15:26 2026 -0700

    TK-11409: anti-dup filter moves to the send gate only (DTD 5/5 verdict C)
    
    DRAFT now always includes recently-emailed vendors, flagged for review; SEND keeps
    the rolling window strictly enforced. A draft sends nothing and leaves no mailbox,
    so suppressing it prevented no harm and only hid work -- which is how 33 overdue
    memos across 10 vendors stayed invisible until Steve asked. The --include-suppressed
    opt-in is why: a safety step that must be REMEMBERED is not a control, and nobody
    ever typed it. Flag retained as a no-op so launchd/callers keep working.
    
    Also killed the stale '8/15' label. There was never an 8/15 cutoff -- the rule is a
    rolling RECENT_DAYS (14) window recomputed every run, and the hardcoded reason string
    misled everyone, including me, into reading it as a one-time event.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 scripts/scheduled-run.mjs | 22 +++++++++++++++++-----
 1 file changed, 17 insertions(+), 5 deletions(-)

diff --git a/scripts/scheduled-run.mjs b/scripts/scheduled-run.mjs
index 507e099..22a6121 100644
--- a/scripts/scheduled-run.mjs
+++ b/scripts/scheduled-run.mjs
@@ -36,7 +36,13 @@ const DRAFT = process.argv.includes('--draft');
 // Steve 9/01: draft the anti-dup-SUPPRESSED vendors ANYWAY (for review) instead of skipping them.
 // Each such draft's subject is flagged so Steve can see the team already emailed that vendor recently.
 // Nothing sends — the draft is still the human gate, Steve's review is the real dedup. DRAFT-mode only.
+// Retained as a no-op for DRAFT (which now always includes suppressed vendors, per DTD verdict C)
+// so existing callers and the launchd plists keep working unchanged.
 const INCLUDE_SUPPRESSED = process.argv.includes('--include-suppressed') || process.env.INCLUDE_SUPPRESSED === '1';
+// Rolling anti-dup window, in days. This is the REAL rule — the old reason string said
+// "already emailed since 8/15 (manual team send)", which was a stale hardcoded label that
+// misled everyone into thinking it was a one-time 8/15 cutoff. It is not: it re-arms daily.
+const RECENT_DAYS = Number(process.env.RECENT_DAYS) || 14;
 const DB = 'WALLPAPER';
 const LAYOUT = 'Report for old memo samples';
 const FIELD_SENT = 'Date Email Sent to Vendor after 10 Days';
@@ -265,16 +271,22 @@ const run = async () => {
   // for (harvested from info@ Sent into data/recently-contacted.json). Stops the sweep from
   // duplicating what the team already sent manually — the whole point of the 2026-08-20 fix.
   const RECENT = new Set(readJSON('data/recently-contacted.json', []).map((e) => String(e).toLowerCase().trim()).filter(Boolean));
-  try { const live = await georgeRecentRecipients(14); for (const e of live) RECENT.add(e); if (live.size) console.log(`(auto-suppression: ${live.size} recipient(s) already emailed in last 14d, from info@ Sent)`); } catch {}
+  try { const live = await georgeRecentRecipients(RECENT_DAYS); for (const e of live) RECENT.add(e); if (live.size) console.log(`(anti-dup pool: ${live.size} recipient(s) emailed in last ${RECENT_DAYS}d — enforced on SEND, drafted-with-flag on DRAFT)`); } catch {}
   const sent = [], skipped = [], needsConfirm = [], drafted = [], suppressed = [];
   for (const [vid, rows] of Object.entries(byVid)) {
     const c = cmap[vid];
     if (!c || c.noChase) { skipped.push({ vid, n: rows.length, reason: c?.noChase ? 'no-chase vendor' : 'no sample email/account on file' }); continue; }
     const recips = String((c.sample_email || c.main_email) || '').toLowerCase().split(/[,;]\s*/).map((s) => s.trim()).filter(Boolean);
     const recentlyEmailed = recips.length > 0 && recips.some((e) => RECENT.has(e));
-    // Default: skip a recently-emailed vendor (anti-dup). With --include-suppressed we DRAFT it anyway,
-    // flagged, so Steve can review + decide per-vendor (nothing sends).
-    if (recentlyEmailed && !(INCLUDE_SUPPRESSED && DRAFT)) { suppressed.push({ vid, name: c.name, to: (c.sample_email || c.main_email), n: rows.length, reason: 'already emailed since 8/15 (manual team send)' }); continue; }
+    // DTD 5/5 verdict C (2026-09-10, TK-11409): the anti-dup filter now sits ONLY next to the
+    // irreversible act. SEND stays strictly suppressed — a duplicate chase to a vendor we just
+    // ordered from is a real embarrassment. DRAFT always includes them, flagged for review,
+    // because a draft sends nothing, leaves no mailbox, writes no record: suppressing it prevented
+    // no harm and only hid work. That hiding is what let 33 overdue memos across 10 vendors sit
+    // invisible for weeks, surfacing only because Steve happened to ask. The old --include-suppressed
+    // opt-in is why: a safety step that must be REMEMBERED is not a control, and nobody ever typed it.
+    // (INCLUDE_SUPPRESSED is retained as a no-op for DRAFT so existing callers/launchd keep working.)
+    if (recentlyEmailed && !DRAFT) { suppressed.push({ vid, name: c.name, to: (c.sample_email || c.main_email), n: rows.length, reason: `already emailed in the last ${RECENT_DAYS}d (rolling anti-dup, SEND mode)` }); continue; }
     // In SEND mode, an unconfirmed (resolved/fallback) recipient is HELD — never fires. In DRAFT
     // mode the draft itself IS the review, so everything is drafted for Steve to check + send.
     if (c.needs_confirm && !DRAFT) { needsConfirm.push({ vid, name: c.name, to: (c.sample_email || c.main_email), n: rows.length, skus: rows.map((r) => r.sku), reason: 'main-email fallback — CONFIRM person vs recent emails before send' }); continue; }
@@ -321,7 +333,7 @@ const run = async () => {
     sent.forEach((s) => console.log(`  ${SEND ? '✓' : '·'} ${s.name.padEnd(30)} [${s.vid}] → ${s.to}  (${s.n} SKU${s.n > 1 ? 's' : ''})`));
     if (needsConfirm.length) { console.log(`\n⚠ ${needsConfirm.length} main-email fallback(s) — HELD for person-confirmation (never auto-sent):`); needsConfirm.forEach((s) => console.log(`  ? ${s.name.padEnd(30)} [${s.vid}] → ${s.to}  (${s.n} SKU${s.n > 1 ? 's' : ''}) — ${s.reason}`)); }
   }
-  if (suppressed.length) { console.log(`\n⊘ Suppressed ${suppressed.length} (team already emailed since 8/15 — not duplicated):`); suppressed.forEach((s) => console.log(`  ⊘ ${(s.name||'').padEnd(30)} [${s.vid}] → ${s.to}`)); }
+  if (suppressed.length) { console.log(`\n⊘ Suppressed ${suppressed.length} (emailed within ${RECENT_DAYS}d — SEND mode only; DRAFT would surface these flagged):`); suppressed.forEach((s) => console.log(`  ⊘ ${(s.name||'').padEnd(30)} [${s.vid}] → ${s.to}`)); }
   if (skipped.length) { console.log(`\nSkipped ${skipped.length} (no contact / no-chase / error):`); skipped.forEach((s) => console.log(`  - [${s.vid}] ${s.n} SKU(s): ${s.reason}`)); }
   console.log(`\n→ data/runs/scheduled-${today.replace(/\//g, '-')}.json`);
 };

← 54f5ad4 auto-data-snapshot: 2026-09-10T11:08:25 (2 data files) — dat  ·  back to Sample Followup Sweep  ·  TK-11409: verify the draft-ledger against reality; surface b ed3bc8d →