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