← back to Sample Followup Sweep
TK-11409: verify the draft-ledger against reality; surface both silent draft-mode outcomes
ed3bc8de9710e9cf7a6c0e823915ec674d9bf1ee · 2026-09-10 11:19:10 -0700 · Steve Abrams
Third instance of this ticket's failure family: a PROXY trusted without verification.
The draft-ledger recorded 'already drafted' for Osborne & Little, Thibaut and York
while ZERO drafts for them existed in info@ Drafts. 'if (!fresh.length) continue'
then skipped them SILENTLY -- no report line at all -- so those 8 memos were
permanently invisible: never re-drafted, never reported, never chased.
- georgeHasDraft(to) checks whether a chase draft still exists before trusting the
ledger. On ANY error it returns true (assume present), so a George hiccup can
never cause a burst of duplicate drafts.
- A stale claim is dropped and the vendor re-drafted.
- Two previously-silent outcomes now appear in the report and console:
alreadyDrafted (verified present, correctly not duplicated) and staleLedger.
Verified live: 3 re-drafted (OSB/THIB/YOR, 8 SKUs), 9 correctly left alone as
verified-still-present. Nothing sent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Files touched
M scripts/scheduled-run.mjs
Diff
commit ed3bc8de9710e9cf7a6c0e823915ec674d9bf1ee
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 10 11:19:10 2026 -0700
TK-11409: verify the draft-ledger against reality; surface both silent draft-mode outcomes
Third instance of this ticket's failure family: a PROXY trusted without verification.
The draft-ledger recorded 'already drafted' for Osborne & Little, Thibaut and York
while ZERO drafts for them existed in info@ Drafts. 'if (!fresh.length) continue'
then skipped them SILENTLY -- no report line at all -- so those 8 memos were
permanently invisible: never re-drafted, never reported, never chased.
- georgeHasDraft(to) checks whether a chase draft still exists before trusting the
ledger. On ANY error it returns true (assume present), so a George hiccup can
never cause a burst of duplicate drafts.
- A stale claim is dropped and the vendor re-drafted.
- Two previously-silent outcomes now appear in the report and console:
alreadyDrafted (verified present, correctly not duplicated) and staleLedger.
Verified live: 3 re-drafted (OSB/THIB/YOR, 8 SKUs), 9 correctly left alone as
verified-still-present. Nothing sent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
scripts/scheduled-run.mjs | 42 +++++++++++++++++++++++++++++++++++++++---
1 file changed, 39 insertions(+), 3 deletions(-)
diff --git a/scripts/scheduled-run.mjs b/scripts/scheduled-run.mjs
index 22a6121..01a47c8 100644
--- a/scripts/scheduled-run.mjs
+++ b/scripts/scheduled-run.mjs
@@ -177,6 +177,29 @@ function georgeSend(payload) {
return georgeRequest({ path: '/api/send', payload, headers: { Authorization: auth, 'X-Send-Approval': token } })
.then(({ body }) => { try { const j = JSON.parse(body); return { ok: !!j.success, id: j.messageId || '', detail: body.slice(0, 200) }; } catch { return { ok: false, detail: body.slice(0, 200) }; } });
}
+// TK-11409: does a chase draft for this vendor ACTUALLY still exist in info@ Drafts?
+// The draft-ledger is only a PROXY for "we already drafted this" and nothing ever verified it.
+// Osborne & Little, Thibaut and York were all ledgered as drafted (08/21-09/01) while ZERO drafts
+// for them existed — so `if (!fresh.length) continue` skipped them SILENTLY, with no report line,
+// making those memos permanently invisible: never re-drafted, never reported, never chased.
+// Same failure family as the other two bugs in this ticket — a proxy trusted without verification.
+// Returns true/false; on ANY error returns true (assume it exists) so a George hiccup can never
+// cause a burst of duplicate drafts. Fail-safe direction is deliberate.
+function georgeHasDraft(to) {
+ const addr = String(to || '').split(/[,;]/)[0].trim();
+ if (!addr) return Promise.resolve(true);
+ const { auth } = georgeCreds();
+ const q = encodeURIComponent(`in:drafts to:${addr}`);
+ return new Promise((resolve) => {
+ const req = http.request({ host: '127.0.0.1', port: 9850, path: `/api/messages?account=info&maxResults=5&q=${q}`,
+ method: 'GET', headers: { Authorization: auth } },
+ (r) => { let b = ''; r.on('data', (d) => b += d); r.on('end', () => {
+ try { const j = JSON.parse(b); resolve((j.messages || []).length > 0); } catch { resolve(true); }
+ }); });
+ req.on('error', () => resolve(true));
+ req.end();
+ });
+}
// Create a Gmail draft in info@ Drafts (POST /api/drafts — no send-approval token needed).
function georgeDraft(payload) {
const { auth } = georgeCreds();
@@ -273,6 +296,8 @@ const run = async () => {
const RECENT = new Set(readJSON('data/recently-contacted.json', []).map((e) => String(e).toLowerCase().trim()).filter(Boolean));
try { const live = await georgeRecentRecipients(RECENT_DAYS); for (const e of live) RECENT.add(e); if (live.size) console.log(`(anti-dup pool: ${live.size} recipient(s) emailed in last ${RECENT_DAYS}d — enforced on SEND, drafted-with-flag on DRAFT)`); } catch {}
const sent = [], skipped = [], needsConfirm = [], drafted = [], suppressed = [];
+ // TK-11409: make the two previously-SILENT draft-mode outcomes visible in the report.
+ const alreadyDrafted = [], staleLedger = [];
for (const [vid, rows] of Object.entries(byVid)) {
const c = cmap[vid];
if (!c || c.noChase) { skipped.push({ vid, n: rows.length, reason: c?.noChase ? 'no-chase vendor' : 'no sample email/account on file' }); continue; }
@@ -291,8 +316,17 @@ const run = async () => {
// mode the draft itself IS the review, so everything is drafted for Steve to check + send.
if (c.needs_confirm && !DRAFT) { needsConfirm.push({ vid, name: c.name, to: (c.sample_email || c.main_email), n: rows.length, skus: rows.map((r) => r.sku), reason: 'main-email fallback — CONFIRM person vs recent emails before send' }); continue; }
if (DRAFT) {
- const fresh = rows.filter((r) => !draftLedger[r.recordId]); // only newly-outstanding SKUs
- if (!fresh.length) continue; // already drafted — no dup
+ let fresh = rows.filter((r) => !draftLedger[r.recordId]); // only newly-outstanding SKUs
+ if (!fresh.length) {
+ // TK-11409: the ledger says "already drafted" — VERIFY that before trusting it. If the
+ // draft is gone (sent, deleted, or purged by the 30-day drain), the ledger is stale and
+ // these memos would otherwise be suppressed forever with no report line at all.
+ const stillThere = await georgeHasDraft(c.sample_email || c.main_email);
+ if (stillThere) { alreadyDrafted.push({ vid, name: c.name, n: rows.length }); continue; }
+ staleLedger.push({ vid, name: c.name, to: (c.sample_email || c.main_email), n: rows.length });
+ for (const r of rows) delete draftLedger[r.recordId]; // drop the stale claim
+ fresh = rows; // and re-draft
+ }
await enrichRows(c.sample_email || c.main_email, fresh); // vendor ref/tracking when they responded
const draft = compose({ name: c.name, account_number: c.account_number, sample_email: c.sample_email, main_email: c.main_email, ship_to: SHIP_TO }, fresh.map((r) => ({ mfr: r.mfr, sku: r.sku, requested: r.requested, name: r.name, ref: r.ref })));
// Steve 9/01: the "recently emailed" flag stays in the RUN REPORT only — never in the subject
@@ -319,7 +353,7 @@ const run = async () => {
const mode = DRAFT ? 'DRAFT' : SEND ? 'SEND' : 'DRY-RUN';
const report = { ranAt: new Date().toISOString(), mode, window: win, stampDate: today,
- vendorsSent: sent.length, vendorsDrafted: drafted.length, vendorsSkipped: skipped.length, vendorsNeedsConfirm: needsConfirm.length, vendorsSuppressed: suppressed.length, sent, drafted, needsConfirm, skipped, suppressed };
+ vendorsSent: sent.length, vendorsDrafted: drafted.length, vendorsSkipped: skipped.length, vendorsNeedsConfirm: needsConfirm.length, vendorsSuppressed: suppressed.length, vendorsAlreadyDrafted: alreadyDrafted.length, vendorsStaleLedger: staleLedger.length, sent, drafted, needsConfirm, skipped, suppressed, alreadyDrafted, staleLedger };
mkdirSync(join(ROOT, 'data', 'runs'), { recursive: true });
writeFileSync(join(ROOT, 'data', 'runs', `scheduled-${today.replace(/\//g, '-')}.json`), JSON.stringify(report, null, 2));
@@ -334,6 +368,8 @@ const run = async () => {
if (needsConfirm.length) { console.log(`\n⚠ ${needsConfirm.length} main-email fallback(s) — HELD for person-confirmation (never auto-sent):`); needsConfirm.forEach((s) => console.log(` ? ${s.name.padEnd(30)} [${s.vid}] → ${s.to} (${s.n} SKU${s.n > 1 ? 's' : ''}) — ${s.reason}`)); }
}
if (suppressed.length) { console.log(`\n⊘ Suppressed ${suppressed.length} (emailed within ${RECENT_DAYS}d — SEND mode only; DRAFT would surface these flagged):`); suppressed.forEach((s) => console.log(` ⊘ ${(s.name||'').padEnd(30)} [${s.vid}] → ${s.to}`)); }
+ if (alreadyDrafted.length) { console.log(`\n= Already drafted ${alreadyDrafted.length} (draft VERIFIED still in Drafts — not duplicated):`); alreadyDrafted.forEach((s) => console.log(` = ${(s.name||'').padEnd(30)} [${s.vid}] (${s.n} SKU${s.n>1?'s':''})`)); }
+ if (staleLedger.length) { console.log(`\n\u26a0 STALE LEDGER ${staleLedger.length} — ledger claimed a draft that NO LONGER EXISTS; re-drafted:`); staleLedger.forEach((s) => console.log(` \u26a0 ${(s.name||'').padEnd(30)} [${s.vid}] \u2192 ${s.to} (${s.n} SKU${s.n>1?'s':''})`)); }
if (skipped.length) { console.log(`\nSkipped ${skipped.length} (no contact / no-chase / error):`); skipped.forEach((s) => console.log(` - [${s.vid}] ${s.n} SKU(s): ${s.reason}`)); }
console.log(`\n→ data/runs/scheduled-${today.replace(/\//g, '-')}.json`);
};
← 55c8110 TK-11409: anti-dup filter moves to the send gate only (DTD 5
·
back to Sample Followup Sweep
·
auto-data-snapshot: 2026-09-10T11:43:04 (2 data files) — dat b128bf8 →