← 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
M scripts/scheduled-run.mjs
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 →