[object Object]

← back to Sample Followup Sweep

sample-followup: auto-widen window to last successful run (belt-and-suspenders)

38d86d19124cc754ba80da24e1dbd9a8fb82de89 · 2026-09-01 10:48:25 -0700 · Steve

Anchor the window's lo to real run history instead of the calendar: lo = day
after the newest entered-date the last DRAFT/SEND run covered (dry-runs excluded).
A silently-missed Thursday is now swept up by Friday automatically. Healthy case
reproduces the weekday band exactly (Tue anchors to Fri -> Sat/Sun/Mon+Tue).
Floor caps widening at MAX_CATCHUP=14d; cold-start falls back to the weekday calc.
Verified: healthy Tue -> 08/19..08/22; simulated missed Thu -> Fri widens to
08/31..09/01. Codex-reviewed. Dedup keeps any re-cover harmless.

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

Files touched

Diff

commit 38d86d19124cc754ba80da24e1dbd9a8fb82de89
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Sep 1 10:48:25 2026 -0700

    sample-followup: auto-widen window to last successful run (belt-and-suspenders)
    
    Anchor the window's lo to real run history instead of the calendar: lo = day
    after the newest entered-date the last DRAFT/SEND run covered (dry-runs excluded).
    A silently-missed Thursday is now swept up by Friday automatically. Healthy case
    reproduces the weekday band exactly (Tue anchors to Fri -> Sat/Sun/Mon+Tue).
    Floor caps widening at MAX_CATCHUP=14d; cold-start falls back to the weekday calc.
    Verified: healthy Tue -> 08/19..08/22; simulated missed Thu -> Fri widens to
    08/31..09/01. Codex-reviewed. Dedup keeps any re-cover harmless.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/scheduled-run.mjs | 67 +++++++++++++++++++++++++++++++++++------------
 1 file changed, 50 insertions(+), 17 deletions(-)

diff --git a/scripts/scheduled-run.mjs b/scripts/scheduled-run.mjs
index 1c48a39..912587f 100644
--- a/scripts/scheduled-run.mjs
+++ b/scripts/scheduled-run.mjs
@@ -16,7 +16,7 @@
 //   node scripts/scheduled-run.mjs            # dry run
 //   node scripts/scheduled-run.mjs --send     # actually send + stamp
 
-import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
+import { readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
 import { fileURLToPath } from 'node:url';
 import { dirname, join } from 'node:path';
 import { homedir } from 'node:os';
@@ -49,13 +49,21 @@ const MIN_AGE_DAYS = 10;
 //   • Tuesday: the last run was Friday and Sat/Sun/Mon didn't run → ONE letter covering Sat, Sun, Mon
 //     AND today (Tue) → entered band [Sat-10 .. Tue-10] = [today-13 .. today-10]. (Steve, verbatim:
 //     "on tuesday send letter for sat, sun, monday, and today tuesday in 1 letter to vendors.")
-// General form: cover the turns-10 cohorts for the days (previous-scheduled-run .. today] inclusive of
-// today. Coverage is gapless and non-overlapping week-to-week (verified over 2 weeks). The old code
-// IGNORED the weekday and applied a flat CATCHUP_DAYS band every run — that's the bug this replaces.
+// General form: cover the turns-10 cohorts for the days (last-successful-run .. today] inclusive of
+// today — anchored to real RUN HISTORY, not the calendar, so a missed run auto-widens the next one.
+// Coverage is gapless and non-overlapping week-to-week (verified). The old code IGNORED the weekday
+// and applied a flat CATCHUP_DAYS band every run — that's the bug this replaces.
 // Env CATCHUP (manual override): if set, revert to the old fixed rolling band [today-10-(N-1)..today-10]
-// as an explicit wide recovery net (e.g. after a silently-missed run). Dedup makes the overlap safe.
+// as an explicit wide recovery net. Dedup makes any overlap safe.
 const RUN_WEEKDAYS = new Set([2, 3, 4, 5]);   // Tue(2) Wed(3) Thu(4) Fri(5)
-const CATCHUP_DAYS = Number(process.env.CATCHUP) || 0;   // 0 = use the weekday-aware band (default)
+const CATCHUP_DAYS = Number(process.env.CATCHUP) || 0;   // 0 = use the auto-widen band (default)
+// Auto-widen recovery (belt-and-suspenders, Steve 9/01): each run starts the day AFTER the newest
+// entered-date the last SUCCESSFUL run covered (its report in data/runs, mode DRAFT/SEND). So a
+// silently-missed Thursday is swept up by Friday automatically — no manual CATCHUP needed. In the
+// healthy case this reproduces the weekday band exactly (on Tue the last success was Fri → Sat/Sun/
+// Mon+Tue). Capped at MAX_CATCHUP days so a long outage can't blast the whole back-catalog; dedup
+// (FileMaker Sent-stamp + draft-ledger) makes any re-covered overlap harmless.
+const MAX_CATCHUP = Number(process.env.MAX_CATCHUP) || 14;
 const SHIP_TO = { name: 'Designer Wallcoverings', line1: '15442 Ventura Blvd. #102', city_state_zip: 'Sherman Oaks, CA 91403', phone: '1-888-373-4564' };
 
 // --- FileMaker client (reuse the connector; creds from ~/.claude.json) ---
@@ -70,28 +78,53 @@ const fm = await import('/Users/macstudio3/Projects/filemaker-mcp/src/fm-client.
 
 const pad = (n) => String(n).padStart(2, '0');
 const fmtDate = (d) => `${pad(d.getMonth() + 1)}/${pad(d.getDate())}/${d.getFullYear()}`;
-// Most recent scheduled run day strictly before `today` (walks back over weekend / off days).
+// Most recent SCHEDULED run day strictly before `today` (walks back over weekend / off days).
+// Cold-start fallback only — the live anchor is real run history (lastCoveredHi).
 function prevScheduledRun(today) {
   const d = new Date(today);
   do { d.setDate(d.getDate() - 1); } while (!RUN_WEEKDAYS.has(d.getDay()));
   return d;
 }
+const parseMDY = (s) => { const m = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(String(s || '').trim()); return m ? new Date(+m[3], +m[1] - 1, +m[2]) : null; };
+// Newest entered-date covered by a prior SUCCESSFUL run (mode DRAFT/SEND) strictly before `today`.
+// Dry-runs cover nothing (excluded), so they never advance the anchor. Returns a Date or null.
+function lastCoveredHi(today) {
+  let dir, files;
+  try { dir = join(ROOT, 'data', 'runs'); files = readdirSync(dir); } catch { return null; }
+  const midToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
+  let best = null;
+  for (const f of files) {
+    if (!f.startsWith('scheduled-') || !f.endsWith('.json')) continue;
+    let rep; try { rep = JSON.parse(readFileSync(join(dir, f), 'utf8')); } catch { continue; }
+    if (rep.mode !== 'DRAFT' && rep.mode !== 'SEND') continue;          // dry-runs cover nothing
+    const stamp = parseMDY(rep.stampDate); if (!stamp || stamp >= midToday) continue;   // strictly prior
+    const hi = parseMDY((rep.window || '').split('...')[1]);
+    if (hi && (!best || hi > best)) best = hi;
+  }
+  return best;
+}
 function windowRange(today = new Date()) {
   if (CATCHUP_DAYS > 0) {
-    // Manual override — old fixed rolling band [today-10-(N-1) .. today-10].
+    // Manual override — fixed rolling band [today-10-(N-1) .. today-10].
     const hi = new Date(today); hi.setDate(hi.getDate() - MIN_AGE_DAYS);
     const lo = new Date(today); lo.setDate(lo.getDate() - MIN_AGE_DAYS - (CATCHUP_DAYS - 1));
     return `${fmtDate(lo)}...${fmtDate(hi)}`;
   }
-  // Default weekday-aware band: cover the turns-10 cohorts for the days (prevScheduledRun .. today],
-  // inclusive of today — today's cohort is chased TODAY, and the weekend's cohorts fold into Tuesday.
-  //   hi = today - 10                    (today's turns-10 cohort — always chased today)
-  //   lo = (prevScheduledRun + 1) - 10   (first turns-10 cohort the last run didn't already cover)
-  // Tue → prevRun=Fri → [today-13 .. today-10] = Sat/Sun/Mon/Tue each -10, one letter per vendor.
-  // Wed/Thu/Fri → prevRun=yesterday → single day today-10.
-  const prev = prevScheduledRun(today);
+  // hi = today's turns-10 cohort (entered today-10) — always chased today.
   const hi = new Date(today); hi.setDate(hi.getDate() - MIN_AGE_DAYS);
-  const lo = new Date(prev);  lo.setDate(lo.getDate() + 1 - 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
   return `${fmtDate(lo)}...${fmtDate(hi)}`;
 }
 const norm = (s) => String(s || '').toLowerCase().trim();
@@ -232,7 +265,7 @@ const run = async () => {
   mkdirSync(join(ROOT, 'data', 'runs'), { recursive: true });
   writeFileSync(join(ROOT, 'data', 'runs', `scheduled-${today.replace(/\//g, '-')}.json`), JSON.stringify(report, null, 2));
 
-  const bandDesc = CATCHUP_DAYS > 0 ? `${CATCHUP_DAYS}-day manual catch-up band` : 'weekday-aware band (Tue covers Sat/Sun/Mon+Tue)';
+  const bandDesc = CATCHUP_DAYS > 0 ? `${CATCHUP_DAYS}-day manual catch-up band` : 'auto-widen band (resumes after last successful run)';
   console.log(`Scheduled follow-up — ${mode} · window ${win} (>=${MIN_AGE_DAYS}d, ${bandDesc})\n`);
   if (DRAFT) {
     console.log(`Created ${drafted.length} draft(s) in info@ Drafts for review:`);

← f546361 sample-followup: chase today's 10-day cohort today; Tue fold  ·  back to Sample Followup Sweep  ·  auto-data-snapshot: 2026-09-01T10:56:56 (1 data files) — dat 3d869fe →