[object Object]

← back to Sample Followup Sweep

scheduled-run: decouple the vid->contact bridge from the fleet snapshot (TK-11255)

6771f15945356f375a4c879610b85082db0c8395 · 2026-09-12 07:47:26 -0700 · Steve Abrams

fleet.json does DOUBLE DUTY: it is the outstanding-items snapshot (which
legitimately shrinks as samples arrive) AND the vid->contact bridge (which must
stay complete). vidContactMap built the bridge only from fleet rows, so a vendor
with no CURRENT overdue items had no contact — and the next time one went
overdue it was silently skipped until someone regenerated the file. Nobody
regenerates it: no launchd job runs build-fleet.js and scheduled-run only reads.

This was measured, not theorised. I regenerated into a SANDBOX and diffed:
  gains 4 vids (1838, BRU, FUT, VAH)
  DROPS 37 (PJ, WOL, PF, SCH, MOR, THI, ELI, LIN, RAD, TWI, KOR, ...)
A naive regeneration would therefore have inflicted the exact bug I was chasing
on 37 vendors to fix it for 4. The regenerated file was NOT promoted.

Fix: slug is derivable from vid, so the bridge can go straight to contacts.json.
vidContactMap now takes the live vid set and UNIONs fleet-derived entries with
slug-derived ones. Union only — it cannot make a vendor that resolves today stop
resolving. Derivation runs from the live VID, never backwards from the slug,
because slug->vid is lossy ('1838-wallcoverings' could come from a space or a
hyphen).

HONEST EFFECT TODAY: zero vendors recovered. Verified live — resolvable is 24 of
28 both before and after, because e343b5f (vid \r trim) and a1c761a (POI, 1838)
already recovered everything recoverable. The value here is that regenerating
fleet.json is now SAFE, and a vendor dropping out of the outstanding list no
longer quietly loses its contact. Prevention, not a fix.

Still unroutable: 1838, FUT, VAH, BRU — 8 overdue records. Those need
contacts.json entries, not code.

Known smell, deliberately not addressed here: SLUG_ALIASES now exists in both
build-fleet.js and scheduled-run.mjs. Two copies of one routing table is exactly
the failure mode that produced SAND->Sandberg; it wants consolidating into one
shared module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Files touched

Diff

commit 6771f15945356f375a4c879610b85082db0c8395
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Sep 12 07:47:26 2026 -0700

    scheduled-run: decouple the vid->contact bridge from the fleet snapshot (TK-11255)
    
    fleet.json does DOUBLE DUTY: it is the outstanding-items snapshot (which
    legitimately shrinks as samples arrive) AND the vid->contact bridge (which must
    stay complete). vidContactMap built the bridge only from fleet rows, so a vendor
    with no CURRENT overdue items had no contact — and the next time one went
    overdue it was silently skipped until someone regenerated the file. Nobody
    regenerates it: no launchd job runs build-fleet.js and scheduled-run only reads.
    
    This was measured, not theorised. I regenerated into a SANDBOX and diffed:
      gains 4 vids (1838, BRU, FUT, VAH)
      DROPS 37 (PJ, WOL, PF, SCH, MOR, THI, ELI, LIN, RAD, TWI, KOR, ...)
    A naive regeneration would therefore have inflicted the exact bug I was chasing
    on 37 vendors to fix it for 4. The regenerated file was NOT promoted.
    
    Fix: slug is derivable from vid, so the bridge can go straight to contacts.json.
    vidContactMap now takes the live vid set and UNIONs fleet-derived entries with
    slug-derived ones. Union only — it cannot make a vendor that resolves today stop
    resolving. Derivation runs from the live VID, never backwards from the slug,
    because slug->vid is lossy ('1838-wallcoverings' could come from a space or a
    hyphen).
    
    HONEST EFFECT TODAY: zero vendors recovered. Verified live — resolvable is 24 of
    28 both before and after, because e343b5f (vid \r trim) and a1c761a (POI, 1838)
    already recovered everything recoverable. The value here is that regenerating
    fleet.json is now SAFE, and a vendor dropping out of the outstanding list no
    longer quietly loses its contact. Prevention, not a fix.
    
    Still unroutable: 1838, FUT, VAH, BRU — 8 overdue records. Those need
    contacts.json entries, not code.
    
    Known smell, deliberately not addressed here: SLUG_ALIASES now exists in both
    build-fleet.js and scheduled-run.mjs. Two copies of one routing table is exactly
    the failure mode that produced SAND->Sandberg; it wants consolidating into one
    shared module.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 scripts/scheduled-run.mjs | 34 ++++++++++++++++++++++++++++++++--
 1 file changed, 32 insertions(+), 2 deletions(-)

diff --git a/scripts/scheduled-run.mjs b/scripts/scheduled-run.mjs
index 75dc0e8..728073a 100644
--- a/scripts/scheduled-run.mjs
+++ b/scripts/scheduled-run.mjs
@@ -142,10 +142,32 @@ const norm = (s) => String(s || '').toLowerCase().trim();
 const readJSON = (f, d) => { try { return JSON.parse(readFileSync(join(ROOT, f), 'utf8')); } catch { return d; } };
 
 // vid -> contact (sample_email + account), built from fleet(slug↔vid) + contacts(slug→email)
-function vidContactMap() {
+function vidContactMap(liveVids = []) {
   const fleet = readJSON('data/fleet.json', { vendors: [] }).vendors;
   const contacts = readJSON('data/contacts.json', {});
   const map = {};
+  // TK-11255: fleet.json does DOUBLE DUTY -- it is both the outstanding-items
+  // snapshot (which legitimately shrinks as samples arrive) and the vid->contact
+  // bridge (which must stay complete). Building the bridge only from fleet rows
+  // means a vendor with no CURRENT overdue items has no contact, so the next time
+  // one goes overdue it is silently skipped until someone regenerates the file.
+  // Measured 2026-09-12: the fleet was 29 days stale and 11 of 129 live overdue
+  // records were unroutable for exactly this reason; a naive regeneration would
+  // have ADDED 4 vids while dropping 37 others out of the bridge.
+  // slug is derivable from vid, so derive the bridge directly from contacts.json
+  // and treat fleet as an ADDITIVE source. Union only -- this can never make a
+  // vendor that resolves today stop resolving.
+  const SLUG_ALIASES = { 'ANNA FRENCH': 'thib', ANNA: 'thib', ARTE: 'arte-international', 'AS CREATION': 'san', BM: 'green' };
+  const slugForVid = (vid) => SLUG_ALIASES[vid] || String(vid).toLowerCase().replace(/[^a-z0-9]+/g, '-');
+  const put = (key, slug) => {
+    if (!key || map[key]) return;
+    const c = contacts[slug]; if (!c) return;
+    if (c.disposition === 'no-chase') { map[key] = { noChase: true, name: c.name || slug }; return; }
+    const email = c.sample_email || c.main_email;
+    if (!email || !c.account_number) return;
+    map[key] = { slug, name: c.name || slug, sample_email: c.sample_email, main_email: c.main_email,
+      account_number: c.account_number, needs_confirm: !c.sample_email || !!c.needs_confirm };
+  };
   for (const v of fleet) {
     const c = contacts[v.slug] || {};
     const key = String(v.vid || '').toUpperCase();   // FileMaker vid case varies (yor vs YOR)
@@ -162,6 +184,14 @@ function vidContactMap() {
       };
     }
   }
+  // Additive second pass: any vid in the LIVE overdue set whose slug resolves in
+  // contacts.json but which is absent from (or stale in) fleet.json. Derived from
+  // the live vid, not from the slug -- slug->vid is lossy ('1838-wallcoverings'
+  // could come from '1838 WALLCOVERINGS' or '1838-WALLCOVERINGS').
+  for (const raw of liveVids) {
+    const vid = String(raw || '').replace(/[\r\n]/g, '').trim().toUpperCase();
+    if (vid) put(vid, slugForVid(vid));
+  }
   return map;
 }
 
@@ -299,7 +329,7 @@ const run = async () => {
     });
   }
 
-  const cmap = vidContactMap();
+  const cmap = vidContactMap(Object.keys(byVid));
   // DRAFT dedup ledger — drafting ≠ sending, so we do NOT false-stamp FileMaker; we track which
   // recordIds already have a draft locally so the daily run never piles up duplicate drafts.
   const LEDGER = 'data/draft-ledger.json';

← a1c761a contacts.json: unblock POI + 1838, both silently skipped by  ·  back to Sample Followup Sweep  ·  contacts.json: route VAH + BRU + bare-1838 — 27 of 28 live v 5c0d68e →