← 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
M data/runs/scheduled-09-23-2026.jsonM lib/sweep.jsM scripts/scheduled-run.mjsM scripts/send-reconciled.mjsA test/never-false-chase.test.cjs
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 →