← back to Sample Followup Sweep
sample-followup: canary no longer counts deferred recipient-busy rows as covered (TK-12119)
32a76cf28e878556dddf8f54161217756bfdc5b1 · 2026-09-24 08:00:53 -0700 · Steve Abrams
Split the recipient-busy suppression by age: a fresh batching deferral (<=24d =
10-day trigger + one 14-day window) counts as covered, but a row deferred past
that window escalates to recipientBusyStale -> uncovered -> WARN. Closes the
false-green where a perpetually-recontacted vendor's never-chased sku hid as
"covered." First live run surfaced 33 real stale-deferred rows. Ships a negative
test proving the aged row flips from the old covered/PASS to WARN.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M lib/sweep.jsM scripts/coverage-reconcile.mjsM test/coverage-reconcile.test.cjs
Diff
commit 32a76cf28e878556dddf8f54161217756bfdc5b1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 24 08:00:53 2026 -0700
sample-followup: canary no longer counts deferred recipient-busy rows as covered (TK-12119)
Split the recipient-busy suppression by age: a fresh batching deferral (<=24d =
10-day trigger + one 14-day window) counts as covered, but a row deferred past
that window escalates to recipientBusyStale -> uncovered -> WARN. Closes the
false-green where a perpetually-recontacted vendor's never-chased sku hid as
"covered." First live run surfaced 33 real stale-deferred rows. Ships a negative
test proving the aged row flips from the old covered/PASS to WARN.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
lib/sweep.js | 37 +++++++++++++++++++++++++++++--------
scripts/coverage-reconcile.mjs | 5 +++--
test/coverage-reconcile.test.cjs | 27 +++++++++++++++++++++------
3 files changed, 53 insertions(+), 16 deletions(-)
diff --git a/lib/sweep.js b/lib/sweep.js
index ce118ce..78fb6c0 100644
--- a/lib/sweep.js
+++ b/lib/sweep.js
@@ -90,13 +90,28 @@ function suppressChase(enteredDate, siblings = []) {
// grouping) = LEAKED = a silent skip = FAIL. This makes "ran successfully while chasing only a
// fraction" impossible: PASS requires the FM population to be fully accounted AND every row in an
// actioned bucket — never PASS merely because the script ran.
-// buckets: chaseable | suppressed14d | needsConfirm | guardSuppressed | noVendorMap | noChase
-// actioned (covered) = chaseable + suppressed14d + guardSuppressed + noChase
-// uncovered (accounted but NOT actually chased) = noVendorMap + needsConfirm -> WARN
+// buckets: chaseable | recipientBusy14d | recipientBusyStale | needsConfirm | guardSuppressed | noVendorMap | noChase
+// actioned (covered) = chaseable + recipientBusy14d + guardSuppressed + noChase
+// uncovered (accounted but NOT actually chased) = noVendorMap + needsConfirm + recipientBusyStale -> WARN
// leaked (outstanding but in no bucket) -> FAIL
+// TK-12119 (Steve false-green doctrine, deferred-bucket half): a row lands in the recipient-busy path
+// ONLY when the vendor's address was emailed within 14d for SOME sku but THIS sku's 10-day letter was
+// never sent (a genuinely-sent sku is already caught by suppressChase -> guardSuppressed, before here).
+// That bucket is a DELAY, not an action, so it must NOT count as "done" indefinitely: a fresh deferral
+// (entered-age <= RECIPIENT_BUSY_STALE_DAYS = the 10-day trigger + one 14-day batching window) is
+// acceptable batching and counts as covered; a row still deferred PAST that window has missed its
+// chase and escalates to recipientBusyStale -> uncovered -> WARN, so a perpetually-recontacted vendor
+// can no longer hide a never-chased sku behind a green canary.
// records: array of { fieldData: {...} } (FileMaker shape) OR plain field objects (fixtures).
-function reconcileCoverage(records, { contacts = {}, recentSet = new Set(), runStale = false } = {}) {
+const RECIPIENT_BUSY_STALE_DAYS = 24; // 10-day trigger + one full 14-day batching window
+function reconcileCoverage(records, { contacts = {}, recentSet = new Set(), runStale = false, today = new Date() } = {}) {
const norm = (v) => String(v == null ? '' : v).replace(/[\r\n]/g, '').trim();
+ // entered is FileMaker MM/DD/YYYY here (ageDays() takes ISO), so parse locally.
+ const ageMDY = (mdy) => {
+ const m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(mdy || '');
+ if (!m) return null;
+ return Math.floor((today.getTime() - new Date(+m[3], +m[1] - 1, +m[2]).getTime()) / 86400000);
+ };
const fd = (r) => (r && r.fieldData) ? r.fieldData : (r || {});
// siblings by sku (all in-window records) for the never-false-chase guard
const siblings = {};
@@ -104,7 +119,7 @@ function reconcileCoverage(records, { contacts = {}, recentSet = new Set(), runS
const d = fd(r); const sku = norm(d['combo sku']); if (!sku) continue;
(siblings[sku] = siblings[sku] || []).push({ entered: norm(d['today for client']), wpSampleSent: norm(d['Date WP Sample Sent']), letterSent: norm(d['Date Sample Request Letter Sent']) });
}
- const b = { chaseable: [], suppressed14d: [], needsConfirm: [], guardSuppressed: [], noVendorMap: [], noChase: [] };
+ const b = { chaseable: [], recipientBusy14d: [], recipientBusyStale: [], needsConfirm: [], guardSuppressed: [], noVendorMap: [], noChase: [] };
const leaked = [];
let population = 0;
for (const r of records) {
@@ -125,14 +140,20 @@ function reconcileCoverage(records, { contacts = {}, recentSet = new Set(), runS
const email = c.sample_email || c.main_email;
if (!email || !c.account_number) { b.noVendorMap.push({ ...tag, why: 'incomplete contact (no email/account)' }); continue; }
const recips = String(email).toLowerCase().split(/[,;]\s*/).map((s) => s.trim()).filter(Boolean);
- if (recips.some((e) => recentSet.has(e))) { b.suppressed14d.push(tag); continue; }
+ if (recips.some((e) => recentSet.has(e))) {
+ const age = ageMDY(entered);
+ const busyTag = { ...tag, ageDays: age };
+ if (age != null && age > RECIPIENT_BUSY_STALE_DAYS) b.recipientBusyStale.push(busyTag); // deferred past its window -> WARN
+ else b.recipientBusy14d.push(busyTag); // fresh batching deferral -> covered
+ continue;
+ }
if (c.needs_confirm) { b.needsConfirm.push(tag); continue; }
b.chaseable.push(tag);
}
const counts = Object.fromEntries(Object.entries(b).map(([k, v]) => [k, v.length]));
const accounted = Object.values(counts).reduce((s, n) => s + n, 0);
- const covered = counts.chaseable + counts.suppressed14d + counts.guardSuppressed + counts.noChase;
- const uncovered = counts.noVendorMap + counts.needsConfirm;
+ const covered = counts.chaseable + counts.recipientBusy14d + counts.guardSuppressed + counts.noChase;
+ const uncovered = counts.noVendorMap + counts.needsConfirm + counts.recipientBusyStale;
const reconciles = (accounted + leaked.length === population);
let verdict;
if (leaked.length > 0 || !reconciles) verdict = 'FAIL'; // silent-skip / reconciliation broke
diff --git a/scripts/coverage-reconcile.mjs b/scripts/coverage-reconcile.mjs
index bd8a9f0..8fbc183 100644
--- a/scripts/coverage-reconcile.mjs
+++ b/scripts/coverage-reconcile.mjs
@@ -107,8 +107,9 @@ async function main() {
console.log(`Coverage reconciliation — window ${window}`);
console.log(` population (outstanding in FM): ${r.population}`);
- console.log(` covered/actioned: ${r.covered} (chaseable ${r.counts.chaseable} + suppressed14d ${r.counts.suppressed14d} + guardSuppressed ${r.counts.guardSuppressed} + noChase ${r.counts.noChase})`);
- console.log(` uncovered (accounted, NOT chased): ${r.uncovered} (noVendorMap ${r.counts.noVendorMap} + needsConfirm ${r.counts.needsConfirm})`);
+ console.log(` covered/actioned: ${r.covered} (chaseable ${r.counts.chaseable} + recipientBusy14d ${r.counts.recipientBusy14d} + guardSuppressed ${r.counts.guardSuppressed} + noChase ${r.counts.noChase})`);
+ if (r.counts.recipientBusyStale) console.log(` recipientBusyStale: ${r.counts.recipientBusyStale} (deferred > ${24}d — vendor recontacted but THIS sku never chased → WARN, was a false-green)`);
+ console.log(` uncovered (accounted, NOT chased): ${r.uncovered} (noVendorMap ${r.counts.noVendorMap} + needsConfirm ${r.counts.needsConfirm} + recipientBusyStale ${r.counts.recipientBusyStale})`);
console.log(` leaked (outstanding, NO bucket): ${r.leaked.length}`);
if (r.leaked.length) r.leaked.slice(0, 10).forEach((x) => console.log(` ⚠ LEAK vid=${JSON.stringify(x.vid)} sku=${x.sku} — ${x.why}`));
console.log(` reconciles (accounted+leaked==population): ${r.reconciles} run-stale: ${runStale}`);
diff --git a/test/coverage-reconcile.test.cjs b/test/coverage-reconcile.test.cjs
index 065195e..a55bc5b 100644
--- a/test/coverage-reconcile.test.cjs
+++ b/test/coverage-reconcile.test.cjs
@@ -52,18 +52,33 @@ const row = (vid, sku, entered, wp = '', letter = '') => ({ fieldData: {
console.log('(4) PASS — clean population but run missed on run-day -> WARN');
}
-// 5. GUARD/RECENT reconcile — arrived-dup guard + recently-emailed both count as ACTIONED (covered) → PASS
+// 5. GUARD/RECENT reconcile — arrived-dup guard + FRESH recipient-busy deferral (<=24d) both count as ACTIONED (covered) → PASS
{
+ const today = new Date(2026, 8, 24); // 2026-09-24, deterministic (Sep = month 8)
const recs = [
- row('FABR', 'DWFC5', '09/08/2026'), // outstanding
+ row('FABR', 'DWFC5', '09/08/2026'), // outstanding, resolvable, not recently emailed -> chaseable
row('FABR', 'DWFC5', '09/08/2026', '09/14/2026'), // arrived same-date twin -> guard suppress
- row('WAT', 'DWW5', '09/10/2026'), // needs_confirm vendor BUT recently emailed -> suppressed14d (covered)
+ row('WAT', 'DWW5', '09/12/2026'), // recently emailed, 12d old (fresh) -> recipientBusy14d (covered batching)
];
- const r = reconcileCoverage(recs, { contacts, recentSet: new Set(['sophie@watts1874.co.uk']) });
+ const r = reconcileCoverage(recs, { contacts, recentSet: new Set(['sophie@watts1874.co.uk']), today });
assert.equal(r.counts.guardSuppressed, 1, '5 arrived twin -> guardSuppressed');
- assert.equal(r.counts.suppressed14d, 1, '5 recently-emailed -> suppressed14d (covered, not needsConfirm WARN)');
+ assert.equal(r.counts.recipientBusy14d, 1, '5 fresh recipient-busy -> recipientBusy14d (covered, not WARN)');
assert.equal(r.verdict, 'PASS', `5 all actioned must PASS, got ${r.verdict}`);
- console.log('(5) PASS — guard-suppressed + recently-emailed both actioned -> PASS');
+ console.log('(5) PASS — guard-suppressed + fresh recipient-busy deferral both actioned -> PASS');
+}
+
+// 6. NEGATIVE (TK-12119) — a recipient-busy deferral aged PAST the batching window (>24d) must NOT hide
+// as covered. Before this fix it was suppressed14d=covered=PASS (a false green for a never-chased sku
+// of a perpetually-recontacted vendor); now it escalates to recipientBusyStale -> uncovered -> WARN.
+{
+ const today = new Date(2026, 8, 24); // 2026-09-24
+ const recs = [ row('WAT', 'DWW6', '08/01/2026') ]; // recently-emailed vendor, but THIS sku entered 54d ago, never chased
+ const r = reconcileCoverage(recs, { contacts, recentSet: new Set(['sophie@watts1874.co.uk']), today });
+ assert.equal(r.counts.recipientBusy14d, 0, '6 stale deferral must NOT count as fresh/covered');
+ assert.equal(r.counts.recipientBusyStale, 1, '6 stale deferral -> recipientBusyStale');
+ assert.ok(r.uncovered >= 1, '6 stale deferral counts as uncovered');
+ assert.equal(r.verdict, 'WARN', `6 stale deferral must WARN (was a false-green PASS), got ${r.verdict}`);
+ console.log(`(6) PASS — recipient-busy aged >24d -> WARN not covered (recipientBusyStale=${r.counts.recipientBusyStale}, was suppressed14d/PASS)`);
}
console.log('\nALL COVERAGE-RECONCILE ASSERTIONS PASSED');
← 4b93811 auto-data-snapshot: 2026-09-24T07:06:26 (2 data files) — dat
·
back to Sample Followup Sweep
·
auto-data-snapshot: 2026-09-24T08:09:22 (3 data files) — dat d70e93d →