[object Object]

← 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

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 →