[object Object]

← back to Sample Followup Sweep

sample-followup: two-part never-false-chase guard (arrival-dedup + already-sent) — TK-12105

d04d1067a3d0b78d4ea322a3a00c1602e4e2c7f3 · 2026-09-23 15:10:44 -0700 · Steve Abrams

Before chasing a sku, pull ALL FileMaker records ('Report for old memo samples')
for that combo sku and suppress if EITHER (a) a sibling with the SAME entered date
already has Date WP Sample Sent (arrived on a duplicate record — catches REID
DWRW210156, spares GRD8806 2024-vs-2026), or (b) any record has Date Sample
Request Letter Sent (Steve's authoritative 10-day-letter stamp). Pure suppressChase()
in lib/sweep.js (+4 unit tests), wired into scheduled-run.mjs + send-reconciled.mjs
with a new guard-suppressed bucket. Fail-open; 14-day Gmail anti-dup kept. Nothing sends.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit d04d1067a3d0b78d4ea322a3a00c1602e4e2c7f3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 23 15:10:44 2026 -0700

    sample-followup: two-part never-false-chase guard (arrival-dedup + already-sent) — TK-12105
    
    Before chasing a sku, pull ALL FileMaker records ('Report for old memo samples')
    for that combo sku and suppress if EITHER (a) a sibling with the SAME entered date
    already has Date WP Sample Sent (arrived on a duplicate record — catches REID
    DWRW210156, spares GRD8806 2024-vs-2026), or (b) any record has Date Sample
    Request Letter Sent (Steve's authoritative 10-day-letter stamp). Pure suppressChase()
    in lib/sweep.js (+4 unit tests), wired into scheduled-run.mjs + send-reconciled.mjs
    with a new guard-suppressed bucket. Fail-open; 14-day Gmail anti-dup kept. Nothing sends.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 data/runs/scheduled-09-23-2026.json | 147 ++++--------------------------------
 lib/sweep.js                        |  25 +++++-
 scripts/scheduled-run.mjs           |  32 +++++++-
 scripts/send-reconciled.mjs         |  34 +++++++++
 test/never-false-chase.test.cjs     |  48 ++++++++++++
 5 files changed, 150 insertions(+), 136 deletions(-)

diff --git a/data/runs/scheduled-09-23-2026.json b/data/runs/scheduled-09-23-2026.json
index 77cbb1d..e461608 100644
--- a/data/runs/scheduled-09-23-2026.json
+++ b/data/runs/scheduled-09-23-2026.json
@@ -1,143 +1,22 @@
 {
-  "ranAt": "2026-09-23T21:47:56.392Z",
+  "ranAt": "2026-09-23T22:10:20.302Z",
   "mode": "DRY-RUN",
-  "window": "07/25/2026...09/13/2026",
+  "window": "09/13/2026...09/13/2026",
   "stampDate": "09/23/2026",
-  "vendorsSent": 2,
+  "vendorsSent": 0,
   "vendorsDrafted": 0,
-  "vendorsSkipped": 1,
-  "vendorsNeedsConfirm": 3,
-  "vendorsSuppressed": 9,
+  "vendorsSkipped": 0,
+  "vendorsNeedsConfirm": 0,
+  "vendorsSuppressed": 0,
   "vendorsAlreadyDrafted": 0,
   "vendorsStaleLedger": 0,
-  "sent": [
-    {
-      "vid": "REID",
-      "name": "Reid Witlin Textiles",
-      "to": "samples@rwltd.com",
-      "n": 1,
-      "skus": [
-        "DWRW210156"
-      ],
-      "dryRun": true
-    },
-    {
-      "vid": "FABR",
-      "name": "Fabricut",
-      "to": "customers@fabricut.com",
-      "n": 1,
-      "skus": [
-        "DWFC230158"
-      ],
-      "dryRun": true
-    }
-  ],
+  "skusGuardSuppressed": 0,
+  "sent": [],
   "drafted": [],
-  "needsConfirm": [
-    {
-      "vid": "WAT",
-      "name": "Watts 1874",
-      "to": "sophie@watts1874.co.uk",
-      "n": 1,
-      "skus": [
-        "DWC324296"
-      ],
-      "reason": "main-email fallback — CONFIRM person vs recent emails before send"
-    },
-    {
-      "vid": "BAR",
-      "name": "Barneby Gates",
-      "to": "studio@barnebygates.com, alice@barnebygates.com",
-      "n": 2,
-      "skus": [
-        "DWBW600341",
-        "DWBW600342"
-      ],
-      "reason": "main-email fallback — CONFIRM person vs recent emails before send"
-    },
-    {
-      "vid": "GRD",
-      "name": "Graduate Collection",
-      "to": "orders@graduatecollection.co.uk",
-      "n": 1,
-      "skus": [
-        "GRD8806"
-      ],
-      "reason": "main-email fallback — CONFIRM person vs recent emails before send"
-    }
-  ],
-  "skipped": [
-    {
-      "vid": "MDCKEN",
-      "n": 8,
-      "reason": "no-chase vendor"
-    }
-  ],
-  "suppressed": [
-    {
-      "vid": "1838",
-      "name": "1838 Wallcoverings",
-      "to": "scott.myer@gmail.com",
-      "n": 2,
-      "reason": "already emailed in the last 14d (rolling anti-dup, SEND mode)"
-    },
-    {
-      "vid": "INN",
-      "name": "Innovations",
-      "to": "hmartinez@innovationsusa.com",
-      "n": 1,
-      "reason": "already emailed in the last 14d (rolling anti-dup, SEND mode)"
-    },
-    {
-      "vid": "MDC",
-      "name": "MDC Wallcoverings",
-      "to": "sample@mdcwall.com",
-      "n": 2,
-      "reason": "already emailed in the last 14d (rolling anti-dup, SEND mode)"
-    },
-    {
-      "vid": "ASTSAND",
-      "name": "Astek",
-      "to": "amy.d@astek.com",
-      "n": 1,
-      "reason": "already emailed in the last 14d (rolling anti-dup, SEND mode)"
-    },
-    {
-      "vid": "1838 WALLCOVERINGS",
-      "name": "1838 Wallcoverings",
-      "to": "scott.myer@gmail.com",
-      "n": 1,
-      "reason": "already emailed in the last 14d (rolling anti-dup, SEND mode)"
-    },
-    {
-      "vid": "VAH",
-      "name": "Vahallan",
-      "to": "info@vahallan.com",
-      "n": 2,
-      "reason": "already emailed in the last 14d (rolling anti-dup, SEND mode)"
-    },
-    {
-      "vid": "KRA",
-      "name": "Kravet",
-      "to": "matt.schoffman@kravet.com",
-      "n": 1,
-      "reason": "already emailed in the last 14d (rolling anti-dup, SEND mode)"
-    },
-    {
-      "vid": "TWI",
-      "name": "twill (Karin Klein)",
-      "to": "karinkleintextiles@gmail.com",
-      "n": 3,
-      "reason": "already emailed in the last 14d (rolling anti-dup, SEND mode)"
-    },
-    {
-      "vid": "GREEN",
-      "name": "Greenland (bmwallpaper)",
-      "to": "sales@bmwallpaper.com",
-      "n": 1,
-      "reason": "already emailed in the last 14d (rolling anti-dup, SEND mode)"
-    }
-  ],
+  "needsConfirm": [],
+  "skipped": [],
+  "suppressed": [],
   "alreadyDrafted": [],
-  "staleLedger": []
+  "staleLedger": [],
+  "guardSuppressed": []
 }
\ No newline at end of file
diff --git a/lib/sweep.js b/lib/sweep.js
index fb0fb83..21c2a8d 100644
--- a/lib/sweep.js
+++ b/lib/sweep.js
@@ -58,4 +58,27 @@ function sweep(rows, opts = {}, today = new Date()) {
   return { followUp, escalation, skipped };
 }
 
-module.exports = { sweep, ageDays };
+// TK-12105 (Steve 2026-09-23): NEVER-FALSE-CHASE guard. Pure decision — given the outstanding
+// record's entered date ('today for client') and ALL FileMaker records for that combo sku
+// ({ entered, wpSampleSent, letterSent } each), decide whether to SUPPRESS the chase. Two rules,
+// suppress if EITHER (checked (a) then (b) so the reason is the most specific):
+//   (a) ARRIVAL DEDUP — a sibling record with the SAME entered date (exact string match) already
+//       has a non-empty `Date WP Sample Sent` → the sample arrived on a duplicate same-day record.
+//       (Catches REID DWRW210156: two 09/08/2026 records, one arrived 09/14. Spares GRD8806: its
+//        arrived record is 06/14/2024, a DIFFERENT entered date than the owed 09/10/2026 one.)
+//   (b) ALREADY-SENT — any record for the sku has a non-empty `Date Sample Request Letter Sent`
+//       (Steve's authoritative "10-day follow-up letter already sent" stamp; the sweep writes it on
+//        send via FIELD_2ND_DATE, so this closes the never-resend loop). NOT 'Date Email Sent...'.
+// Records with an empty entered date are noise and never trigger (a). Fail-open by design: an empty
+// siblings list => not suppressed (the human batch-send gate + 14-day Gmail anti-dup are backstops).
+const _ne = (v) => String(v == null ? '' : v).trim() !== '';
+function suppressChase(enteredDate, siblings = []) {
+  const entered = String(enteredDate == null ? '' : enteredDate).trim();
+  if (entered !== '' && siblings.some((s) => _ne(s.wpSampleSent) && String(s.entered == null ? '' : s.entered).trim() === entered))
+    return { suppress: true, reason: 'arrived (duplicate record, same entered date)' };
+  if (siblings.some((s) => _ne(s.letterSent)))
+    return { suppress: true, reason: '10-day letter already sent' };
+  return { suppress: false, reason: null };
+}
+
+module.exports = { sweep, ageDays, suppressChase };
diff --git a/scripts/scheduled-run.mjs b/scripts/scheduled-run.mjs
index c4197ca..b736018 100644
--- a/scripts/scheduled-run.mjs
+++ b/scripts/scheduled-run.mjs
@@ -30,6 +30,7 @@ const __dir = dirname(fileURLToPath(import.meta.url));
 const ROOT = join(__dir, '..');
 const { compose } = require(join(ROOT, 'lib', 'compose.js'));
 const { georgeRequest } = require(join(ROOT, 'lib', 'george-transport.js'));
+const { suppressChase } = require(join(ROOT, 'lib', 'sweep.js'));   // TK-12105 never-false-chase guard
 
 const SEND = process.argv.includes('--send');
 // Steve 8/20: DRAFT mode — create a Gmail draft per vendor in info@ Drafts for human review,
@@ -334,6 +335,34 @@ const run = async () => {
     });
   }
 
+  // TK-12105 NEVER-FALSE-CHASE guard — before chasing any sku, pull ALL FileMaker records for that
+  // combo sku (LAYOUT has entered + Date WP Sample Sent + Date Sample Request Letter Sent) and drop
+  // the row if the sample already arrived on a duplicate same-entered-date record, or a 10-day letter
+  // was already sent. Fail-open (empty siblings => keep) — the human batch-send gate + 14-day Gmail
+  // anti-dup are backstops, so an FM hiccup can never silently drop a real chase.
+  const guardSuppressed = [];
+  {
+    const skus = [...new Set(Object.values(byVid).flat().map((r) => r.sku).filter(Boolean))];
+    const sib = {};
+    for (const sku of skus) {
+      const rr = await fm.findRecords(DB, LAYOUT, { 'combo sku': sku }, { limit: 50 }).catch(catchAll);
+      sib[sku] = (rr.records || []).map((x) => ({
+        entered: x.fieldData['today for client'] || '',
+        wpSampleSent: x.fieldData['Date WP Sample Sent'] || '',
+        letterSent: x.fieldData[FIELD_2ND_DATE] || '',
+      }));
+    }
+    for (const [vid, rows] of Object.entries(byVid)) {
+      const kept = [];
+      for (const r of rows) {
+        const g = suppressChase(r.requested, sib[r.sku] || []);
+        if (g.suppress) guardSuppressed.push({ vid, sku: r.sku, mfr: r.mfr, reason: g.reason });
+        else kept.push(r);
+      }
+      if (kept.length) byVid[vid] = kept; else delete byVid[vid];
+    }
+  }
+
   const cmap = vidContactMap(Object.keys(byVid));
   // DRAFT dedup ledger — drafting ≠ sending, so we do NOT false-stamp FileMaker; we track which
   // recordIds already have a draft locally so the daily run never piles up duplicate drafts.
@@ -425,7 +454,7 @@ const run = async () => {
 
   const mode = DRAFT ? 'DRAFT' : SEND ? 'SEND' : 'DRY-RUN';
   const report = { ranAt: new Date().toISOString(), mode, window: win, stampDate: today,
-    vendorsSent: sent.length, vendorsDrafted: drafted.length, vendorsSkipped: skipped.length, vendorsNeedsConfirm: needsConfirm.length, vendorsSuppressed: suppressed.length, vendorsAlreadyDrafted: alreadyDrafted.length, vendorsStaleLedger: staleLedger.length, sent, drafted, needsConfirm, skipped, suppressed, alreadyDrafted, staleLedger };
+    vendorsSent: sent.length, vendorsDrafted: drafted.length, vendorsSkipped: skipped.length, vendorsNeedsConfirm: needsConfirm.length, vendorsSuppressed: suppressed.length, vendorsAlreadyDrafted: alreadyDrafted.length, vendorsStaleLedger: staleLedger.length, skusGuardSuppressed: guardSuppressed.length, sent, drafted, needsConfirm, skipped, suppressed, alreadyDrafted, staleLedger, guardSuppressed };
   mkdirSync(join(ROOT, 'data', 'runs'), { recursive: true });
   writeFileSync(join(ROOT, 'data', 'runs', `scheduled-${today.replace(/\//g, '-')}.json`), JSON.stringify(report, null, 2));
 
@@ -442,6 +471,7 @@ const run = async () => {
   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 (alreadyDrafted.length) { console.log(`\n= Already drafted ${alreadyDrafted.length} (draft VERIFIED still in Drafts — not duplicated):`); alreadyDrafted.forEach((s) => console.log(`  = ${(s.name||'').padEnd(30)} [${s.vid}] (${s.n} SKU${s.n>1?'s':''})`)); }
   if (staleLedger.length) { console.log(`\n\u26a0 STALE LEDGER ${staleLedger.length} — ledger claimed a draft that NO LONGER EXISTS; re-drafted:`); staleLedger.forEach((s) => console.log(`  \u26a0 ${(s.name||'').padEnd(30)} [${s.vid}] \u2192 ${s.to} (${s.n} SKU${s.n>1?'s':''})`)); }
+  if (guardSuppressed.length) { console.log(`\n\u{1F6E1} Never-false-chase guard suppressed ${guardSuppressed.length} SKU(s) (already arrived / 10-day letter already sent):`); guardSuppressed.forEach((s) => console.log(`  ⊘ [${s.vid}] ${s.sku} ${s.mfr} — ${s.reason}`)); }
   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`);
 };
diff --git a/scripts/send-reconciled.mjs b/scripts/send-reconciled.mjs
index 8587122..6fdc460 100644
--- a/scripts/send-reconciled.mjs
+++ b/scripts/send-reconciled.mjs
@@ -16,6 +16,9 @@ const { georgeRequest } = require(join(ROOT, 'lib/george-transport.js'));
 // BACK to contacts.json via the same slugForVid bridge scheduled-run.mjs uses — ONE source of
 // truth. no-chase vendors are honoured (not drafted); an unresolvable vid still reports cleanly.
 const { slugForVid, normVid } = require(join(ROOT, 'lib/slug-aliases.cjs'));
+const { suppressChase } = require(join(ROOT, 'lib/sweep.js'));   // TK-12105 never-false-chase guard
+const GUARD_LAYOUT = 'Report for old memo samples';              // has entered + WP Sample Sent + Letter Sent
+const GUARD_FIELD_LETTER = 'Date Sample Request Letter Sent';   // Steve's authoritative "10-day letter sent" stamp
 const CONTACTS = (() => { try { return JSON.parse(readFileSync(join(ROOT, 'data/contacts.json'), 'utf8')); } catch { return {}; } })();
 // Resolve a raw FileMaker vid → { name, to, acct, proc, needsConfirm } | { noChase, name } | null.
 function resolveVendor(rawVid) {
@@ -85,6 +88,33 @@ const V = {
 const items = {};
 for (const [vid, arr] of Object.entries(snap)) { const nv = normVid(vid); const key = (nv === 'BM' || nv === 'GREEN') ? 'BMGREEN' : nv; (items[key] = items[key] || []).push(...arr); }
 
+// TK-12105 NEVER-FALSE-CHASE guard — before drafting any sku, pull ALL FileMaker records for that
+// combo sku and drop the row if the sample already arrived on a duplicate same-entered-date record,
+// or a 10-day letter was already sent. Fail-open (empty siblings => keep); human batch-send gate +
+// 14-day Gmail anti-dup are backstops so an FM hiccup can never silently drop a real chase.
+const guardSuppressed = [];
+{
+  const skus = [...new Set(Object.values(items).flat().map((r) => r.sku).filter(Boolean))];
+  const sib = {};
+  for (const sku of skus) {
+    const rr = await fm.findRecords('WALLPAPER', GUARD_LAYOUT, { 'combo sku': sku }, { limit: 50 }).catch(() => ({ records: [] }));
+    sib[sku] = (rr.records || []).map((x) => ({
+      entered: x.fieldData['today for client'] || '',
+      wpSampleSent: x.fieldData['Date WP Sample Sent'] || '',
+      letterSent: x.fieldData[GUARD_FIELD_LETTER] || '',
+    }));
+  }
+  for (const [key, rows] of Object.entries(items)) {
+    const kept = [];
+    for (const r of rows) {
+      const g = suppressChase(r.req, sib[r.sku] || []);
+      if (g.suppress) guardSuppressed.push({ key, sku: r.sku, mfr: r.mfr, reason: g.reason });
+      else kept.push(r);
+    }
+    if (kept.length) items[key] = kept; else delete items[key];
+  }
+}
+
 function letter(v, rows) {
   // Steve 8/20: do NOT show our internal Ref # (DW SKU) to the vendor — Date Ordered + Mfr # only.
   const tbl = rows.map(r => `<tr style="border-bottom:1px solid #eee"><td style="padding:5px 18px 5px 0;white-space:nowrap">${esc(r.req || '—')}</td><td style="padding:5px 0">${esc(r.mfr)}</td></tr>`).join('\n');
@@ -131,4 +161,8 @@ for (const [key, rows] of Object.entries(items)) {
   console.log(`  ${r === 'ok' ? '✎' : '✗'} ${v.name.padEnd(26)} → ${v.to.padEnd(40)} (${fresh.length})${cflag} ${r === 'ok' ? '' : r}`);
   if (r === 'ok') for (const row of fresh) ledger[`${key}:${row.sku}`] = stamp;
 }
+if (guardSuppressed.length) {
+  console.log(`\n\u{1F6E1} Never-false-chase guard suppressed ${guardSuppressed.length} SKU(s) (already arrived / 10-day letter already sent):`);
+  for (const s of guardSuppressed) console.log(`  ⊘ [${s.key}] ${s.sku} ${s.mfr} — ${s.reason}`);
+}
 if (DO) writeFileSync(LEDGER, JSON.stringify(ledger, null, 2));
diff --git a/test/never-false-chase.test.cjs b/test/never-false-chase.test.cjs
new file mode 100644
index 0000000..afda8a6
--- /dev/null
+++ b/test/never-false-chase.test.cjs
@@ -0,0 +1,48 @@
+'use strict';
+// TK-12105 never-false-chase guard unit test. Fixtures mirror live FileMaker data.
+const assert = require('node:assert');
+const { suppressChase } = require('../lib/sweep');
+
+// 1. REID-like: same SKU, two records same entered date 09/08/2026, one arrived -> SUPPRESSED (arrival dedup)
+{
+  const siblings = [
+    { entered: '', wpSampleSent: '', letterSent: '' },                        // noise record (empty entered)
+    { entered: '09/08/2026', wpSampleSent: '09/14/2026', letterSent: '' },    // arrived twin
+    { entered: '09/08/2026', wpSampleSent: '', letterSent: '' },              // the outstanding one we'd chase
+  ];
+  const g = suppressChase('09/08/2026', siblings);
+  assert.equal(g.suppress, true, '1 REID must be suppressed');
+  assert.equal(g.reason, 'arrived (duplicate record, same entered date)', '1 reason must be arrival dedup');
+  console.log('(1) PASS — REID same-date arrived twin -> SUPPRESSED:', g.reason);
+}
+
+// 2. GRD-like: 2024 arrived + 2026 outstanding (different entered dates) -> NOT suppressed
+{
+  const siblings = [
+    { entered: '', wpSampleSent: '', letterSent: '' },                        // noise
+    { entered: '06/14/2024', wpSampleSent: '06/14/2024', letterSent: '' },    // arrived, 2024
+    { entered: '09/10/2026', wpSampleSent: '', letterSent: '' },              // owed, 2026
+  ];
+  const g = suppressChase('09/10/2026', siblings);
+  assert.equal(g.suppress, false, '2 GRD-2026 must be chaseable');
+  console.log('(2) PASS — GRD 2024-arrived vs 2026-owed (different dates) -> CHASEABLE');
+}
+
+// 3. Already-sent: Date Sample Request Letter Sent filled -> SUPPRESSED
+{
+  const siblings = [ { entered: '09/10/2026', wpSampleSent: '', letterSent: '09/12/2026' } ];
+  const g = suppressChase('09/10/2026', siblings);
+  assert.equal(g.suppress, true, '3 letter-sent must be suppressed');
+  assert.equal(g.reason, '10-day letter already sent', '3 reason must be already-sent');
+  console.log('(3) PASS — 10-day letter already sent -> SUPPRESSED:', g.reason);
+}
+
+// 4. Clean: not arrived on any record, no letter sent -> chaseable
+{
+  const siblings = [ { entered: '09/10/2026', wpSampleSent: '', letterSent: '' } ];
+  const g = suppressChase('09/10/2026', siblings);
+  assert.equal(g.suppress, false, '4 clean must be chaseable');
+  console.log('(4) PASS — clean (no arrival, no letter) -> CHASEABLE');
+}
+
+console.log('\nALL 4 ASSERTIONS PASSED');

← 0269ad6 auto-data-snapshot: 2026-09-23T14:48:42 (2 data files) — dat  ·  back to Sample Followup Sweep  ·  sample-followup: draft the 5 verified-owed 10-day SKUs + CON 6c9b709 →