[object Object]

← back to Sample Followup Sweep

fix(window): query full active [today-60..today-10] cohort every run

5374f08065efcf92014ac296455d01f731880c2f · 2026-09-22 11:07:18 -0700 · Steve Abrams

The auto-widen anchor only reached back to the last SUCCESSFUL run
(lastCoveredHi=09/02 -> lo 09/03), so older still-outstanding, still-unchased
memos (07/24-09/02) fell outside the window and were never chased; a run of
missed days made the hole permanent. send-reconciled used a today-16..today-10
band with the same defect. Both now query the full active window; the FileMaker
Sent-stamp set-difference (scheduled-run) / draft-ledger (send-reconciled) is the
dedup, so re-covering already-chased memos is a no-op and a missed run day can
never drop the older cohort.

TK-12014 / TK-12013
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBN9bgoXhsUaDnGkXU1T6m

Files touched

Diff

commit 5374f08065efcf92014ac296455d01f731880c2f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Sep 22 11:07:18 2026 -0700

    fix(window): query full active [today-60..today-10] cohort every run
    
    The auto-widen anchor only reached back to the last SUCCESSFUL run
    (lastCoveredHi=09/02 -> lo 09/03), so older still-outstanding, still-unchased
    memos (07/24-09/02) fell outside the window and were never chased; a run of
    missed days made the hole permanent. send-reconciled used a today-16..today-10
    band with the same defect. Both now query the full active window; the FileMaker
    Sent-stamp set-difference (scheduled-run) / draft-ledger (send-reconciled) is the
    dedup, so re-covering already-chased memos is a no-op and a missed run day can
    never drop the older cohort.
    
    TK-12014 / TK-12013
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01UBN9bgoXhsUaDnGkXU1T6m
---
 scripts/scheduled-run.mjs   | 28 ++++++++++++++--------------
 scripts/send-reconciled.mjs |  9 ++++++---
 2 files changed, 20 insertions(+), 17 deletions(-)

diff --git a/scripts/scheduled-run.mjs b/scripts/scheduled-run.mjs
index 68a1974..2fe336f 100644
--- a/scripts/scheduled-run.mjs
+++ b/scripts/scheduled-run.mjs
@@ -52,6 +52,14 @@ const FIELD_SENT = 'Date Email Sent to Vendor after 10 Days';
 // "2nd request date" field is exposed to the Data API; this real date field is on the same layout).
 const FIELD_2ND_DATE = 'Date Sample Request Letter Sent';
 const MIN_AGE_DAYS = 10;
+// Steve 2026-09-22 (TK-12013/TK-12014): the ACTIVE follow-up cohort is EVERY outstanding memo aged
+// [MIN_AGE_DAYS .. MAX_AGE_DAYS] inclusive — not a narrow "just crossed 10d since last run" band. The
+// old auto-widen anchor only reached back to the last SUCCESSFUL run, so any older still-outstanding,
+// still-unchased memo fell outside the window and was NEVER chased (and a run of missed days made the
+// hole permanent). We now query the full active window every run; the FileMaker Sent-stamp set-difference
+// (B) is the dedup, so re-covering already-chased memos is a no-op and a missed run day can never drop
+// the older cohort. >MAX_AGE_DAYS = dead lead (Steve's rule), excluded by the lo floor.
+const MAX_AGE_DAYS = Number(process.env.MAX_AGE) || 60;
 // Steve 9/01 (FINAL RULE — weekday-aware catch-up): the schedule is Tue–Fri (Mon/Sat/Sun never run).
 // A memo is chased on the day it turns EXACTLY 10 days old (entered today-10) — chased TODAY, not
 // deferred. The only wrinkle is the weekend: Sat/Sun/Mon have no run, so their turns-10 cohorts wait
@@ -123,21 +131,13 @@ function windowRange(today = new Date()) {
     const lo = new Date(today); lo.setDate(lo.getDate() - MIN_AGE_DAYS - (CATCHUP_DAYS - 1));
     return `${fmtDate(lo)}...${fmtDate(hi)}`;
   }
-  // hi = today's turns-10 cohort (entered today-10) — always chased today.
+  // Active follow-up window = requests aged [MIN_AGE_DAYS .. MAX_AGE_DAYS], INCLUSIVE.
+  //   hi = today - MIN_AGE_DAYS  → includes the exactly-10-day cohort (FileMaker `lo...hi` range is inclusive).
+  //   lo = today - MAX_AGE_DAYS  → the 60-day dead-lead floor.
+  // Full cohort every run; the Sent-stamp set-difference is the dedup (see MAX_AGE_DAYS note above),
+  // so this is robust to missed run days — nothing in the active window is ever silently dropped.
   const hi = new Date(today); hi.setDate(hi.getDate() - MIN_AGE_DAYS);
-  // lo = the day AFTER the last successful run's newest covered date → auto-widens over a missed run.
-  //   healthy Tue: last success = Fri → lo = Sat-10 → [today-13..today-10] (Sat/Sun/Mon+Tue).
-  //   healthy Wed/Thu/Fri: last success = yesterday → single day today-10.
-  //   missed Thursday: Friday's last success = Wednesday → lo sweeps in Thu's missed cohort too.
-  // Cold start / no history → weekday fallback (prev scheduled run + 1, -10).
-  const anchor = lastCoveredHi(today);
-  let lo;
-  if (anchor) { lo = new Date(anchor); lo.setDate(lo.getDate() + 1); }
-  else { const prev = prevScheduledRun(today); lo = new Date(prev); lo.setDate(lo.getDate() + 1 - MIN_AGE_DAYS); }
-  // Floor: never widen more than MAX_CATCHUP days below hi (long-outage guard).
-  const floor = new Date(hi); floor.setDate(floor.getDate() - (MAX_CATCHUP - 1));
-  if (lo < floor) lo = floor;
-  if (lo > hi) lo = new Date(hi);   // degenerate (same-day re-cover) → single day
+  const lo = new Date(today); lo.setDate(lo.getDate() - MAX_AGE_DAYS);
   return `${fmtDate(lo)}...${fmtDate(hi)}`;
 }
 const norm = (s) => String(s || '').toLowerCase().trim();
diff --git a/scripts/send-reconciled.mjs b/scripts/send-reconciled.mjs
index 044929e..3f6c293 100644
--- a/scripts/send-reconciled.mjs
+++ b/scripts/send-reconciled.mjs
@@ -13,9 +13,12 @@ const { georgeRequest } = require(join(ROOT, 'lib/george-transport.js'));
 // below stops the same memo being drafted twice across runs.)
 const _pad = (n) => String(n).padStart(2, '0');
 const _fmt = (d) => `${_pad(d.getMonth() + 1)}/${_pad(d.getDate())}/${d.getFullYear()}`;
-const _MIN_AGE = 10, _CATCHUP = Number(process.env.CATCHUP) || 7;
-const _hi = new Date(); _hi.setDate(_hi.getDate() - _MIN_AGE);
-const _lo = new Date(); _lo.setDate(_lo.getDate() - _MIN_AGE - (_CATCHUP - 1));
+// Steve 2026-09-22 (TK-12013/TK-12014): full active window [today-MAX_AGE .. today-MIN_AGE] inclusive,
+// NOT a narrow rolling catch-up band — so no older still-outstanding memo is ever dropped. Draft-dedup
+// (reconciled-draft-ledger keyed by vendor+combo-sku) makes re-covering already-drafted memos a no-op.
+const _MIN_AGE = 10, _MAX_AGE = Number(process.env.MAX_AGE) || 60;
+const _hi = new Date(); _hi.setDate(_hi.getDate() - _MIN_AGE);   // includes exactly-10d cohort (inclusive range)
+const _lo = new Date(); _lo.setDate(_lo.getDate() - _MAX_AGE);   // 60-day dead-lead floor
 const WIN = process.env.WIN || `${_fmt(_lo)}...${_fmt(_hi)}`;
 
 // --- FileMaker: pull outstanding (Date WP Sample Sent empty) in WIN, group by vid ---

← fcd34dc fix(sweep): include exactly-10-day-old requests (age < minAg  ·  back to Sample Followup Sweep  ·  auto-data-snapshot: 2026-09-22T11:11:00 (3 data files) — dat 14acdb8 →