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